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]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 15:24:54 +08:00
1 parent 238445a258
commit dfb08ab552
8 files changed
+268 -2

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
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;
`,
);
}
+15
View File
@@ -810,6 +810,7 @@ export interface LibraryRootTable {
notes: string | null;
created_at: GeneratedTimestamptz;
updated_at: GeneratedTimestamptz;
naming: Json | null;
}
export interface LoginAttemptTable {
@@ -854,6 +855,7 @@ export interface MediaEntityTable {
last_metadata_refresh_at: Timestamptz | null;
created_at: GeneratedTimestamptz;
updated_at: GeneratedTimestamptz;
naming_override: Json | null;
}
export interface MediaServerInstanceTable {
@@ -886,6 +888,15 @@ export interface MetadataFieldTable {
updated_at: GeneratedTimestamptz;
}
export interface NamingTemplateVersionTable {
id: Generated<string>;
version: number;
fingerprint: string;
templates: Json;
created_at: GeneratedTimestamptz;
updated_at: GeneratedTimestamptz;
}
export interface ObservationTable {
id: Generated<string>;
feed_id: string;
@@ -1575,6 +1586,7 @@ export interface DB {
media_entity: MediaEntityTable;
media_server_instance: MediaServerInstanceTable;
metadata_field: MetadataFieldTable;
naming_template_version: NamingTemplateVersionTable;
observation: ObservationTable;
organization_file_result: OrganizationFileResultTable;
organization_record: OrganizationRecordTable;
@@ -1722,6 +1734,9 @@ export type MediaServerInstanceUpdate = Updateable<MediaServerInstanceTable>;
export type MetadataFieldRow = Selectable<MetadataFieldTable>;
export type NewMetadataField = Insertable<MetadataFieldTable>;
export type MetadataFieldUpdate = Updateable<MetadataFieldTable>;
export type NamingTemplateVersionRow = Selectable<NamingTemplateVersionTable>;
export type NewNamingTemplateVersion = Insertable<NamingTemplateVersionTable>;
export type NamingTemplateVersionUpdate = Updateable<NamingTemplateVersionTable>;
export type ObservationRow = Selectable<ObservationTable>;
export type NewObservation = Insertable<ObservationTable>;
export type ObservationUpdate = Updateable<ObservationTable>;
+2
View File
@@ -99,6 +99,8 @@ const EXPECTED_TABLES = [
// W7-C 定时任务注册表(0028)
"scheduled_job",
"scheduled_job_run",
// W9-C 命名模板版本(0032)
"naming_template_version",
// W9-D 规则组(0033)
"rule_group",
"rule_atom",