- 新表 identify_words(0094):全局一块 + 每部作品一块原文,带版本与审计;episode_mapping_rule 转成文本行后删表
- 全局识别词移植 MP WordsMatcher(屏蔽 / 替换 / 集数偏移 / 替换+偏移 / {[tmdbid=…]}),所有按标题 / 文件名识别的地方解析前套用,命中的行记在解析结果上;保存后后台按改动行重新识别
- 作品识别词:S02 >> S01 EP+24 / S01E13 >> S00E01 / S01 @组 >> 忽略,解析器 + 打印器 + 编译;发布组繁简视为同一个
- 去掉规则自动停用:算出来对不上季表只待确认,要停用就注释掉那一行
- 集数待确认的采用 / 忽略与智能体 apply_rule 改为往作品识别词追加一行(带来历注释)
- 编辑器可视化 / 文本两种模式改同一份原文,保存前试算;作品页「集数映射」用同一个编辑器
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
122 lines
5.0 KiB
TypeScript
122 lines
5.0 KiB
TypeScript
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<EpisodeResolutionMethod, string> = {
|
||
raw: "原号",
|
||
rule: "按作品识别词",
|
||
absolute_in_season: "季内绝对集号",
|
||
absolute_cross_season: "跨季绝对集号",
|
||
air_date: "按播出日期",
|
||
tmdb_season: "按 TMDB 默认季集换算",
|
||
unverified: "元数据未覆盖,先按原号",
|
||
unresolved: "待确认",
|
||
skipped: "不适用",
|
||
};
|
||
|
||
export const SPECIAL_LABEL: Record<EpisodeSpecialKind, string> = {
|
||
special: "特别篇",
|
||
extra: "花絮",
|
||
ignore: "忽略",
|
||
};
|
||
|
||
/** 校验状态:状态色只表示状态 */
|
||
export const VALIDATION: Record<EpisodeValidationState, { label: string; tone: StatusTone }> = {
|
||
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<EpisodeResolutionView, "method" | "special"> | 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<EpisodeResolutionView, "source"> | 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<EpisodeResolutionView, "canonical" | "special" | "method"> | 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(",")}`;
|
||
}
|