Files
UHD_Media_Tool/packages/domain/src/acquisition/budget.ts
T
rootandClaude Fable 5.1 7ba712ea55 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]>
2026-09-27 18:11:29 +08:00

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 };
}