- 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]>
60 lines
2.0 KiB
TypeScript
60 lines
2.0 KiB
TypeScript
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 };
|
|
}
|