feat(domain): naming template engine (Jinja safe subset, MP variables, path layout)

- naming/engine: lexer/parser/AST/render for {{ }} / if-elif-else / whitelisted filters
  (default pad pad2 upper lower trim replace truncate date join); rejects for/set/attribute/
  subscript/call/~/% with line:col errors and 'did you mean' hints
- naming/variables: variable table aligned with MoviePilot names plus ours (cut, airDate,
  issue, slotName, libraryRoot ...); buildNamingVars from parsed release + entity + units
- naming/layout: split by literal '/', segment roles (category/root/season/sub/file),
  value escaping, reserved names, 255-byte segments, validateNamingTemplate
- naming/defaults: MP default movie/tv + our variety template, v1 syntax upgrade,
  entity > root > global override resolution, builtin preview samples
- path/binding: resolveTargetDirectory keeps bound dirs, only unbound segments render;
  renderMediaFileName = last segment of the full template; DEFAULT_TEMPLATES = MP v2
- differential: 34 cases vs MP (21 identical, 13 documented deviations)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 15:24:32 +08:00
1 parent e8745115f4
commit f7c75ad781
22 files changed
+2674 -281

No files matched your search

@@ -0,0 +1,420 @@
{
"note": "命名模板差分样例:ours 期望 = expect/expectError;MP 输出见 naming-mp.json(gen-naming-mp.py 生成)。deviation 标注故意偏离/我们特有/表示差异,其余要求与 MP 逐字一致。",
"cases": [
{
"id": "N01",
"kind": "movie",
"fileName": "Inception.2010.2160p.BluRay.x265-CMCT.mkv",
"entity": {
"title": "Inception",
"year": 2010
},
"expect": "Inception (2010)/Inception (2010) - 2160p.mkv"
},
{
"id": "N02",
"kind": "movie",
"fileName": "Inception.2010.mkv",
"entity": {
"title": "Inception",
"year": 2010
},
"expect": "Inception (2010)/Inception (2010).mkv"
},
{
"id": "N03",
"kind": "movie",
"fileName": "Inception.2010.CD1.1080p.BluRay.x264-CMCT.mkv",
"entity": {
"title": "Inception",
"year": 2010
},
"expect": "Inception (2010)/Inception (2010)-CD1 - 1080p.mkv"
},
{
"id": "N04",
"kind": "movie",
"fileName": "Some.Movie.mkv",
"entity": {
"title": "A/B:C"
},
"expect": "A/B:C/A/B:C.mkv"
},
{
"id": "N05",
"kind": "tv",
"fileName": "Breaking.Bad.S01E01.1080p.WEB-DL.H264-NTb.mkv",
"entity": {
"title": "绝命毒师",
"year": 2008,
"enTitle": "Breaking Bad"
},
"expect": "绝命毒师 (2008)/Season 1/绝命毒师 - S01E01 - 第 1 集.mkv"
},
{
"id": "N06",
"kind": "tv",
"fileName": "Breaking.Bad.S01E01-E02.1080p.WEB-DL.H264-NTb.mkv",
"entity": {
"title": "绝命毒师",
"year": 2008,
"enTitle": "Breaking Bad"
},
"expect": "绝命毒师 (2008)/Season 1/绝命毒师 - S01E01-E02 - 第 1-2 集.mkv"
},
{
"id": "N07",
"kind": "tv",
"fileName": "Breaking.Bad.S01E01-E03.1080p.WEB-DL.H264-NTb.mkv",
"entity": {
"title": "绝命毒师",
"year": 2008,
"enTitle": "Breaking Bad"
},
"expect": "绝命毒师 (2008)/Season 1/绝命毒师 - S01E01-E03 - 第 1-3 集.mkv",
"deviation": {
"kind": "intentional",
"note": "MP 3 集及以上截成首集(S01E01 - 第 1 集);我们保留区间,集账本按 3 集登记"
}
},
{
"id": "N08",
"kind": "tv",
"fileName": "Breaking.Bad.S00E03.1080p.WEB-DL.H264-NTb.mkv",
"entity": {
"title": "绝命毒师",
"year": 2008,
"enTitle": "Breaking Bad"
},
"expect": "绝命毒师 (2008)/Season 0/绝命毒师 - S00E03 - 第 3 集.mkv"
},
{
"id": "N09",
"kind": "tv",
"fileName": "Some.Show.E05.1080p.ass",
"entity": {
"title": "某剧"
},
"expect": "某剧/Season 1/某剧 - S01E05 - 第 5 集.ass"
},
{
"id": "N10",
"kind": "tv",
"fileName": "X.S12E100.1080p.mkv",
"entity": {
"title": "X"
},
"expect": "X/Season 12/X - S12E100 - 第 100 集.mkv"
},
{
"id": "N11",
"kind": "movie",
"fileName": "Inception.2010.1080p.BluRay.x264-CMCT.MKV",
"entity": {
"title": "Inception",
"year": 2010
},
"expect": "Inception (2010)/Inception (2010) - 1080p.mkv",
"deviation": {
"kind": "intentional",
"note": "MP 保留原扩展名大小写 .MKV;我们统一小写"
}
},
{
"id": "N12",
"kind": "tv",
"fileName": "Breaking.Bad.S02E01.1080p.WEB-DL.H264-NTb.mkv",
"entity": {
"title": "绝命毒师",
"year": 2008,
"enTitle": "Breaking Bad"
},
"template": "{{title_year}}/{{title}} {{season_fmt}}{{fileExt}}",
"expect": "绝命毒师 (2008)/绝命毒师 S02.mkv"
},
{
"id": "N13",
"kind": "movie",
"fileName": "X.2020.1080p.BluRay.DV.x265-A.mkv",
"entity": {
"title": "X",
"year": 2020
},
"template": "{{title}}/{{edition}}/{{title}}{{fileExt}}",
"expect": "X/BluRay DV/X.mkv"
},
{
"id": "N14",
"kind": "movie",
"fileName": "X.2020.1080p.WEB-DL.H264-A.mkv",
"entity": {
"title": "X",
"year": 2020
},
"template": "{{title}}/{{title}} - {{resource_term}}{{fileExt}}",
"expect": "X/X - WEB-DL 1080p.mkv",
"deviation": {
"kind": "intentional",
"note": "MP resource_term 恒带前导空格,渲染出两个空格;我们合并连续空白"
}
},
{
"id": "N15",
"kind": "movie",
"fileName": "X.2020.1080p.WEB-DL.H264-A.mkv",
"entity": {
"title": "X",
"year": 2020
},
"template": "{{title}}/{{title}} - {{releaseGroup}}{{fileExt}}",
"vars": {
"releaseGroup": "A/B"
},
"expect": "X/X - A/B.mkv",
"deviation": {
"kind": "intentional",
"note": "MP 只转义标题类变量,releaseGroup 里的 / 会多出一层目录 A;我们所有变量值都转全角,值不产生目录"
}
},
{
"id": "N16",
"kind": "movie",
"fileName": "Inception.2010.mkv",
"entity": {
"title": "盗梦空间",
"year": 2010
},
"template": "{{en_title or title}}/{{en_title or title}}{{fileExt}}",
"expect": "盗梦空间/盗梦空间.mkv"
},
{
"id": "N17",
"kind": "tv",
"fileName": "Breaking.Bad.S03E01.1080p.WEB-DL.H264-NTb.mkv",
"entity": {
"title": "绝命毒师",
"year": 2008,
"enTitle": "Breaking Bad"
},
"template": "{{title}}/S{{season | pad(2)}}/{{title}}{{fileExt}}",
"expect": "绝命毒师/S03/绝命毒师.mkv",
"deviation": {
"kind": "intentional",
"note": "pad 是我们的过滤器,MP(Jinja2)没有,报 No filter named 'pad';MP 写法 {{ '%02d' % season|int }} 我们拒绝并提示用 pad"
}
},
{
"id": "N18",
"kind": "movie",
"fileName": "Some.Movie.mkv",
"entity": {
"title": "X"
},
"template": "{{title}} ({{year}})/{{title}}{{fileExt}}",
"expect": "X ()/X.mkv"
},
{
"id": "N19",
"kind": "movie",
"fileName": "Some.Movie.mkv",
"entity": {
"title": "长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长"
},
"expect": "长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长/长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长.mkv",
"deviation": {
"kind": "intentional",
"note": "每段按 UTF-8 255 字节截断,文件名保留扩展名;MP 不截,超长由文件系统报错"
}
},
{
"id": "N20",
"kind": "variety",
"fileName": "快乐大本营.20260927.第12期.1080p.WEB-DL.H264.mp4",
"entity": {
"title": "快乐大本营",
"kind": "variety"
},
"expect": "快乐大本营/Season 1/快乐大本营 - 2026-09-27 - 第 12 期.mp4",
"deviation": {
"kind": "ours",
"note": "综艺默认模板是我们特有(date 过滤器、issue 变量),MP 无对应"
}
},
{
"id": "N21",
"kind": "tv",
"fileName": "Breaking.Bad.S01E02.1080p.WEB-DL.H264-NTb.mkv",
"entity": {
"title": "绝命毒师",
"year": 2008,
"enTitle": "Breaking Bad"
},
"template": "{{type}}/{{category}}/{{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {{season_episode}}{{fileExt}}",
"expect": "电视剧/绝命毒师 (2008)/Season 1/绝命毒师 - S01E02.mkv"
},
{
"id": "N22",
"kind": "movie",
"fileName": "Some.Movie.mkv",
"entity": {
"title": "X"
},
"template": "{% for x in y %}{{x}}{% endfor %}{{title}}/{{title}}{{fileExt}}",
"expectError": "unsupported",
"deviation": {
"kind": "intentional",
"note": "安全子集不支持 for;MP 照常渲染"
}
},
{
"id": "N23",
"kind": "movie",
"fileName": "Some.Movie.mkv",
"entity": {
"title": "X"
},
"template": "{{title.__class__}}/{{title}}{{fileExt}}",
"expectError": "attribute_access",
"deviation": {
"kind": "intentional",
"note": "安全子集不支持属性访问;MP 渲染出 <class 'str'>"
}
},
{
"id": "N24",
"kind": "movie",
"fileName": "Some.Movie.mkv",
"entity": {
"title": "X"
},
"template": "{{title}}/{{title}}",
"expectError": "missing_file_ext",
"deviation": {
"kind": "intentional",
"note": "文件名必须以 {{fileExt}} 结尾;MP 会生成无扩展名文件"
}
},
{
"id": "N25",
"kind": "movie",
"fileName": "X.2020.1080p.WEB-DL.H265-A.mkv",
"entity": {
"title": "X",
"year": 2020
},
"template": "{{title}}/{{title}} - {{videoCodec}}{{fileExt}}",
"expect": "X/X - H265.mkv"
},
{
"id": "N26",
"kind": "movie",
"fileName": "X.2020.1080p.WEB-DL.HEVC-A.mkv",
"entity": {
"title": "X",
"year": 2020
},
"template": "{{title}}/{{title}} - {{videoCodec}}{{fileExt}}",
"expect": "X/X - H265.mkv",
"deviation": {
"kind": "representation",
"note": "解析器把 HEVC/H.265/H265 归一成 H.265 再输出 H265;MP 保留原词 HEVC"
}
},
{
"id": "N27",
"kind": "movie",
"fileName": "X.2020.2160p.WEB-DL.10bit.H265-A.mkv",
"entity": {
"title": "X",
"year": 2020
},
"template": "{{title}}/{{title}}{% if videoBit %} {{videoBit}}{% endif %}{{fileExt}}",
"expect": "X/X 10bit.mkv"
},
{
"id": "N28",
"kind": "movie",
"fileName": "Inception.2010.mkv",
"entity": {
"title": "Inception",
"year": 2010,
"enTitle": "Inception Movie"
},
"template": "{{ en_title | replace(' ', '.') }}/{{ title | upper }}{{fileExt}}",
"expect": "Inception.Movie/INCEPTION.mkv"
},
{
"id": "N29",
"kind": "tv",
"fileName": "Breaking.Bad.S00E03.1080p.WEB-DL.H264-NTb.mkv",
"entity": {
"title": "绝命毒师",
"year": 2008,
"enTitle": "Breaking Bad"
},
"template": "{{title}}/{% if season == '0' %}Specials{% else %}Season {{season}}{% endif %}/{{title}} - {{season_episode}}{{fileExt}}",
"expect": "绝命毒师/Specials/绝命毒师 - S00E03.mkv"
},
{
"id": "N30",
"kind": "movie",
"fileName": "X.2020.2160p.WEB-DL.H265-A.mkv",
"entity": {
"title": "X",
"year": 2020
},
"template": "{{title}}/{{title}} [{% if videoFormat == '2160p' %}4K{% elif videoFormat %}{{videoFormat}}{% else %}SD{% endif %}]{{fileExt}}",
"expect": "X/X [4K].mkv"
},
{
"id": "N31",
"kind": "movie",
"fileName": "Some.Movie.mkv",
"entity": {
"title": "X"
},
"template": "{{title}}/{{title}} - {{ releaseGroup | default('NOGRP') }}{{fileExt}}",
"expect": "X/X - NOGRP.mkv"
},
{
"id": "N32",
"kind": "movie",
"fileName": "Inception.2010.mkv",
"entity": {
"title": "Inception",
"year": 2010,
"tmdbId": 27205
},
"template": "{{title_year}} [tmdbid={{tmdbid}}]/{{title_year}}{{fileExt}}",
"expect": "Inception (2010) [tmdbid=27205]/Inception (2010).mkv"
},
{
"id": "N33",
"kind": "movie",
"fileName": "Some.Movie.mkv",
"entity": {
"title": "CON"
},
"expect": "CON_/CON_.mkv",
"deviation": {
"kind": "intentional",
"note": "Windows 保留名加 _;MP 不处理"
}
},
{
"id": "N34",
"kind": "movie",
"fileName": "Inception.2010.mkv",
"entity": {
"title": "Inception",
"year": 2010
},
"template": "{{title}}: {{year}}/{{title}}{{fileExt}}",
"expect": "Inception: 2010/Inception.mkv",
"deviation": {
"kind": "intentional",
"note": "模板字面量里的非法字符也转全角;MP 原样保留 :"
}
}
]
}