Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions myx_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand Down
17 changes: 16 additions & 1 deletion tests/test_mam_unsnatched.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}')]
Expand Down Expand Up @@ -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"),
Expand Down
Loading