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:
1 parent
238445a258
commit
dfb08ab552
8 files changed
+268
-2
No files matched your search
@@ -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;
|
||||
`,
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user