""" 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, clean_query, has_episode_marker, leading_episode_number, naive_title, parse_episode_filename, parse_movie_filename, season_from_folder_name, sequel_variants, year_in, ) # ── §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") == [] # ── §10.1/V10: wider sequel-index handling ────────────────────────────────── def test_sequel_variants_roman_numeral_offers_the_digit_form(): v = sequel_variants("Old Frontier III") assert "Old Frontier" in v assert "Old Frontier 3" in v def test_sequel_variants_rewrites_a_part_keyword_index_but_keeps_the_name(): # §10.1/V14: with a "Part"/"Episode"/… keyword the bare base is # withheld — "Some Saga" alone collides with a franchise-origin film. v = sequel_variants("Some Saga Part 2") assert "Some Saga II" in v assert "Some Saga 2" in v assert "Some Saga" not in v def test_sequel_variants_reads_a_spelled_out_index(): v = sequel_variants("Story Chapter Three") assert "Story 3" in v and "Story III" in v assert "Story" not in v # keyword present -> no bare base def test_sequel_variants_saga_shape_does_not_offer_the_bare_franchise(): # Every " Chapter " was matching the franchise's first entry # because the bare "" variant hit it at ratio 1.0 (§10.1/V14). v = sequel_variants("Some Saga Chapter III") assert "Some Saga" not in v assert "Some Saga 3" in v def test_sequel_variants_ignores_a_trailing_word_that_is_not_an_index(): assert sequel_variants("The Dark Knight") == [] assert sequel_variants("In Bruges") == [] def test_sequel_variants_ignores_a_four_digit_year_suffix(): assert sequel_variants("Blade Runner 2049") == [] def test_year_in_lifts_a_year_from_a_show_folder_name(): assert year_in("Some.Show.2022.S01") == 2022 assert year_in("Some Show") is None assert year_in("Episode 100 of 2010") == 2010 def test_clean_query_despaces_a_folder_name_without_eating_the_last_word(): # naive_title would rsplit on the last dot and drop ".Name" assert clean_query("Some.Show.Name") == "Some Show Name" assert clean_query("Some.Show.Name.S01") == "Some Show Name S01" assert naive_title("Some.Show.Name") != "Some Show Name" # the trap it avoids # ── §10.1/V14: telling a real episode marker from a mangled number ────────── def test_has_episode_marker_accepts_real_markers(): for name in ["Some.Show.S01E08.mkv", "some.show.s1.e8.mkv", "Some Show 1x08.mkv", "Some Show 01x08.mkv", "Some Show Episode 8.mkv", "Some Show ep08.mkv", "Some Show ep.8.mkv", "Some Show Season 1.mkv", "Une Serie Saison 3.mkv"]: assert has_episode_marker(name), name def test_has_episode_marker_rejects_bare_numbers_and_ordinary_words(): # "1080p" truncated to "108", "1280" left in, a year, plain words that # merely contain "ep" — none of these are episode markers. for name in ["Some.Film.2017.MULTI.108.grp.mkv", "Some.Flick.2013.1280.x264-grp.mkv", "Some Movie 2017.mkv", "The Dark Knight.mkv", "Sleep 8.mkv", "Deep 8 mm.mkv", "Ocean's 11.mkv"]: assert not has_episode_marker(name), name # ── bug 2026-08-29: guessit peels "Volume N" off the title ───────────────── def test_movie_volume_number_is_folded_back_into_the_title(): # guessit parses both to title="Some Saga" + volume=1/2; leaving it that # way collapsed the two parts onto one TMDB match and made "Fix match" # (grouped by display_title) unable to separate them. r1 = parse_movie_filename("Some.Saga.Volume.1.2003.mkv") r2 = parse_movie_filename("Some.Saga.Volume.2.2004.mkv") assert r1.display_title == "Some Saga 1" assert r2.display_title == "Some Saga 2" assert r1.display_title != r2.display_title assert r1.year == 2003 and r2.year == 2004 def test_movie_without_a_volume_token_is_unchanged(): # the "3" is already part of guessit's title here, not a volume field r = parse_movie_filename("Old.Frontier.3.2001.mkv") assert r.display_title == "Old Frontier 3" # ── §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_season_folder_book_word_roman_numeral(): # Some shows number their seasons by "book" (Roman numerals) rather # than "season"/"saison" — real, observed vocabulary (§3.4). assert season_from_folder_name("Livre I") == 1 assert season_from_folder_name("Livre VI") == 6 def test_season_folder_bare_s_abbreviation(): # A common abbreviated convention distinct from SEASON_WORDS' full # words — found live: a show with "S1"/"S2"/"S3" folders instead of # "Season 1" etc, exactly the shape that grouped its later seasons as # loose individual entries rather than under the show. assert season_from_folder_name("S1") == 1 assert season_from_folder_name("S2") == 2 assert season_from_folder_name("S02") == 2 def test_season_folder_bare_s_abbreviation_does_not_match_inside_a_longer_name(): # Anchored to the whole folder name — a real folder that merely starts # with "S" followed by digits somewhere in a longer, unrelated name # must not be read as a season abbreviation. assert season_from_folder_name("S1 Extended Cut") is None assert season_from_folder_name("Something2") is None 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 # ── §3.4c: a bare leading episode number, guessit's 3-digit blind spot ─────── def test_leading_episode_number_reads_the_whole_number(): assert leading_episode_number("001 Episode's Own Title.mkv") == 1 assert leading_episode_number("099 Episode's Own Title.mkv") == 99 def test_leading_episode_number_not_split_by_guessit_at_three_digits(): # The real bug this guards: guessit itself reads a bare "100" as season=1, # episode=0 (a concatenated SxxE guess), not episode=100 — silently, with # nothing in its output telling apart a real 2-digit episode from this. assert leading_episode_number("100 Episode's Own Title.mkv") == 100 def test_leading_episode_number_ignores_a_leading_year(): # Four digits, not the 1-3 an episode-number prefix can be — this is a # normal movie filename shape, not an episode number. assert leading_episode_number("2010 - Some Movie.mkv") is None def test_leading_episode_number_none_without_a_leading_number(): assert leading_episode_number("Some Show.S01E01.mkv") 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 == ""