From c0c77590b1bc430cd9337d2272e1bf597b3e0588 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 30 Oct 2026 23:17:55 -0700 Subject: [PATCH] fix(media): a sidecar named "episode.mkv.srt" anchors, and loses the container word The first run against the real library died in the anchor lookup: a subtitle whose own name carries the media extension matches BOTH prefix forms of its video, and the "keep the longest" comparison read the destination slot of the tuple instead of the length slot. The fixture only ever matched one prefix, so the seam was green over a crash. The tail the sidecar keeps is now stripped of that container word: Jellyfin and Plex both read "Show S01E01.forced.srt", not "Show S01E01.mkv.forced.srt", and an nfo still carries the media extension because that one is constructed, not parsed. --- scripts/media-shows.py | 10 +++++++++- tests/media_shows.bats | 7 +++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/media-shows.py b/scripts/media-shows.py index 44d52bb..ccee97c 100755 --- a/scripts/media-shows.py +++ b/scripts/media-shows.py @@ -112,6 +112,9 @@ VIDEO_EXT = {".mkv", ".mp4", ".avi", ".m4v", ".mov", ".ts", ".m2ts", ".mpg", SUB_EXT = {".srt", ".ass", ".ssa", ".sub", ".vtt"} IMG_EXT = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tbn"} NFO_EXT = {".nfo", ".xml"} +# The container's own word, for a sidecar named "episode.mkv.srt": that component +# is not a language flag, and both matchers read the plain form better. +CONTAINER = {e[1:] for e in VIDEO_EXT} # Files that are never anybody's business: hidden, forks, resume state, thumbnails. SKIP_NAME = re.compile(r"^(\.|#|dl_state|@eaDir|Thumbs\.db|desktop\.ini)|\.!q$") # Show-level metadata keeps its own name at the series root; season-level at the @@ -556,7 +559,7 @@ def anchor_of(path, stem): base = os.path.basename(src) for prefix in (os.path.splitext(base)[0], base): if stem == prefix or stem.startswith(prefix + "."): - if best is None or len(prefix) > best[1]: + if best is None or len(prefix) > best[2]: best = (src, dst, len(prefix)) if best: return best[0], best[1] @@ -568,6 +571,11 @@ def sidecar_name(r, src, dst): carry — hung on the video file's new base.""" stem = os.path.splitext(os.path.basename(src))[0] tail = os.path.splitext(r["base"])[0][len(stem):] + # "Show.S01E01.mkv.srt" carries the container's name, not a flag: Jellyfin and + # Plex both read "Show S01E01.srt", so the container word is dropped here. + parts = tail.split(".") + if len(parts) > 1 and parts[1].lower() in CONTAINER: + tail = "".join("." + p for p in parts[2:]) if r["ext"] in NFO_EXT: tail = os.path.splitext(os.path.basename(src))[1] + tail return clean(os.path.splitext(os.path.basename(dst))[0] + tail + r["ext"]) diff --git a/tests/media_shows.bats b/tests/media_shows.bats index fd4029b..db5e349 100755 --- a/tests/media_shows.bats +++ b/tests/media_shows.bats @@ -54,6 +54,7 @@ setup() { # trailer: four things that have to end up in four different places. at "$SEASON/The.Examples.S01E01.Pilot.1080p.x265.mkv" 9000 at "$SEASON/The.Examples.S01E01.Pilot.1080p.x265.en.srt" 200 + at "$SEASON/The.Examples.S01E01.Pilot.1080p.x265.mkv.forced.srt" 250 at "$SEASON/folder.jpg" 300 at "$SEASON/Trailers/teaser.mkv" 1000 at "$SEASON/tvshow.nfo" 400 @@ -143,6 +144,12 @@ PY = "$LIB/The Examples (2010)/Season 01/The Examples S01E01.en.srt" ] } +@test "a subtitle that carries the container's word in its own name still follows the episode" { + plan + [ "$(dst "$SEASON/The.Examples.S01E01.Pilot.1080p.x265.mkv.forced.srt")" \ + = "$LIB/The Examples (2010)/Season 01/The Examples S01E01.forced.srt" ] +} + @test "a season image keeps its own name; a series nfo keeps the series folder" { plan [ "$(dst "$SEASON/folder.jpg")" = "$LIB/The Examples (2010)/Season 01/folder.jpg" ]