Files
rootandClaude Opus 5.5 8651a35594 feat(organize): naming endpoints and override chain for target paths
- 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]>
2026-09-28 15:25:08 +08:00

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:

  • 标题:title en_title original_title name year title_year type category
  • 季集:season(不补零,剧集缺季号为 1)season_fmt season_year episode(多集 1-3)season_episode(多集 S01E01-E03)total_episodes episode_title episode_date part
  • 技术:videoFormat resourceType effect edition resource_term releaseGroup videoCodec videoBit audioCodec webSource fps customization fileExt(带点,小写)
  • ID:tmdbid imdbid doubanid
  • 我们特有:cut contentType segment airDate issue slotName libraryRoot

来源:作品名/原名/年份取 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)。