Files
UHD_Media_Tool/packages/domain/src/slot/episode-total.ts
T
rootandClaude Opus 5.5 d162d464c4 feat(domain): W5-C subscription gaps, episode totals, multi-inflight slot, hard conditions and priority ladder
- resolveEpisodeTotal: totals only grow; TMDB regression floored at highest covered episode; manual lock
- computeUnitGaps / seasonProgress / unitsOverlap: missing = units ∩ range − covered − in-flight,
  season wildcard reservation, suspected-missing when total unknown
- transitionWithInflight: slot status aggregates several in-flight plans (Slot × unit reservation)
- hard conditions: excludeSources, hdr sets, codec sets, per-unit size, include/exclude regex
  (whitelist-validated), site scope, minSeeders, freeOnly (site facts via EvaluationContext)
- priority ladder (MP rule-group tiers) → pri 0-100 with per-tier trace; priority-basis upgrades,
  targetPri ceiling, upgrade modes movie/per_unit/full_pack, improvableUnits
- all evaluations carry readable trace via describeViolation

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 23:33:46 +08:00

76 lines
2.7 KiB
TypeScript

// 季总集数账本(对照 MP __resolve_effective_total_episode / __resolve_total_episode_decrease /
// SubscribeEpisodesRefresh 只升注入):纯函数,来源值由调用方给。
export type EpisodeTotalSource = "tmdb" | "injected" | "manual" | "covered_floor" | "previous";
export interface EpisodeTotalInput {
/** 账本里现有的总集数(null = 未知) */
previous: number | null;
/** 元数据(TMDB)本次给出的集数;0 / null = 未公布 */
fresh?: number | null;
/** 插件/Agent 注入值:只升不降 */
injected?: number | null;
/** 已存在(入库覆盖)的最高集号:回退时不得低于它 */
coveredMax?: number | null;
/** 人工锁定:锁定后元数据与注入都不再改写 */
locked?: boolean;
}
export interface EpisodeTotalResult {
total: number | null;
/** 总集数是否已知(未知时追更不结束) */
known: boolean;
source: EpisodeTotalSource;
changed: boolean;
/** 元数据回退被已有最高集号兜住 */
clampedByCovered: boolean;
}
const pos = (v: number | null | undefined): number | null =>
typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : null;
/**
* 解析总集数:
* - 锁定 → 保持现值;
* - 元数据变大 → 采用(追更范围扩大);元数据变小 → 取 max(新值, 已存在最高集号)(防 TMDB 临时回退误完成);
* - 注入值只升;元数据未公布(0/null)→ 保持现值,现值也没有 → 未知。
*/
export function resolveEpisodeTotal(input: EpisodeTotalInput): EpisodeTotalResult {
const prev = pos(input.previous);
const covered = pos(input.coveredMax);
const keep = (source: EpisodeTotalSource = "previous"): EpisodeTotalResult => ({
total: prev,
known: prev !== null,
source,
changed: false,
clampedByCovered: false,
});
if (input.locked) return keep("manual");
const fresh = pos(input.fresh);
const injected = pos(input.injected);
let total = prev;
let source: EpisodeTotalSource = "previous";
let clamped = false;
if (fresh !== null) {
if (prev === null || fresh >= prev) {
total = fresh;
source = "tmdb";
} else {
const floor = Math.max(fresh, covered ?? 0);
total = floor;
source = floor === fresh ? "tmdb" : "covered_floor";
clamped = floor !== fresh;
}
}
if (injected !== null && (total === null || injected > total)) {
total = injected;
source = "injected";
clamped = false;
}
if (total === null && covered !== null) {
// 没有任何来源:已见过的最高集只说明"至少这么多",不当作已知总数
return { total: null, known: false, source: "previous", changed: false, clampedByCovered: false };
}
return { total, known: total !== null, source, changed: total !== prev, clampedByCovered: clamped };
}