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:
1 parent
e8745115f4
commit
f7c75ad781
22 files changed
+2674
-281
No files matched your search
@@ -0,0 +1,80 @@
|
||||
"""命名模板差分:用 MP 上游(MetaInfo + TemplateContextBuilder + jinja2.Template)生成 naming-mp.json。
|
||||
|
||||
用法(MP 仓库根目录,用其 .venv):
|
||||
cd /opt/moviepilot-plus/upstream/MoviePilot && PYTHONPATH=. \
|
||||
.venv/bin/python /opt/uhd_media_tool/packages/domain/test/differential/gen-naming-mp.py
|
||||
|
||||
只记录行为输出,不复制 MP 源码。默认模板取 settings.RENAME_FORMAT(与 MP 规范化一致);
|
||||
MP 没有综艺类型,综艺样例用剧集类型渲染我们的模板(预期 MP 报错,属我们特有)。
|
||||
"""
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import subprocess
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
from jinja2 import Template
|
||||
|
||||
from app.application.messaging.message import TemplateHelper
|
||||
from app.core.config import settings
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
cases = json.loads((HERE / "naming-cases.json").read_text(encoding="utf-8"))["cases"]
|
||||
|
||||
|
||||
def normalize_template(s: str) -> str:
|
||||
# 同 settings.RENAME_FORMAT:\ → /,合并 /,去首尾 /
|
||||
import re
|
||||
|
||||
s = s.replace("\\", "/")
|
||||
s = re.sub(r"/+", "/", s)
|
||||
return s.strip("/")
|
||||
|
||||
|
||||
def run(case: dict) -> dict:
|
||||
kind = case["kind"]
|
||||
mtype = MediaType.MOVIE if kind == "movie" else MediaType.TV
|
||||
meta = MetaInfo(case["fileName"])
|
||||
if mtype == MediaType.TV and meta.begin_episode is not None:
|
||||
# transhandler.plan_transfer:单文件 3 集及以上截成首集
|
||||
meta.end_season = None
|
||||
if meta.total_season:
|
||||
meta.total_season = 1
|
||||
if meta.total_episode > 2:
|
||||
meta.total_episode = 1
|
||||
meta.end_episode = None
|
||||
e = case["entity"]
|
||||
mi = MediaInfo()
|
||||
mi.type = mtype
|
||||
mi.title = e["title"]
|
||||
mi.year = str(e["year"]) if e.get("year") else None
|
||||
mi.en_title = e.get("enTitle")
|
||||
mi.original_title = e.get("originalTitle")
|
||||
mi.category = e.get("category", "")
|
||||
if e.get("tmdbId") is not None:
|
||||
mi.tmdb_id = e["tmdbId"]
|
||||
ext = "." + case["fileName"].rsplit(".", 1)[1]
|
||||
ctx = TemplateHelper().builder.build(meta=meta, mediainfo=mi, file_extension=ext, include_raw_objects=False)
|
||||
ctx.pop("media_source", None)
|
||||
ctx.pop("media_id", None)
|
||||
ctx.update(case.get("vars") or {})
|
||||
tpl = case.get("template") or settings.RENAME_FORMAT(mtype)
|
||||
try:
|
||||
out = Template(normalize_template(tpl)).render(ctx)
|
||||
except Exception as ex: # noqa: BLE001 - 记录 MP 的报错
|
||||
return {"error": f"{type(ex).__name__}: {ex}"}
|
||||
return {"path": str(PurePosixPath(out))}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
commit = subprocess.run(["git", "rev-parse", "--short", "HEAD"], capture_output=True, text=True).stdout.strip()
|
||||
result = {"generatedFrom": {"repo": "MoviePilot", "commit": commit}, "mp": {c["id"]: run(c) for c in cases}}
|
||||
(HERE / "naming-mp.json").write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"wrote {len(cases)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user