summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_title_parse.py
blob: 95c798b5a76d95978e4cca3ec83abb0e08643611 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
"""
Tests for indexer/title_parse.py — synthetic filenames only, one per
docs/MESHBAY_DESIGN.md §9.7 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") == []


# ── 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():
    # 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 "<Saga> Chapter <N>" was matching the franchise's first entry
    # because the bare "<Saga>" variant hit it at ratio 1.0 (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


# ── 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


# ── V-findings: 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 == ""