- 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]>
81 lines
3.0 KiB
Python
81 lines
3.0 KiB
Python
"""命名模板差分:用 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()
|