video org: youtube channel template (Plex TV-by-date)
add a 'youtube' scope to render_path: channel=show, season=upload year, episode named '$channel - $date - $title'. rides the existing $token engine (sanitised, dangling-separator tidy). undated videos fall back cleanly (no empty 'Season ' / no stray ' - '). default template editable like the movie/episode ones; per-channel override comes with the settings modal later.
This commit is contained in:
parent
568e297d36
commit
00d05a04f5
2 changed files with 54 additions and 1 deletions
|
|
@ -37,6 +37,9 @@ DEFAULTS = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"movie_template": "$title ($year)/$title ($year) $quality",
|
"movie_template": "$title ($year)/$title ($year) $quality",
|
||||||
"episode_template": "$series/Season $season/$series - S$seasonE$episode - $episodetitle $quality",
|
"episode_template": "$series/Season $season/$series - S$seasonE$episode - $episodetitle $quality",
|
||||||
|
# YouTube channels organise as a Plex "TV by date" show: channel = series,
|
||||||
|
# season = upload YEAR, episode named by upload DATE + title.
|
||||||
|
"youtube_template": "$channel/Season $year/$channel - $date - $title",
|
||||||
"verify_with_ffprobe": True,
|
"verify_with_ffprobe": True,
|
||||||
"replace_existing": True,
|
"replace_existing": True,
|
||||||
"transfer_mode": "copy",
|
"transfer_mode": "copy",
|
||||||
|
|
@ -60,7 +63,7 @@ def normalize(raw: Any) -> dict:
|
||||||
d = default_settings()
|
d = default_settings()
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
return d
|
return d
|
||||||
for key in ("movie_template", "episode_template"):
|
for key in ("movie_template", "episode_template", "youtube_template"):
|
||||||
v = raw.get(key)
|
v = raw.get(key)
|
||||||
if isinstance(v, str) and v.strip():
|
if isinstance(v, str) and v.strip():
|
||||||
d[key] = v.strip()
|
d[key] = v.strip()
|
||||||
|
|
@ -154,6 +157,26 @@ def _episode_values(f: dict) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _youtube_values(f: dict) -> dict:
|
||||||
|
"""Template values for a YouTube upload — channel-as-show, season=year, date-named
|
||||||
|
episode (Plex 'TV by date'). ``published_at``/``date`` is 'YYYY-MM-DD'."""
|
||||||
|
channel = f.get("channel") or f.get("series") or f.get("title") or "Unknown"
|
||||||
|
pub = str(f.get("published_at") or f.get("date") or "")[:10]
|
||||||
|
y = m = d = ""
|
||||||
|
if len(pub) == 10 and pub[4] == "-" and pub[7] == "-":
|
||||||
|
y, m, d = pub[0:4], pub[5:7], pub[8:10]
|
||||||
|
has_year = _plausible_year(y)
|
||||||
|
return {
|
||||||
|
"channel": channel,
|
||||||
|
"title": _str(f.get("title")) or "Unknown",
|
||||||
|
"year": y if has_year else "",
|
||||||
|
"date": pub if has_year else "", # only a trustworthy full date
|
||||||
|
"month": m if has_year else "",
|
||||||
|
"day": d if has_year else "",
|
||||||
|
"videoid": _str(f.get("youtube_id")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def render_template(template: Any, values: dict) -> str:
|
def render_template(template: Any, values: dict) -> str:
|
||||||
"""Substitute ``$token`` / ``${token}`` from ``values`` into ``template``. Each
|
"""Substitute ``$token`` / ``${token}`` from ``values`` into ``template``. Each
|
||||||
value is path-sanitised first (so a title with '/' can't spawn a folder), and
|
value is path-sanitised first (so a title with '/' can't spawn a folder), and
|
||||||
|
|
@ -194,6 +217,9 @@ def render_path(scope: Any, root: Any, fields: dict, settings: Any, ext: Any) ->
|
||||||
elif sc == "episode":
|
elif sc == "episode":
|
||||||
tmpl = settings.get("episode_template") or DEFAULTS["episode_template"]
|
tmpl = settings.get("episode_template") or DEFAULTS["episode_template"]
|
||||||
values = _episode_values(fields)
|
values = _episode_values(fields)
|
||||||
|
elif sc == "youtube":
|
||||||
|
tmpl = settings.get("youtube_template") or DEFAULTS["youtube_template"]
|
||||||
|
values = _youtube_values(fields)
|
||||||
else:
|
else:
|
||||||
base = (sanitize(fields.get("title")) or "download") + _ext(ext)
|
base = (sanitize(fields.get("title")) or "download") + _ext(ext)
|
||||||
return {"dir": root, "filename": base, "path": os.path.join(root, base)}
|
return {"dir": root, "filename": base, "path": os.path.join(root, base)}
|
||||||
|
|
|
||||||
|
|
@ -83,3 +83,30 @@ def test_token_values_cannot_inject_extra_folders():
|
||||||
fields = {"title": "AC/DC Live", "year": 2020, "quality": "Bluray-1080p"}
|
fields = {"title": "AC/DC Live", "year": 2020, "quality": "Bluray-1080p"}
|
||||||
got = organization.render_path("movie", "/m", fields, organization.default_settings(), ".mkv")
|
got = organization.render_path("movie", "/m", fields, organization.default_settings(), ".mkv")
|
||||||
assert got["dir"] == os.path.join("/m", "ACDC Live (2020)")
|
assert got["dir"] == os.path.join("/m", "ACDC Live (2020)")
|
||||||
|
|
||||||
|
|
||||||
|
# ── youtube channels: Plex "TV by date" organisation ─────────────────────────
|
||||||
|
def test_youtube_default_template_is_channel_year_date():
|
||||||
|
fields = {"channel": "Veritasium", "title": "How Electricity Works",
|
||||||
|
"published_at": "2024-03-15", "youtube_id": "abc123"}
|
||||||
|
got = organization.render_path("youtube", "/yt", fields, organization.default_settings(), ".mp4")
|
||||||
|
assert got["path"] == os.path.join(
|
||||||
|
"/yt", "Veritasium", "Season 2024",
|
||||||
|
"Veritasium - 2024-03-15 - How Electricity Works.mp4")
|
||||||
|
|
||||||
|
|
||||||
|
def test_youtube_sanitises_channel_and_title():
|
||||||
|
# slashes/illegal chars in channel or title can't spawn folders
|
||||||
|
fields = {"channel": "Mark/Rober", "title": "Glitter Bomb 4/5",
|
||||||
|
"published_at": "2023-12-01", "youtube_id": "v1"}
|
||||||
|
got = organization.render_path("youtube", "/yt", fields, organization.default_settings(), ".mp4")
|
||||||
|
assert got["dir"] == os.path.join("/yt", "MarkRober", "Season 2023")
|
||||||
|
assert got["filename"] == "MarkRober - 2023-12-01 - Glitter Bomb 45.mp4"
|
||||||
|
|
||||||
|
|
||||||
|
def test_youtube_undated_falls_back_cleanly():
|
||||||
|
# no date → no empty 'Season ' garbage, no dangling ' - ' in the filename
|
||||||
|
fields = {"channel": "Some Channel", "title": "Mystery", "published_at": None}
|
||||||
|
got = organization.render_path("youtube", "/yt", fields, organization.default_settings(), ".mp4")
|
||||||
|
assert got["dir"] == os.path.join("/yt", "Some Channel", "Season")
|
||||||
|
assert got["filename"] == "Some Channel - Mystery.mp4"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue