import type { ParsedRelease } from "../release-parser/types.ts"; // 命名变量全表(docs/design/naming-template.md §3):名称照抄 MP(驼峰/下划线混用不改),ours = 我们特有。 export type NamingKind = "movie" | "tv" | "variety"; export type NamingVarGroup = "title" | "episode" | "tech" | "id" | "ours"; export interface NamingVariableDef { name: string; group: NamingVarGroup; label: string; example: string; ours: boolean; /** 这些类型下总有值(其余情况可能为空,模板里建议用 {% if %} 包住) */ always?: readonly NamingKind[]; } const ALL: readonly NamingKind[] = ["movie", "tv", "variety"]; const SERIES: readonly NamingKind[] = ["tv", "variety"]; export const NAMING_VARIABLES: readonly NamingVariableDef[] = [ { name: "title", group: "title", label: "作品标题(本地作品名)", example: "绝命毒师", ours: false, always: ALL }, { name: "en_title", group: "title", label: "英文标题", example: "Breaking Bad", ours: false }, { name: "original_title", group: "title", label: "原语种标题", example: "Breaking Bad", ours: false }, { name: "name", group: "title", label: "文件名识别出的标题", example: "Breaking Bad", ours: false }, { name: "year", group: "title", label: "年份(作品年份,缺失取文件名年份)", example: "2008", ours: false }, { name: "title_year", group: "title", label: "标题 (年份);无年份只有标题", example: "绝命毒师 (2008)", ours: false, always: ALL, }, { name: "type", group: "title", label: "类型:电影 / 电视剧 / 综艺", example: "电视剧", ours: false, always: ALL }, { name: "category", group: "title", label: "二级分类(没有则为空,所在目录段整段省略)", example: "欧美剧", ours: false, }, { name: "season", group: "episode", label: "季号,不补零;剧集缺季号为 1", example: "1", ours: false, always: SERIES }, { name: "season_fmt", group: "episode", label: "S01;多季 S01-S02", example: "S01", ours: false, always: SERIES }, { name: "season_year", group: "episode", label: "该季首播年份", example: "2008", ours: false }, { name: "episode", group: "episode", label: "集号,不补零;多集 1-3", example: "1", ours: false }, { name: "season_episode", group: "episode", label: "S01E01;多集 S01E01-E03", example: "S01E01", ours: false }, { name: "total_episodes", group: "episode", label: "本季总集数,未知为 0", example: "7", ours: false, always: ALL }, { name: "episode_title", group: "episode", label: "单集标题", example: "Pilot", ours: false }, { name: "episode_date", group: "episode", label: "单集播出日期", example: "2008-01-20", ours: false }, { name: "part", group: "episode", label: "分段(CD1、Part2)", example: "CD1", ours: false }, { name: "videoFormat", group: "tech", label: "分辨率", example: "2160p", ours: false }, { name: "resourceType", group: "tech", label: "来源", example: "WEB-DL", ours: false }, { name: "effect", group: "tech", label: "特效(HDR 格式,空格分隔)", example: "DV", ours: false }, { name: "edition", group: "tech", label: "来源 + 特效", example: "WEB-DL DV", ours: false }, { name: "resource_term", group: "tech", label: "来源 + 特效 + 分辨率", example: "WEB-DL DV 2160p", ours: false }, { name: "releaseGroup", group: "tech", label: "发布组", example: "CMCT", ours: false }, { name: "videoCodec", group: "tech", label: "视频编码", example: "H265", ours: false }, { name: "videoBit", group: "tech", label: "位深", example: "10bit", ours: false }, { name: "audioCodec", group: "tech", label: "音频编码", example: "DDP 5.1", ours: false }, { name: "webSource", group: "tech", label: "流媒体平台", example: "NF", ours: false }, { name: "fps", group: "tech", label: "帧率", example: "60", ours: false }, { name: "customization", group: "tech", label: "自定义占位符(暂恒为空)", example: "", ours: false }, { name: "fileExt", group: "tech", label: "扩展名,带点,小写", example: ".mkv", ours: false, always: ALL }, { name: "tmdbid", group: "id", label: "TMDB ID", example: "1396", ours: false }, { name: "imdbid", group: "id", label: "IMDb ID", example: "tt0903747", ours: false }, { name: "doubanid", group: "id", label: "豆瓣 ID", example: "1234567", ours: false }, { name: "cut", group: "ours", label: "剪辑版本", example: "Director's Cut", ours: true }, { name: "contentType", group: "ours", label: "内容类型(main / extra …)", example: "main", ours: true }, { name: "segment", group: "ours", label: "综艺分段(upper / lower …)", example: "upper", ours: true }, { name: "airDate", group: "ours", label: "综艺播出日期 YYYYMMDD,配合 date() 格式化", example: "20260927", ours: true, }, { name: "issue", group: "ours", label: "综艺期号", example: "12", ours: true }, { name: "slotName", group: "ours", label: "版本目标名", example: "4K HDR", ours: true }, { name: "libraryRoot", group: "ours", label: "库根名", example: "电视剧-A", ours: true }, ]; export const NAMING_VARIABLE_NAMES: readonly string[] = NAMING_VARIABLES.map((v) => v.name); /** 作品根识别用:含这些变量的目录段才可能是作品根(同 MP get_media_root_path) */ export const TITLE_VARS: readonly string[] = ["title", "title_year", "en_title", "original_title", "name"]; /** 季目录识别用 */ export const SEASON_VARS: readonly string[] = ["season", "season_fmt", "season_year", "season_episode", "episode"]; export interface NamingEntityInput { title: string; originalTitle?: string | undefined; enTitle?: string | undefined; year?: number | undefined; /** 命名类型;不传时按解析结果推断(有季/集/播出日期 = tv) */ kind?: NamingKind | undefined; category?: string | undefined; tmdbId?: string | number | undefined; imdbId?: string | undefined; doubanId?: string | undefined; slotName?: string | undefined; libraryRoot?: string | undefined; /** 季/集元数据(content_unit):季号 → 季首播年份/总集数;`季:集` → 单集标题/播出日期 */ seasons?: Readonly> | undefined; episodes?: Readonly> | undefined; } const pad2 = (n: number) => String(n).padStart(2, "0"); const KIND_LABEL: Record = { movie: "电影", tv: "电视剧", variety: "综艺" }; /** 我们的 videoCodec 规范值 → MP 写法(H.265 → H265);有 x264/x265 编码器时用编码器名(同 MP 保留压制器) */ function codecOf(p: Partial): string | undefined { if (p.encoder) return p.encoder; return p.videoCodec?.replace(".", ""); } /** 来源(MP 写法):原盘/压制 → BluRay,Remux → BluRay REMUX,标题带 UHD 时前缀 UHD */ function sourceOf(p: Partial): string | undefined { if (!p.source) return undefined; const uhd = p.uhd ? "UHD " : ""; if (p.source === "BluRay-Disc" || p.source === "BluRay") return `${uhd}BluRay`; if (p.source === "REMUX") return `${uhd}BluRay REMUX`; return p.source; } /** * 渲染上下文(MP 变量名):作品信息 + 发布解析 + 季集元数据 + 版本目标/库根。 * 标题用本地作品名,不用发布标题。空值不放进上下文(同 MP 剔除 None)。 */ export function buildNamingVars( entity: NamingEntityInput, parsed?: Partial, extra: { fileExt?: string | undefined } = {}, ): Record { const v: Record = {}; const set = (k: string, x: string | number | undefined | null) => { if (x !== undefined && x !== null && x !== "") v[k] = x; }; const p = parsed ?? {}; const hasEp = !!p.episodes || p.airDate !== undefined; const kind: NamingKind = entity.kind ?? (p.season !== undefined || hasEp ? "tv" : "movie"); set("title", entity.title); set("en_title", entity.enTitle); set("original_title", entity.originalTitle); set("name", p.title); const year = entity.year ?? p.year; set("year", year !== undefined ? String(year) : undefined); v.title_year = year !== undefined ? `${entity.title} (${year})` : entity.title; v.type = KIND_LABEL[kind]; set("category", entity.category); set("tmdbid", entity.tmdbId !== undefined ? String(entity.tmdbId) : undefined); set("imdbid", entity.imdbId); set("doubanid", entity.doubanId); let season: number | undefined; if (kind !== "movie") { season = p.season ?? 1; v.season = String(season); v.season_fmt = p.seasonEnd !== undefined && p.seasonEnd > season ? `S${pad2(season)}-S${pad2(p.seasonEnd)}` : `S${pad2(season)}`; const sm = entity.seasons?.[season]; set("season_year", sm?.year !== undefined ? String(sm.year) : undefined); v.total_episodes = sm?.episodeTotal ?? p.totalEpisodes ?? 0; if (p.episodes) { const { start, end } = p.episodes; // 多集保留区间(有意偏离 MP 的 ≥3 集截成首集) v.episode = end > start ? `${start}-${end}` : String(start); v.season_episode = `S${pad2(season)}E${pad2(start)}${end > start ? `-E${pad2(end)}` : ""}`; const em = entity.episodes?.[`${season}:${start}`]; set("episode_title", em?.title); set("episode_date", em?.airDate); } } else { v.total_episodes = 0; } set("part", p.part); set("videoFormat", p.resolution?.toLowerCase()); const src = sourceOf(p); set("resourceType", src); const effect = p.hdr?.length ? p.hdr.join(" ") : undefined; set("effect", effect); set("edition", [src, effect].filter(Boolean).join(" ")); set("resource_term", [src, effect, p.resolution?.toLowerCase()].filter(Boolean).join(" ")); set("releaseGroup", p.releaseGroup?.name); set("videoCodec", codecOf(p)); set("videoBit", p.bitDepth !== undefined ? `${p.bitDepth}bit` : undefined); set("audioCodec", p.audio?.[0]); set("webSource", p.webSource); set("fps", p.fps !== undefined ? String(p.fps) : undefined); if (extra.fileExt !== undefined) set("fileExt", normalizeExt(extra.fileExt)); set("cut", p.cut); if (p.contentType && p.contentType !== "unknown") set("contentType", p.contentType); set("segment", p.segment); set("airDate", p.airDate); set("issue", p.issue !== undefined ? String(p.issue) : undefined); set("slotName", entity.slotName); set("libraryRoot", entity.libraryRoot); return v; } /** 扩展名:带点、小写(有意偏离 MP 保留原大小写) */ export function normalizeExt(ext: string): string { const e = ext.replace(/^\.+/, "").toLowerCase(); return e ? `.${e}` : ""; } /** 变量示例上下文(预览/校验用,所有变量都有值) */ export function exampleVars(kind: NamingKind): Record { const v: Record = {}; for (const d of NAMING_VARIABLES) if (d.example !== "") v[d.name] = d.example; v.type = KIND_LABEL[kind]; if (kind === "movie") { for (const k of [ "season", "season_fmt", "season_year", "episode", "season_episode", "episode_title", "episode_date", ]) delete v[k]; v.total_episodes = 0; } return v; }