import type { NamingEntityInput, NamingKind } from "./variables.ts"; // 默认模板(与 MP 一致,综艺为我们特有)、v1 旧语法迁移、三级覆盖回落、预览内置样本。 export interface NamingTemplates { movie: string; tv: string; variety: string; } export type NamingOverride = Partial; export type NamingSource = "entity" | "root" | "global"; const TITLE_YEAR = "{{title}}{% if year %} ({{year}}){% endif %}"; /** MP 默认(app/runtime/config.py MOVIE_RENAME_FORMAT / TV_RENAME_FORMAT 原样)+ 综艺(我们特有:无集号按播出日期) */ export const MP_DEFAULT_NAMING: Readonly = { movie: `${TITLE_YEAR}/${TITLE_YEAR}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}{{fileExt}}`, tv: `${TITLE_YEAR}/Season {{season}}/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}{{fileExt}}`, variety: `${TITLE_YEAR}/Season {{season}}/{{title}} - {% if season_episode and episode %}{{season_episode}}{% else %}{{airDate | date('YYYY-MM-DD')}}{% endif %}{% if issue %} - 第 {{issue}} 期{% endif %}{{fileExt}}`, }; /** 当前默认模板版本号(path_binding.template_version 的全局默认;1 = v1 旧模板) */ export const LEGACY_NAMING_VERSION = 1; // ── v1 旧语法:{var}、{var:02}、{airDate:dash}、[[ 可选段 ]] ── const V1_VAR_MAP: Record = { originalTitle: "original_title", seasonEpisode: "season_episode", resolution: "videoFormat", source: "resourceType", hdr: "effect", episodeEnd: "episode", }; const V1_FORMAT: Record = { dash: " | date('YYYY-MM-DD')", dot: " | date('YYYY.MM.DD')", }; function upgradeParts(src: string, vars: string[]): string { return src.replace(/\{([A-Za-z]+)(?::([A-Za-z0-9-]+))?\}/g, (_m, name: string, fmt?: string) => { const v = Object.hasOwn(V1_VAR_MAP, name) ? V1_VAR_MAP[name]! : name; if (!vars.includes(v)) vars.push(v); let f = ""; if (fmt) { const pad = /^0(\d)$/.exec(fmt); f = pad ? ` | pad(${pad[1]})` : Object.hasOwn(V1_FORMAT, fmt) ? V1_FORMAT[fmt]! : ""; } return `{{${v}${f}}}`; }); } /** v1 单段模板 → 新语法:`[[ … ]]` 可选段改写为 `{% if 段内变量 %}…{% endif %}` */ export function upgradeLegacyTemplate(v1: string): string { let out = ""; let i = 0; while (i < v1.length) { const open = v1.indexOf("[[", i); if (open === -1) { out += upgradeParts(v1.slice(i), []); break; } out += upgradeParts(v1.slice(i, open), []); const end = v1.indexOf("]]", open + 2); if (end === -1) { out += upgradeParts(v1.slice(open), []); break; } const vars: string[] = []; const inner = upgradeParts(v1.slice(open + 2, end), vars); out += vars.length ? `{% if ${vars.join(" and ")} %}${inner}{% endif %}` : inner; i = end + 2; } return out; } /** v1 六条子模板(目录/季目录/文件名分开)的形状 */ export interface LegacyNamingTemplateSet { movieDir: string; tvDir: string; seasonDir: string; movieFile: string; episodeFile: string; dailyFile: string; } export const LEGACY_V1_TEMPLATES: Readonly = { movieDir: "{title}[[ ({year})]]", tvDir: "{title}[[ ({year})]]", seasonDir: "Season {season}", movieFile: "{title}[[ ({year})]][[ - {cut}]][[ - {part}]][[ - {resolution}]]", episodeFile: "{title} - {seasonEpisode}[[ - {part}]]", dailyFile: "{title} - {airDate:dash}[[ - 第{issue}期]]", }; /** v1 六条 → 新的三条全路径模板;综艺 = 有集号用剧集文件名,否则按日期 */ export function upgradeLegacyTemplateSet(t: LegacyNamingTemplateSet): NamingTemplates { const u = upgradeLegacyTemplate; const tvPrefix = `${u(t.tvDir)}/${u(t.seasonDir)}/`; return { movie: `${u(t.movieDir)}/${u(t.movieFile)}{{fileExt}}`, tv: `${tvPrefix}${u(t.episodeFile)}{{fileExt}}`, variety: `${tvPrefix}{% if season_episode %}${u(t.episodeFile)}{% else %}${u(t.dailyFile)}{% endif %}{{fileExt}}`, }; } /** v1 旧模板(= LEGACY_NAMING_VERSION),锁定文件名模板版本时给已有 template_version=1 的作品用 */ export const LEGACY_V1_NAMING: Readonly = upgradeLegacyTemplateSet(LEGACY_V1_TEMPLATES); /** 模板集指纹的规范化输入(server 与迁移用 sha256 求指纹,相同内容 = 同一版本) */ export function namingCanonicalJson(t: NamingTemplates): string { return JSON.stringify({ movie: t.movie, tv: t.tv, variety: t.variety }); } // ── 覆盖层级:作品 > 库根 > 全局,按类型分别回落 ── export function resolveNamingTemplates(layers: { global: NamingTemplates; root?: NamingOverride | null | undefined; entity?: NamingOverride | null | undefined; }): { templates: NamingTemplates; sources: Record } { const pick = (k: NamingKind): [string, NamingSource] => { const e = layers.entity?.[k]; if (e) return [e, "entity"]; const r = layers.root?.[k]; if (r) return [r, "root"]; return [layers.global[k], "global"]; }; const [movie, sm] = pick("movie"); const [tv, st] = pick("tv"); const [variety, sv] = pick("variety"); return { templates: { movie, tv, variety }, sources: { movie: sm, tv: st, variety: sv } }; } /** 作品类型 → 命名类型(series 及其它剧集类 = tv;collection/other 按电影) */ export function namingKindOf(entityKind: string): NamingKind { if (entityKind === "variety") return "variety"; if (entityKind === "series" || entityKind === "tv") return "tv"; return "movie"; } // ── 预览内置样本 ── export interface NamingSample { id: number; label: string; kind: NamingKind; fileName: string; entity: NamingEntityInput; } export const NAMING_SAMPLES: readonly NamingSample[] = [ { id: 1, label: "电影", kind: "movie", fileName: "Dune.Part.Two.2024.2160p.UHD.BluRay.REMUX.DV.HDR.HEVC.TrueHD.7.1.Atmos-FraMeSToR.mkv", entity: { title: "沙丘2", originalTitle: "Dune: Part Two", enTitle: "Dune: Part Two", year: 2024, tmdbId: 693134 }, }, { id: 2, label: "电影分段", kind: "movie", fileName: "Inception.2010.CD1.1080p.BluRay.x264-CMCT.mkv", entity: { title: "盗梦空间", enTitle: "Inception", year: 2010, tmdbId: 27205 }, }, { id: 3, label: "剧集单集", kind: "tv", fileName: "Breaking.Bad.S01E01.2160p.WEB-DL.DV.H265.DDP5.1-CMCT.mkv", entity: { title: "绝命毒师", enTitle: "Breaking Bad", year: 2008, tmdbId: 1396, seasons: { 1: { year: 2008, episodeTotal: 7 } }, episodes: { "1:1": { title: "Pilot", airDate: "2008-01-20" } }, }, }, { id: 4, label: "剧集多集", kind: "tv", fileName: "Breaking.Bad.S01E01-E03.1080p.BluRay.x264-CMCT.mkv", entity: { title: "绝命毒师", enTitle: "Breaking Bad", year: 2008, tmdbId: 1396 }, }, { id: 5, label: "特别篇 S00", kind: "tv", fileName: "Breaking.Bad.S00E03.1080p.WEB-DL.H264-NTb.mkv", entity: { title: "绝命毒师", enTitle: "Breaking Bad", year: 2008, tmdbId: 1396 }, }, { id: 6, label: "综艺按日期", kind: "variety", fileName: "快乐大本营.20260927.第12期.1080p.WEB-DL.H264.mp4", entity: { title: "快乐大本营", kind: "variety" }, }, ];