import { createHash } from "node:crypto"; import type { Kysely } from "kysely"; import { dropTables, entityTable, execDdl } from "../src/migration-helpers.ts"; // W9-C 整理命名模板(docs/design/naming-template.md §6、§9): // - library_root.naming / media_entity.naming_override:按类型(movie/tv/variety)覆盖模板,null = 不覆盖。 // 生效顺序 作品 > 库根 > 全局(settings.naming)。 // - naming_template_version:生效模板集的版本登记(指纹去重),path_binding.template_version 引用 version; // "已绑定作品固定文件名模板版本"按它取回当时的模板。1 = v1 旧模板(迁移后语法),2 = MP 默认。 // 模板串是写入时的快照,之后不随代码变化。 const LEGACY_V1 = { movie: "{{title}}{% if year %} ({{year}}){% endif %}/{{title}}{% if year %} ({{year}}){% endif %}{% if cut %} - {{cut}}{% endif %}{% if part %} - {{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}{{fileExt}}", tv: "{{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {{season_episode}}{% if part %} - {{part}}{% endif %}{{fileExt}}", variety: "{{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{% if season_episode %}{{title}} - {{season_episode}}{% if part %} - {{part}}{% endif %}{% else %}{{title}} - {{airDate | date('YYYY-MM-DD')}}{% if issue %} - 第{{issue}}期{% endif %}{% endif %}{{fileExt}}", }; const MP_DEFAULT = { movie: "{{title}}{% if year %} ({{year}}){% endif %}/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}{{fileExt}}", tv: "{{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}{{fileExt}}", variety: "{{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {% if season_episode and episode %}{{season_episode}}{% else %}{{airDate | date('YYYY-MM-DD')}}{% endif %}{% if issue %} - 第 {{issue}} 期{% endif %}{{fileExt}}", }; /** 与 @umt/domain namingFingerprint 同算法:sha256(JSON {movie,tv,variety}) */ const fingerprint = (t: { movie: string; tv: string; variety: string }) => createHash("sha256") .update(JSON.stringify({ movie: t.movie, tv: t.tv, variety: t.variety })) .digest("hex"); const lit = (s: string) => `'${s.replace(/'/g, "''")}'`; const seed = (version: number, t: typeof MP_DEFAULT) => `insert into naming_template_version (version, fingerprint, templates) values (${version}, ${lit(fingerprint(t))}, ${lit(JSON.stringify(t))}::jsonb);`; // biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型 export async function up(db: Kysely): Promise { await execDdl( db, ` alter table library_root add column naming jsonb check (naming is null or jsonb_typeof(naming) = 'object'); alter table media_entity add column naming_override jsonb check (naming_override is null or jsonb_typeof(naming_override) = 'object'); ${entityTable( "naming_template_version", ` version integer not null unique check (version >= 1), fingerprint text not null unique, templates jsonb not null check (jsonb_typeof(templates) = 'object') `, )} create sequence naming_template_version_seq start with 3 owned by naming_template_version.version; ${seed(1, LEGACY_V1)} ${seed(2, MP_DEFAULT)} `, ); } // biome-ignore lint/suspicious/noExplicitAny: 同上 export async function down(db: Kysely): Promise { await execDdl( db, ` ${dropTables(["naming_template_version"])} alter table media_entity drop column naming_override; alter table library_root drop column naming; `, ); }