- 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]>
111 lines
8.5 KiB
Markdown
111 lines
8.5 KiB
Markdown
# 整理命名模板运行手册(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 输出(改了样例或跟进上游后):
|
|
|
|
```sh
|
|
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`)。
|