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,
|
||||
"movie_template": "$title ($year)/$title ($year) $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,
|
||||
"replace_existing": True,
|
||||
"transfer_mode": "copy",
|
||||
|
|
@ -60,7 +63,7 @@ def normalize(raw: Any) -> dict:
|
|||
d = default_settings()
|
||||
if not isinstance(raw, dict):
|
||||
return d
|
||||
for key in ("movie_template", "episode_template"):
|
||||
for key in ("movie_template", "episode_template", "youtube_template"):
|
||||
v = raw.get(key)
|
||||
if isinstance(v, str) and 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:
|
||||
"""Substitute ``$token`` / ``${token}`` from ``values`` into ``template``. Each
|
||||
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":
|
||||
tmpl = settings.get("episode_template") or DEFAULTS["episode_template"]
|
||||
values = _episode_values(fields)
|
||||
elif sc == "youtube":
|
||||
tmpl = settings.get("youtube_template") or DEFAULTS["youtube_template"]
|
||||
values = _youtube_values(fields)
|
||||
else:
|
||||
base = (sanitize(fields.get("title")) or "download") + _ext(ext)
|
||||
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"}
|
||||
got = organization.render_path("movie", "/m", fields, organization.default_settings(), ".mkv")
|
||||
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