summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_title_parse.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-24 10:04:46 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-24 10:04:46 +0200
commit6af05abf410bbd038ce7fa6915a659defc509071 (patch)
tree09b1c941fa446b077ff51282fa18250998528263 /packages/meshbay-node/tests/test_title_parse.py
parentc4981454078a59f776d484f0f1828f2fc5eaad09 (diff)
downloadmeshbay-6af05abf410bbd038ce7fa6915a659defc509071.tar.gz
feat(node,hub): add Videos group app (poster grid, flat list, TMDB metadata)
Implements docs/mediacenter.md: a "Videos" group application built on the existing files index rather than a separate catalogue. On the node side, new indexer enrichment (technical probe, filename/season parsing, thumbnail generation) runs per-file once an operator has chosen a video_root for the group, plus a TMDB client for on-demand poster/metadata lookups (never client-side, thumbnails delivered over the existing chunk path). On the hub side, a new video-app.js renders a lazily-mounted poster grid or a thumbnail-only flat list, with TMDB entirely optional per group. Along the way: the global apps registry now drives Settings' default-tab picker instead of a hardcoded list, and the video_root is configured from group Settings (like uploads) rather than from Files, with the node refusing to run any TMDB/thumbnail work until one is set. Fixes several bugs found via live testing against a real library, notably a race between two effects writing the same "image ready" state that could leave a poster grid spinning forever on a same-tab revisit — see mediacenter.md §5.4 for the full account of each one.
Diffstat (limited to 'packages/meshbay-node/tests/test_title_parse.py')
-rw-r--r--packages/meshbay-node/tests/test_title_parse.py129
1 files changed, 129 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_title_parse.py b/packages/meshbay-node/tests/test_title_parse.py
new file mode 100644
index 0000000..1a277f2
--- /dev/null
+++ b/packages/meshbay-node/tests/test_title_parse.py
@@ -0,0 +1,129 @@
+"""
+Tests for indexer/title_parse.py — synthetic filenames only, one per
+docs/mediacenter.md §3.3/§3.4 rule. The real ~1950-file library validation
+is a manual acceptance step (§11), not something this repo's corpus holds.
+"""
+
+from meshbay_node.indexer.title_parse import (
+ ParsedName,
+ naive_title,
+ parse_episode_filename,
+ parse_movie_filename,
+ season_from_folder_name,
+ sequel_variants,
+)
+
+
+# ── §3.3 row: plain, well-formed movie filename ──────────────────────────────
+
+def test_plain_movie_filename_parses_confidently():
+ r = parse_movie_filename("The.Great.Adventure.2015.1080p.BluRay.x264.mkv")
+ assert r.display_title == "The Great Adventure"
+ assert r.year == 2015
+ assert r.confidence is True
+
+
+# ── §3.3 row 1: real title in alternative_title ──────────────────────────────
+
+def test_franchise_numeric_code_exposes_alternative_title():
+ r = parse_movie_filename("Franchise.007.-.The.Real.Subtitle.1999.720p.mkv")
+ assert r.year == 1999
+ # guessit lands the franchise fragment in title and the real subtitle in
+ # alternative_title — both must be surfaced so a caller (tmdb.py) can
+ # try either, per the design doc's "search both fields" fix.
+ assert r.alt_title is not None
+
+
+# ── §3.3 row 2: hyphenated title split before a parenthesized year ───────────
+
+def test_naive_title_normalizes_hyphens_dots_and_underscores():
+ nt = naive_title("Hero-Name.(2002).DVDRip.XviD-GROUP.avi")
+ assert "-" not in nt
+ assert "." not in nt
+ assert "_" not in nt
+ assert "2002" not in nt # the year itself is stripped, not just its parens
+ assert "Hero" in nt and "Name" in nt
+
+
+# ── §3.3 row 3: French edition vocabulary stuck to the title ─────────────────
+
+def test_french_edition_phrases_are_stripped():
+ r = parse_movie_filename("Some.Movie.Version.Longue.2010.mkv")
+ assert "version" not in (r.display_title or "").lower()
+ assert "longue" not in (r.display_title or "").lower()
+
+ r2 = parse_movie_filename("Autre.Film.Remasterise.1998.mkv")
+ assert "remasteris" not in (r2.display_title or "").lower()
+
+
+# ── §3.3 row 4: trailing sequel digit / Roman numeral ────────────────────────
+
+def test_sequel_variants_strips_digit_and_offers_roman_numeral():
+ variants = sequel_variants("Some Sequel 2")
+ assert "Some Sequel" in variants
+ assert "Some Sequel II" in variants
+
+
+def test_sequel_variants_empty_when_no_trailing_digit():
+ assert sequel_variants("Some Movie") == []
+
+
+# ── §3.3 row 6: no usable title at all ───────────────────────────────────────
+
+def test_low_confidence_when_no_title_or_year():
+ r = parse_movie_filename("abc123.mkv")
+ assert r.confidence is False
+ # the mandated fallback is always available regardless
+ assert r.naive_title == "abc123"
+
+
+# ── §3.2/§3.4: episode filename with no show name at all ─────────────────────
+
+def test_episode_only_filename_has_no_title_but_has_season_episode():
+ r = parse_episode_filename("S08E02.SUBFRENCH.720p.mkv")
+ assert r.display_title is None
+ assert r.season == 8
+ assert r.episode == 2
+ # confidence is False (no title yet) — the indexer must supply one from
+ # a representative sibling filename in the same folder, per §3.4.
+ assert r.confidence is False
+
+
+def test_episode_filename_with_show_name_parses_confidently():
+ r = parse_episode_filename("Some.Show.Name.S02E05.720p.WEB.mkv")
+ assert r.display_title == "Some Show Name"
+ assert r.season == 2
+ assert r.episode == 5
+ assert r.confidence is True
+
+
+# ── §3.4: season-like ancestor folders, including non-English vocabulary ────
+
+def test_season_folder_english():
+ assert season_from_folder_name("Season 2") == 2
+
+
+def test_season_folder_french_word():
+ assert season_from_folder_name("Saison 3") == 3
+
+
+def test_season_folder_roman_numeral():
+ assert season_from_folder_name("Saison IV") == 4
+
+
+def test_specials_folder_maps_to_season_zero():
+ assert season_from_folder_name("Specials") == 0
+ assert season_from_folder_name("Bonus") == 0
+ assert season_from_folder_name("Extras") == 0
+
+
+def test_non_season_folder_name_returns_none():
+ assert season_from_folder_name("Some Show Name") is None
+
+
+def test_parsed_name_is_a_plain_dataclass():
+ # sanity: constructible with just the one required field, per the
+ # "None => caller must supply from elsewhere" contract.
+ p = ParsedName(display_title=None)
+ assert p.confidence is False
+ assert p.naive_title == ""