- GET /naming/variables, POST /naming/preview (builtin/fileName/sourceRelease/asset samples,
line:col errors, warnings, binding-aware), GET/PUT /naming/templates,
PUT /library-roots/{id}/naming, PUT /media-entities/{id}/naming-override
- planImport picks templates entity > library root > global, registers the template
version for new path bindings; naming vars include en title, external ids, season/
episode metadata, slot and root names; optional lock of file-name template for bound works
- runbook: docs/runbook/naming.md
Co-Authored-By: Claude Opus 5.5 <[email protected]>
8.5 KiB
整理命名模板运行手册(W9-C)
设计:docs/design/naming-template.md。实现:
| 位置 | 内容 |
|---|---|
packages/domain/src/naming/engine.ts |
词法 → 语法 → AST → 渲染,纯函数,错误带行列号 |
packages/domain/src/naming/variables.ts |
变量全表 NAMING_VARIABLES、buildNamingVars(解析结果 + 作品 + 季集元数据 + 版本目标 + 库根) |
packages/domain/src/naming/layout.ts |
切段、段角色、非法字符/长度处理、保存前校验 validateNamingTemplate |
packages/domain/src/naming/defaults.ts |
MP 默认模板、v1 旧语法迁移、三级回落 resolveNamingTemplates、6 条内置样本 |
packages/domain/src/path/binding.ts |
resolveTargetDirectory(路径绑定不变)、renderMediaFileName(文件名 = 模板最后一段) |
apps/server/src/modules/acquisition-organize/services/naming.ts |
读三级模板、模板版本登记、命名变量来源 |
apps/server/src/modules/acquisition-organize/routes/naming.ts |
/naming/* 与两个覆盖端点 |
迁移 0032_naming |
library_root.naming、media_entity.naming_override、naming_template_version |
语法
Jinja 写法的安全子集,MP 的模板可以直接粘贴。
| 写法 | 说明 |
|---|---|
{{ var }} |
输出变量;没有值输出空串(同 MP) |
{{ a or b or 'x' }} |
取第一个有值的 |
{{ var | 过滤器(参数) }} |
过滤器可串联 |
{% if 条件 %}…{% elif 条件 %}…{% else %}…{% endif %} |
最多嵌套 4 层 |
| 条件 | var、not var、==/!=(数字按数值比)、> < >= <=(整数)、and/or/括号 |
{# 注释 #}、{%-/-%} |
注释丢弃;- 去掉相邻空白 |
过滤器:default('x')、pad(n)(数字补零,1-3 → 01-03)、pad2、upper、lower、trim、replace('a','b')、truncate(n)(按字符)、date('YYYY-MM-DD'|'YYYY.MM.DD'|'YYYYMMDD')、join(sep)。
拒绝(编译错误):for/set/宏/include 等语句、属性访问 a.b、下标 a[0]、函数调用、~ 拼接、% 格式化与算术、行内 x if c else y、未知变量(提示"是否指 videoFormat")、未知过滤器。限制:模板 ≤ 1024 字符,渲染结果 ≤ 4096 字节。
变量
GET /naming/variables 返回完整清单(名称、分组、说明、示例值、ours)。名称照抄 MP:
- 标题:
titleen_titleoriginal_titlenameyeartitle_yeartypecategory - 季集:
season(不补零,剧集缺季号为 1)season_fmtseason_yearepisode(多集1-3)season_episode(多集S01E01-E03)total_episodesepisode_titleepisode_datepart - 技术:
videoFormatresourceTypeeffecteditionresource_termreleaseGroupvideoCodecvideoBitaudioCodecwebSourcefpscustomizationfileExt(带点,小写) - ID:
tmdbidimdbiddoubanid - 我们特有:
cutcontentTypesegmentairDateissueslotNamelibraryRoot
来源:作品名/原名/年份取 media_entity;英文名取 title_alias(language 以 en 开头,锁定优先);外部 ID 取已接受的 external_identity;season_year/total_episodes/episode_title/episode_date 取 content_unit;category 取 media_entity.metadata.category;slotName 取整理请求的版本目标;其余来自文件名解析。
目录层级与段角色
模板里的 / 切段(\ 视同 /,多余的 / 合并)。最后一段是文件名,必须以 {{fileExt}} 结尾。从后往前第一个含标题变量(title/title_year/en_title/original_title/name)且不含季变量的目录段是作品根;作品根之后含季变量的段是季目录,其余是子目录;作品根之前是分类前缀。分类/子目录段渲染为空时整段省略,作品根或文件名为空报错。
非法字符与长度
- 变量值里的
\/:*?"<>|转全角,删控制字符。所有变量都转(MP 只转标题类),值里的/不会多出目录。 - 模板字面量里的这些字符(
/除外)也转全角。 - 每段合并连续空白、去首尾空白和点;Windows 保留名(CON、NUL、COM1…)加
_。 - 每段 ≤ 255 字节(UTF-8),文件名截主干保留扩展名;全路径 ≤ 4096 字节。
覆盖顺序与路径绑定
- 生效顺序:作品(
media_entity.naming_override)> 库根(library_root.naming)> 全局(settings.naming),按 movie/tv/variety 分别回落。 - 作品类型:
movie(及 collection/other)用 movie;series用 tv;variety用 variety。剧集文件只有播出日期没有集号时也用 variety。 - 路径绑定不变:作品根与季目录只在首次建立
path_binding时按模板渲染;之后入库直接用绑定路径,模板和译名变化都不改已有目录。绑定之后模板里剩余的子目录段(如{{edition}})照常渲染。 - 文件名每次入库按当前生效模板渲染,已入库文件不回溯改名。设置
naming.lockFileNameForBound = true后,已有作品绑定的作品按绑定记录的模板版本出文件名。 - 模板版本:生效模板集按内容指纹(sha256)登记在
naming_template_version,新建绑定的template_version就是这个版本号。1 = v1 旧模板(迁移后的新语法写法),2 = MP 默认,之后的从 3 起。
接口
| 方法 路径 | 权限 | 说明 |
|---|---|---|
GET /naming/variables |
read | 变量表、过滤器说明、6 条内置样本 |
POST /naming/preview |
read | {template?, kind, sample?: {fileName}|{sourceReleaseId}|{assetId}|{builtin}, libraryRootId?, entityId?} → {ok, relativePath, segments[{text, role}], errors[{code, message, line, col}], warnings, missing, usesBinding, templateSource, template, sample, vars}。不传 template 用三级回落的生效模板;作品已有绑定时目录用绑定路径 |
GET /naming/templates?libraryRootId&entityId |
read | 全局、库根、作品三级模板,MP 默认,按类型的生效模板与来源 |
PUT /naming/templates |
admin | 保存全局模板(部分字段),逐条编译,有错 422 |
PUT /library-roots/{id}/naming |
admin | {naming: {movie?, tv?, variety?} | null} |
PUT /media-entities/{id}/naming-override |
admin | {namingOverride: {movie?, tv?, variety?} | null} |
PATCH /settings 也能改 naming,但只做形状校验,不编译模板;前端保存请走 PUT /naming/templates。
与 MP 的差分
packages/domain/test/differential/naming-cases.json(34 条)+ naming-mp.json(MP 输出)。没有登记 deviation 的 21 条与 MP 逐字一致;登记的 13 条:
| # | 差异 | 理由 |
|---|---|---|
| N07 | E01-E03 保留区间;MP 截成 S01E01 - 第 1 集 |
集账本要登记 3 集,截断会让后两集看起来缺失 |
| N11 | 扩展名统一小写;MP 保留 .MKV |
避免同一文件因大小写不同被当成两个目标 |
| N14 | 合并连续空白;MP 的 resource_term 带前导空格,出现 - WEB-DL |
只在变量为空或 MP 自带空格时有差异 |
| N15 | releaseGroup 里的 / 转全角;MP 多出一层目录 |
值不应该决定目录层级 |
| N17 | pad 是我们的过滤器,MP 报错 |
MP 的 '%02d' % season|int 需要运算,安全子集不开放 |
| N19 | 每段截到 255 字节;MP 不截 | 超长由文件系统报错太晚 |
| N20 | 综艺模板(airDate、issue、date())是我们特有 |
MP 没有综艺类型 |
| N22、N23 | for、属性访问编译报错;MP 照常渲染 |
安全子集 |
| N24 | 模板缺 {{fileExt}} 报错;MP 生成无扩展名文件 |
|
| N26 | videoCodec 输出规范值 H265;MP 保留原词 HEVC |
解析器把 HEVC/H.265/x265 归一 |
| N33 | Windows 保留名加 _ |
MP 不处理 |
| N34 | 模板字面量里的 : 转全角 |
MP 原样保留 |
另外(不在差分文件里):已有作品绑定 旧名 (2008)、新模板渲染出 新名 (2008) 时,目录仍用 旧名 (2008)/Season 1/…,文件名用新模板(packages/domain/test/naming.test.ts "路径绑定不变")。
重新生成 MP 输出(改了样例或跟进上游后):
cd /opt/moviepilot-plus/upstream/MoviePilot && PYTHONPATH=. \
.venv/bin/python /opt/uhd_media_tool/packages/domain/test/differential/gen-naming-mp.py
测试断言:无 deviation 的样例与 MP 一致;有 deviation 的必须确实不同,差异消除后删掉登记。
迁移说明
- 0032 之前的默认模板(v1:
{var}+[[可选段]])已由upgradeLegacyTemplate改写为新语法,作为版本 1 登记;已有绑定(template_version = 1)不动。 - 升级后新建的绑定和所有新入库文件按 MP 默认(版本 2)命名:剧集文件名多出
- 第 N 集。要保持旧文件名,打开naming.lockFileNameForBound(只对已有绑定的作品生效),或把全局模板改回 v1 写法(LEGACY_V1_NAMING)。