- 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]>
61 lines
2.8 KiB
TypeScript
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" };
|
|
}
|