feat(domain): pure business logic for parsing, identity, slots, acquisition, paths, organize, download state (W1-2)
- release-parser: title/file/dir parsing with MP parity + deviations (airDate/issue, cut, release group suffix fallback, dir+file merge, safe episode offset) - identity: title normalization (NFKC, trad->simp map, season suffix) and evidence-level candidate ranking (year tolerance ±1, no probabilities) - slot: hard/soft/upgrade evaluation (missing vs unmet fields), state machine, movie/unit completion (season pack must be covered by actual file list) - acquisition: new_title/catch_up/upgrade modes, priority order, budget, cache volume hard threshold with resume watermark (no double counting) - path: immutable path bindings, restricted naming templates, sanitize, conflict detection, downloader<->host path mapping - organize: planOrganize with sidecar pairing, sample exclusion, existing target policy, crossDevice marking, H&R -> conflict for move - download-state: qB state mapping, completion, no-progress evidence 230 tests, ~97% statement coverage. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
a6d049db81
commit
7ba712ea55
40 files changed
+5885
-2
No files matched your search
@@ -0,0 +1,59 @@
|
||||
export interface AcquisitionBudget {
|
||||
maxNewTitlesPerDay?: number;
|
||||
maxBytesPerDay?: number;
|
||||
maxConcurrent?: number;
|
||||
}
|
||||
|
||||
export interface BudgetUsage {
|
||||
newTitlesToday: number;
|
||||
bytesToday: number;
|
||||
/** 已预留但尚未计入 bytesToday 的计划字节 */
|
||||
reservedBytesToday?: number;
|
||||
activeDownloads: number;
|
||||
}
|
||||
|
||||
export interface BudgetRequest {
|
||||
countsAsNewTitle: boolean;
|
||||
bytes?: number;
|
||||
}
|
||||
|
||||
export interface BudgetViolation {
|
||||
kind: "new_titles" | "bytes" | "concurrency";
|
||||
limit: number;
|
||||
current: number;
|
||||
requested: number;
|
||||
}
|
||||
|
||||
export interface BudgetDecision {
|
||||
decision: "allow" | "deny" | "unknown";
|
||||
violations: BudgetViolation[];
|
||||
/** 有字节预算但候选体积未知 */
|
||||
unknown: "bytes"[];
|
||||
}
|
||||
|
||||
/** 预算检查(每日新增作品数、下载字节、并发)。已知超限 → deny;体积未知 → unknown(不放行) */
|
||||
export function checkBudget(budget: AcquisitionBudget, usage: BudgetUsage, req: BudgetRequest): BudgetDecision {
|
||||
const violations: BudgetViolation[] = [];
|
||||
const unknown: "bytes"[] = [];
|
||||
if (
|
||||
req.countsAsNewTitle &&
|
||||
budget.maxNewTitlesPerDay !== undefined &&
|
||||
usage.newTitlesToday + 1 > budget.maxNewTitlesPerDay
|
||||
)
|
||||
violations.push({
|
||||
kind: "new_titles",
|
||||
limit: budget.maxNewTitlesPerDay,
|
||||
current: usage.newTitlesToday,
|
||||
requested: 1,
|
||||
});
|
||||
if (budget.maxBytesPerDay !== undefined) {
|
||||
const current = usage.bytesToday + (usage.reservedBytesToday ?? 0);
|
||||
if (req.bytes === undefined) unknown.push("bytes");
|
||||
else if (current + req.bytes > budget.maxBytesPerDay)
|
||||
violations.push({ kind: "bytes", limit: budget.maxBytesPerDay, current, requested: req.bytes });
|
||||
}
|
||||
if (budget.maxConcurrent !== undefined && usage.activeDownloads + 1 > budget.maxConcurrent)
|
||||
violations.push({ kind: "concurrency", limit: budget.maxConcurrent, current: usage.activeDownloads, requested: 1 });
|
||||
const decision = violations.length > 0 ? "deny" : unknown.length > 0 ? "unknown" : "allow";
|
||||
return { decision, violations, unknown };
|
||||
}
|
||||
Reference in new issue
Block a user