diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d18adb..555d3fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this fork. Upstream history before the fork point is in t point is tag `upstream-baseline`. Versions continue upstream's numbering (its last tag was `2.2.0-beta`); the fork's first release is 3.0.0 because exit codes and the cookie store changed in ways a caller can notice. +## Unreleased + +### Fixed +- `mam/accept_unsnatched`: a MAM-only subtitle that merely *contains* the row's series name is no longer + treated as that series name (`token_set_ratio` was 100 for a subset). *Dune* was accepted against + *Dune: Children of Dune* / *Dune: Dune Messiah* (series Dune); the audio of book 1 could be filed as a + later cycle volume when that sequel was the only row that also passed the author and file-type checks. + The subtitle must now *be* the series name (`token_sort_ratio`, same bar as the main title). + *Leviathan Wakes: The Expanse, Book 1* in series The Expanse still matches. + ## 3.0.6 - 2026-10-07 ### Added diff --git a/myx_classes.py b/myx_classes.py index 9c84a66..f4d1616 100644 --- a/myx_classes.py +++ b/myx_classes.py @@ -90,7 +90,8 @@ def sameMamTitle(ours, theirs, series=()): must agree too ("Cradle: Unsouled" is not "Cradle: Soulsmith"); a subtitle only we have is refused ("Thrawn: Treason" is not "Thrawn"); one only MAM has is accepted only when it is the name of the candidate's own series (`series`, from MAM's series_info) and names no bundle or part: "Leviathan Wakes: The Expanse, Book 1" in series - The Expanse yes; "Dune: The Complete Saga", "Dune: Part One", "Mistborn: Secret History" no.""" + The Expanse yes; "Dune: The Complete Saga", "Dune: Part One", "Mistborn: Secret History" no. token_sort, not + token_set: "Dune: Children of Dune" contains the series name "Dune" but is the sequel, not this book.""" om, osub, _ = _titleParts(ours) tm, tsub, traw = _titleParts(theirs) if not om or not tm or fuzz.token_sort_ratio(om, tm) < MAM_TITLE_MIN: @@ -113,7 +114,9 @@ def sameMamTitle(ours, theirs, series=()): if not tsub: return True names = [_titleWords(_clip(n)) for n in series or ()] - return any(n and fuzz.token_set_ratio(n, tsub) >= MAM_TITLE_MIN for n in names) + # token_sort (same bar as the main title): token_set is 100 when the series name is only a subset of + # the subtitle, so "Dune: Children of Dune" / "Dune: Dune Messiah" in series Dune used to pass + return any(n and fuzz.token_sort_ratio(n, tsub) >= MAM_TITLE_MIN for n in names) def _num(part): diff --git a/tests/test_mam_unsnatched.py b/tests/test_mam_unsnatched.py index 0fe7a3c..a4d5af0 100644 --- a/tests/test_mam_unsnatched.py +++ b/tests/test_mam_unsnatched.py @@ -273,6 +273,17 @@ def test_a_sibling_volume_is_not_accepted(self): row(2, "Cradle: Soulsmith", authors=("Will Wight",))], mb=mb) self.assertIsNone(best) + def test_a_sequel_whose_subtitle_contains_the_series_name_is_not_accepted(self): + # MAM title+author+filename search for "Dune.m4b" also returns later cycle books; token_set used to + # treat "Children of Dune" as series "Dune". The own torrent is listed Last, First (fails the author + # check), so the sequel was the only passing row and Dune was filed as book 3. + mb = release(title="Dune", authors=("Frank Herbert",), name="Frank Herbert - Dune", file="Dune.m4b") + best, out, _ = self.best([row(1, "Dune", authors=("Herbert, Frank",)), + row(2, "Dune: Children of Dune", authors=("Frank Herbert",), + series='{"1": ["Dune", "3"]}')], mb=mb) + self.assertIsNone(best) + self.assertIn("no unsnatched one passes the title, author and file type check", out) + def test_a_mam_only_subtitle_must_be_the_rows_own_series(self): rows = [row(1, "Leviathan Wakes: The Expanse, Book 1", authors=("James S. A. Corey",), series='{"9": ["The Expanse", "1"]}')] @@ -343,10 +354,14 @@ def test_sibling_volumes_parts_and_box_sets_are_not_the_same_title(self): ("Red Rising", "Red Rising [Graphic Audio]"), ("The Hobbit", "The Hobbit (Dramatised)"), ("Dune (Abridged)", "Dune")]: self.assertFalse(same(ours, theirs), (ours, theirs)) - # a subtitle only MAM has passes only when it is the candidate's own series + # a subtitle only MAM has passes only when it *is* the candidate's own series, not when it merely + # contains that name (token_set used to accept "Dune: Children of Dune" against series Dune) self.assertTrue(same("Leviathan Wakes", "Leviathan Wakes: The Expanse, Book 1", ["The Expanse"])) self.assertFalse(same("Leviathan Wakes", "Leviathan Wakes: The Expanse, Book 1")) self.assertFalse(same("Mistborn", "Mistborn: Secret History", ["Mistborn"])) + self.assertFalse(same("Dune", "Dune: Children of Dune", ["Dune"])) + self.assertFalse(same("Dune", "Dune: Dune Messiah", ["Dune"])) + self.assertFalse(same("Foundation", "Foundation: Foundation and Empire", ["Foundation"])) for ours, theirs in [("Shogun", "Shōgun"), ("Ender’s Game", "Ender's Game"), ("Across the Fence: Expanded Edition: The Secret War in Vietnam (Unabridged)", "Across the Fence: Expanded Edition - The Secret War in Vietnam"),