import { CN_NUM } from "../release-parser/cn-number.ts"; import { toSimplified } from "./zh-map.ts"; const SEASON_SUFFIX = [ new RegExp(`\\s*第\\s*${CN_NUM}\\s*[季部]\\s*$`, "u"), /\s*[::-]?\s*(?:season|series)\s*\d+\s*$/iu, /\s+S\d{1,2}\s*$/iu, /\s*[((]\s*(?:19|20)\d{2}\s*[))]\s*$/u, ]; /** 去掉标题末尾的季号/年份后缀("庆余年 第二季"、"Show Season 2"、"Title (2019)") */ export function stripSeasonSuffix(s: string): string { let out = s.trim(); for (let changed = true; changed; ) { changed = false; for (const re of SEASON_SUFFIX) { const next = out.replace(re, ""); if (next !== out && next.trim().length > 0) { out = next.trim(); changed = true; } } } return out; } /** * 标题归一化(用于比较,不用于展示): * 全角→半角(NFKC)、繁→简、小写、& → and、去季号后缀、去标点与空白。 */ export function normalizeTitle(s: string): string { const base = stripSeasonSuffix(toSimplified(s.normalize("NFKC"))); return base .toLowerCase() .replace(/&/g, "and") .replace(/[^\p{L}\p{N}]+/gu, ""); } /** 分类词(站点分类 / RSS 标题前缀)与常见停用词:不能当片名或别名用于身份比对(ID-2) */ const GENERIC_NAMES = new Set( [ "电影", "电视剧", "剧集", "连续剧", "动漫", "动画", "番剧", "国漫", "日漫", "综艺", "纪录片", "记录片", "音乐", "短剧", "微短剧", "体育", "游戏", "软件", "学习", "其他", "其它", "未知", "新", "合集", "全集", "中字", "国语", "粤语", "高清", "蓝光", "原盘", "movie", "movies", "tv", "tvseries", "series", "anime", "animation", "documentary", "music", "variety", "show", "the", "season", "complete", "unknown", "other", ].map((s) => s.toLowerCase()), ); /** * 归一化后的名称是否太"通用"不能用于身份比对:分类词、停用词、单个汉字、两个以内的拉丁字符、纯数字。 * 参数是 normalizeTitle 之后的形态。 */ export function isGenericAliasName(norm: string): boolean { if (!norm) return true; if (GENERIC_NAMES.has(norm)) return true; if (/^\d+$/.test(norm)) return true; const han = norm.match(/\p{Script=Han}/gu)?.length ?? 0; if (han > 0) return [...norm].length < 2; return norm.length <= 2; }