import type { EpisodeMappingPreview, EpisodeReresolveResult, EpisodeResolutionMethod, EpisodeResolutionView, EpisodeSpecialKind, EpisodeValidationState, } from "@umt/contracts"; import type { StatusTone } from "~/components/console/status-badge.tsx"; // 集数映射的界面文案与纯函数:方式名称、季集写法、作品页「集数映射」分区的地址。 // 规则本身是作品识别词(components/identify-words),这里不再有结构化规则。 /** 识别方式的人话 */ export const METHOD_LABEL: Record = { raw: "原号", rule: "按作品识别词", absolute_in_season: "季内绝对集号", absolute_cross_season: "跨季绝对集号", air_date: "按播出日期", tmdb_season: "按 TMDB 默认季集换算", unverified: "元数据未覆盖,先按原号", unresolved: "待确认", skipped: "不适用", }; export const SPECIAL_LABEL: Record = { special: "特别篇", extra: "花絮", ignore: "忽略", }; /** 校验状态:状态色只表示状态 */ export const VALIDATION: Record = { pass: { label: "对得上", tone: "success" }, unverified: { label: "未核对", tone: "warning" }, fail: { label: "对不上", tone: "destructive" }, special: { label: "不按集整理", tone: "neutral" }, skipped: { label: "不适用", tone: "neutral" }, }; /** 方式名称:按规则映射成特别篇等时写具体去向 */ export function methodText(r: Pick | null | undefined): string { if (!r) return "未解析"; if (r.special) return `按作品识别词:${SPECIAL_LABEL[r.special]}`; return METHOD_LABEL[r.method] ?? r.method; } /** 绝对集号类方式:订阅表单据此提示「这个发布组用的是绝对集号」 */ export const isAbsoluteMethod = (m: EpisodeResolutionMethod) => m === "absolute_in_season" || m === "absolute_cross_season"; const pad = (n: number) => String(n).padStart(2, "0"); /** 季集写法:S01E25、S02E01-E02;只有集号没季号写 E25 */ export function episodeCode(season: number | null | undefined, episodes: { start: number; end: number } | undefined) { if (!episodes) return season != null ? `S${pad(season)}` : ""; const s = season != null ? `S${pad(season)}` : ""; const e = episodes.start === episodes.end ? `E${pad(episodes.start)}` : `E${pad(episodes.start)}-E${pad(episodes.end)}`; return `${s}${e}`; } /** 发布者声称的季集(原始) */ export function sourceText(r: Pick | null | undefined): string { if (!r) return "—"; const { season, episodes, airDate } = r.source; if (episodes) return episodeCode(season, episodes); if (airDate) return airDate.replace(/^(\d{4})(\d{2})(\d{2})$/, "$1-$2-$3"); return "—"; } /** 媒体库应当使用的季集(规范);按规则去特别篇等写去向,换不出写「—」 */ export function canonicalText(r: Pick | null | undefined) { if (!r) return "—"; if (r.special) return SPECIAL_LABEL[r.special]; if (r.canonical) return episodeCode(r.canonical.season, r.canonical.episodes); return "—"; } export const scopeText = (season: number | null | undefined, group: string | null | undefined) => `${season == null ? "任意季" : `第 ${season} 季`} · ${group?.trim() || "任意发布组"}`; // ── 作品页「集数映射」分区的地址(集数待确认「修改」跳转时带上要追加的一行) ── export const MAPPING_ANCHOR = "episode-mapping"; const LINE_PARAM = "mapLine"; export function mappingHref(libraryPath: string, line?: string): string { const qs = line ? `?${new URLSearchParams({ [LINE_PARAM]: line })}` : ""; return `${libraryPath}${qs}#${MAPPING_ANCHOR}`; } /** 从地址读要追加进作品识别词草稿的一行;没有返回 null */ export function lineFromSearch(sp: URLSearchParams): string | null { return sp.get(LINE_PARAM)?.trim() || null; } // ── 统计 ── /** 按发布组的方式统计一句话:`ANi:跨季绝对集号 12、原号 3`(只列非零) */ export function groupMethodsText(g: EpisodeMappingPreview["groups"][number]): string { const parts = [...g.methods].sort((a, b) => b.count - a.count).map((m) => `${METHOD_LABEL[m.method]} ${m.count}`); return `${g.releaseGroup ?? "未知发布组"}:${parts.join("、")}`; } /** 用绝对集号的发布组(订阅表单提示用) */ export function absoluteGroups(p: EpisodeMappingPreview | undefined): string[] { if (!p) return []; return p.groups .filter((g) => g.methods.some((m) => isAbsoluteMethod(m.method))) .map((g) => g.releaseGroup ?? "未知发布组"); } /** 规则写入后的提示:只列非零项 */ export function reresolveToast(r: EpisodeReresolveResult, verb = "已保存"): string { const parts = [`重算 ${r.releases} 条资源`]; if (r.changed) parts.push(`${r.changed} 条有变化`); if (r.resolved) parts.push(`${r.resolved} 条待确认已解决`); return `${verb}:${parts.join(",")}`; }