Files
UHD_Media_Tool/packages/domain/src/acquisition/mode.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

61 lines
2.8 KiB
TypeScript

/** "每日新资源"拆成的三种模式(REQ §6.3) */
export type AcquisitionMode = "new_title" | "catch_up" | "upgrade";
export interface ModeInput {
/** 本地实体;null = 库里没有这部作品 */
entity: { tracked: boolean; kind: "movie" | "tv" } | null;
/** 候选覆盖的 content_unit(剧集/综艺);电影留空 */
releaseUnits: readonly string[];
/** Slot 中已满足的 content_unit */
satisfiedUnits: readonly string[];
/** 电影:该 Slot 是否已满足 */
slotSatisfied: boolean;
/** 作品发行/首播日期 YYYY-MM-DD(不是站点上传日期) */
firstReleaseDate?: string;
/** 当前时间(ISO),由调用方注入 */
now: string;
/** 新作品窗口(天) */
newTitleWindowDays: number;
/** evaluateCandidate 对当前资产给出了 upgrade */
upgradeCandidate?: boolean;
}
export type ModeDecision = { mode: AcquisitionMode; reason: string } | { mode: null; reason: string };
const DAY_MS = 86_400_000;
/**
* 判定一条候选属于哪种收录模式。
* - 库里没有:只有发行/首播在窗口内才是新作品;站点今天上传旧片不算新片
* - 已有剧集:候选覆盖未满足的单元 → 追更补缺;都已满足且可升级 → 升级
* - 已有电影:Slot 缺口或更优版本 → 升级(独立于上映日期)
* - 只登记(未追踪)的作品不因新规则隐式变成自动下载
*/
export function determineMode(input: ModeInput): ModeDecision {
const { entity } = input;
if (!entity) {
if (!input.firstReleaseDate) return { mode: null, reason: "release_date_unknown" };
const released = Date.parse(`${input.firstReleaseDate}T00:00:00Z`);
const now = Date.parse(input.now);
if (Number.isNaN(released) || Number.isNaN(now)) return { mode: null, reason: "release_date_unknown" };
const ageDays = (now - released) / DAY_MS;
if (ageDays < -1) return { mode: null, reason: "not_yet_released" };
if (ageDays <= input.newTitleWindowDays) return { mode: "new_title", reason: "released_within_window" };
return { mode: null, reason: "not_new_release" };
}
if (!entity.tracked) return { mode: null, reason: "not_tracked" };
if (entity.kind === "tv") {
const satisfied = new Set(input.satisfiedUnits);
const gaps = input.releaseUnits.filter((u) => !satisfied.has(u));
if (gaps.length > 0) return { mode: "catch_up", reason: `covers_missing_units:${gaps.length}` };
if (input.releaseUnits.length === 0) return { mode: null, reason: "no_units_resolved" };
return input.upgradeCandidate
? { mode: "upgrade", reason: "better_version" }
: { mode: null, reason: "already_satisfied" };
}
if (!input.slotSatisfied) return { mode: "upgrade", reason: "slot_gap" };
return input.upgradeCandidate
? { mode: "upgrade", reason: "better_version" }
: { mode: null, reason: "already_satisfied" };
}