Files
UHD_Media_Tool/packages/db/migrations/0032_naming.ts
T
rootandClaude Opus 5.5 dfb08ab552 feat(contracts,db): naming settings, override fields and template versions (0032)
- settings.naming {movie, tv, variety, lockFileNameForBound}, default = MP templates;
  library section gains #naming anchor
- naming API schemas and Api.naming endpoints (variables, preview, templates, overrides)
- 0032: library_root.naming, media_entity.naming_override, naming_template_version
  (version 1 = upgraded v1 templates, 2 = MP default, fingerprint-deduplicated)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 15:24:54 +08:00

70 lines
3.6 KiB
TypeScript

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<any>): Promise<void> {
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<any>): Promise<void> {
await execDdl(
db,
`
${dropTables(["naming_template_version"])}
alter table media_entity drop column naming_override;
alter table library_root drop column naming;
`,
);
}