- 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]>
76 lines
2.7 KiB
TypeScript
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 };
|
|
}
|