// 季总集数账本(对照 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 }; }