import { confidenceOf, parseRelease } from "./parse.ts"; import type { Confidence, PackHint, ParsedRelease, ParseOptions } from "./types.ts"; export interface PathSegments { file: string; parent?: string; grandParent?: string; } const EXTRAS_DIR = /^(?:featurettes?|extras?|bonus(?:es)?|behind[ ._-]the[ ._-]scenes|deleted[ ._-]scenes|interviews?|trailers?|shorts|scenes|花絮|特典|特辑|特輯|幕后|幕後)$/iu; const SAMPLE_DIR = /^samples?$/i; /** 把路径拆成 文件 / 父目录 / 祖父目录;附加内容目录会被跳过并记为 extra */ export function splitReleasePath(path: string): PathSegments & { extrasDir?: string; sampleDir?: boolean } { const parts = path.split(/[\\/]+/).filter((p) => p.length > 0); const file = parts.pop() ?? ""; let parent = parts.pop(); let extrasDir: string | undefined; let sampleDir = false; if (parent && (EXTRAS_DIR.test(parent) || SAMPLE_DIR.test(parent))) { if (SAMPLE_DIR.test(parent)) sampleDir = true; else extrasDir = parent; parent = parts.pop(); } const grandParent = parts.pop(); return { file, ...(parent !== undefined ? { parent } : {}), ...(grandParent !== undefined ? { grandParent } : {}), ...(extrasDir ? { extrasDir } : {}), ...(sampleDir ? { sampleDir } : {}), }; } /** 合并后的置信度:由证据推导,多次合并不会把降级"洗掉" */ function mergedConfidence(r: ParsedRelease): Confidence { let conf = confidenceOf(r); if (r.evidence.some((e) => /_dir:/.test(e))) conf = minConfidence(conf, "multi_evidence"); if (r.evidence.some((e) => e.endsWith(":type_inferred"))) conf = "needs_confirm"; if (r.mediaTypeHint !== "tv" && r.evidence.some((e) => e.endsWith(":conflict_year"))) conf = "needs_confirm"; return conf; } const RANK: Record = { rule_confirmed: 2, multi_evidence: 1, needs_confirm: 0 }; const minConfidence = (a: Confidence, b: Confidence): Confidence => (RANK[a] <= RANK[b] ? a : b); function filePackHint(r: ParsedRelease): PackHint { if (r.airDate !== undefined || r.issue !== undefined) return "single"; if (r.episodes) return r.episodes.end > r.episodes.start ? "multi_episode" : "single"; return r.mediaTypeHint === "tv" ? "unknown" : "single"; } /** * 文件优先、上级目录只补空字段(对照 MP MetaInfoPath.merge)。 * 偏离:文件本身无剧集信号而目录是剧集时,MP 不合并;本项目合并名称与季号, * 打 `:type_inferred` 证据并把置信度降为 needs_confirm。 */ export function mergeParsed( primary: ParsedRelease, fallback: ParsedRelease, level: "parent_dir" | "grandparent_dir", ): ParsedRelease { const r: ParsedRelease = { ...primary, audio: [...primary.audio], hdr: [...primary.hdr], variantTags: [...primary.variantTags], evidence: [...primary.evidence], }; const used: string[] = []; // 目录只有日期(`…Collection.20240501.FRDS` 这类收藏/打包日期)不算剧集信号,不参与类型合并 const dateOnly = fallback.airDate !== undefined && fallback.season === undefined && fallback.episodes === undefined && fallback.issue === undefined && fallback.totalEpisodes === undefined; const fbTv = fallback.mediaTypeHint === "tv" && !dateOnly; const deviation = primary.mediaTypeHint !== "tv" && fbTv; if (!primary.title) { if (fallback.title) { r.title = fallback.title; r.titleCn = fallback.titleCn; r.titleEn = fallback.titleEn; used.push("title"); } } if (r.year === undefined && fallback.year !== undefined) { r.year = fallback.year; used.push("year"); } if ((r.season === undefined || r.seasonInferred) && fallback.season !== undefined && !fallback.seasonInferred) { r.season = fallback.season; r.seasonInferred = undefined; if (!r.isFile && fallback.seasonEnd !== undefined) r.seasonEnd = fallback.seasonEnd; used.push("season"); } const fill = (k: K) => { if (r[k] === undefined && fallback[k] !== undefined) { r[k] = fallback[k]; used.push(String(k)); } }; for (const k of [ "resolution", "source", "videoCodec", "encoder", "bitDepth", "releaseGroup", "cut", "webSource", "uhd", "fps", "subtitles", ] as const) fill(k); if (used.includes("source")) r.isEncode = fallback.isEncode; if (r.audio.length === 0 && fallback.audio.length > 0) { r.audio = [...fallback.audio]; used.push("audio"); } if (r.hdr.length === 0 && fallback.hdr.length > 0) { r.hdr = [...fallback.hdr]; used.push("hdr"); } if (r.explicitIds.length === 0 && fallback.explicitIds.length > 0) { r.explicitIds = [...fallback.explicitIds]; used.push("explicitIds"); } if (r.contentType === "unknown" && fallback.contentType !== "unknown") { r.contentType = fallback.contentType; used.push("contentType"); } if (fbTv) r.mediaTypeHint = "tv"; for (const u of used) r.evidence.push(`${level}:${u}`); if (r.isFile) { r.packHint = filePackHint(r); r.isSeasonPack = false; } if (deviation && used.length > 0) r.evidence.push(`${level}:type_inferred`); if (primary.year !== undefined && fallback.year !== undefined && primary.year !== fallback.year) { // 电影合集年份冲突:以文件为准,但需要确认 r.evidence.push(`${level}:conflict_year`); } r.confidence = mergedConfidence(r); return r; } /** * 目录名 + 文件名合并识别。纯函数,不访问文件系统;最多看两级上级目录。 * 附加内容目录(Featurettes/Extras/花絮…)不参与命名,结果标记 contentType=extra。 */ export function parseReleasePath(path: string | PathSegments, opts: ParseOptions = {}): ParsedRelease { const seg = typeof path === "string" ? splitReleasePath(path) : path; const { subtitle: _subtitle, ...dirOpts } = opts; let r = parseRelease(seg.file, { ...opts, isFile: true }); if (seg.parent) r = mergeParsed(r, parseRelease(seg.parent, { ...dirOpts, isFile: false }), "parent_dir"); if (seg.grandParent) r = mergeParsed(r, parseRelease(seg.grandParent, { ...dirOpts, isFile: false }), "grandparent_dir"); const extras = typeof path === "string" ? splitReleasePath(path) : undefined; if (extras?.extrasDir && seg.parent) { // 附加内容归属父作品:名称取父目录,文件自身名称不参与身份识别 const owner = parseRelease(seg.parent, { ...dirOpts, isFile: false }); r.title = owner.title; r.titleCn = owner.titleCn; r.titleEn = owner.titleEn; r.contentType = "extra"; r.evidence.push("dir:extras"); r.confidence = "needs_confirm"; } if (extras?.sampleDir) r.evidence.push("dir:sample"); return r; }