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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ first release is 3.0.0 because exit codes and the cookie store changed in ways a

## Unreleased

### Fixed
- MAM ranking accepted a different snatched book by the same author when the id3 title and artist were
already usable: search is by filename (`01.mp3`, `file.m4b`), and the author-only gate skipped the title
check unless the release name had to supply title or authors. `The Guest` tagged as James Patterson was
filed as *Along Came a Spider* (token_sort 67, above `matchrate` 60). Ranking now requires a title match
whenever the id3/parsed title is not junk, and applies `Config/matchrate` the same way Audible already did.

## 3.0.4 - 2026-09-22

### Fixed
Expand Down
18 changes: 13 additions & 5 deletions myx_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1092,9 +1092,13 @@ def getMAMBooks(self, cfg, bookFile:BookFile):
extension = f'"{bookFile.getExtension()}"'
#for RANKING (never for the MAM query string) use the parsed release name where the id3 tags are junk
rankBook, parsedApplied = self.applyParsedName(self.ffprobeBook, cfg) if (self.ffprobeBook is not None and not interactive) else (None, [])
# when the parse supplied title or authors, the title gate must see the search title
# (id3 or parsed), not the file basename — otherwise `file.m4b` rejects a correct MAM hit
rankTitle = rankBook.title if ("title" in parsedApplied or "authors" in parsedApplied) else title
# MAM is searched by filename, so a usable id3/parsed title must still gate the snatched hits.
# Using the file basename here used to skip the title gate whenever tags were already good
# (author-only), and filed Along Came a Spider as The Guest.
if rankBook is not None and not myx_names.isJunkTitle(rankBook.title):
rankTitle = rankBook.title
else:
rankTitle = title

# Search using book key and authors (using or search in case the metadata is bad)
print(f"Searching MAM for\n\tTitleFilename: {title}\n\tauthors:{authors}")
Expand Down Expand Up @@ -1144,9 +1148,13 @@ def getMAMBooks(self, cfg, bookFile:BookFile):

else:
bestMatchRate=0
minMatchRate = int(cfg.get("Config/matchrate"))
#find the best match
print(f"Finding the best MAM match out of {len(books)} results")
targetBook = '|'.join([book.title, book.getAuthors(), book.getSeriesParts()])
# a usable title (id3 or parsed) must pass the title gate: MAM search is filename-based,
# so author-only used to accept that author's other snatched books (token_sort 67 >= 60)
requireTitle = rankBook is None or not myx_names.isJunkTitle(rankBook.title)

for abook in books:
#if this book is snatched, include in the match
Expand All @@ -1155,7 +1163,7 @@ def getMAMBooks(self, cfg, bookFile:BookFile):
#otherwise, if maybe this title is close enough
#print (f"{abook.title} by {abook.authors}...")
authorOK = bool(len(book.authors) and myx_utilities.isThisMyAuthorsBook(book.authors, abook, cfg))
if authorOK and "title" not in parsedApplied and "authors" not in parsedApplied:
if authorOK and not requireTitle:
mamBook = '|'.join([abook.getAuthors(), abook.getCleanTitle(), abook.getSeriesParts()])
if add_narrators:
mamBook = '|'.join([mamBook, abook.getNarrators()])
Expand All @@ -1173,7 +1181,7 @@ def getMAMBooks(self, cfg, bookFile:BookFile):

print(f"\tMatch Rate: {matchRate}\n\tSearch: {targetBook}\n\tResult: {mamBook}\n\tBest Match Rate: {bestMatchRate}\n")

if (matchRate[fuzzy_match] > bestMatchRate):
if (matchRate[fuzzy_match] > bestMatchRate) and (matchRate[fuzzy_match] >= minMatchRate):
bestMatchRate=matchRate[fuzzy_match]
self.bestMAMMatch=abook
else:
Expand Down
19 changes: 19 additions & 0 deletions tests/test_matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,25 @@ def test_different_mam_title_by_the_parsed_author_is_rejected(self):
self.assertIsNone(best)
self.assertIn("This book doesn't have a matching title or author", out)

def test_good_id3_author_and_title_still_reject_a_different_book_by_that_author(self):
# usable id3 title AND artist: parse does not apply, so the author-only gate used to accept
# Patterson's Along Came a Spider as The Guest (token_sort 67 >= matchrate 60). MAM search is
# the file basename (`file.m4b` / `01.mp3`), which is how the wrong snatched hit is returned.
best, out = self._run("The Guest",
id3_book("The Guest", ["James Patterson"], 400 * 60),
[_mam_book("Along Came a Spider", ["James Patterson"])])
self.assertIsNone(best)
self.assertIn("This book doesn't have a matching title or author", out)
self.assertNotIn("Using parsed release name", out)

def test_good_id3_author_and_title_still_accept_the_matching_title(self):
best, out = self._run("The Guest",
id3_book("The Guest", ["James Patterson"], 400 * 60),
[_mam_book("The Guest", ["James Patterson"])])
self.assertIsNotNone(best)
self.assertEqual(best.title, "The Guest")
self.assertNotIn("Using parsed release name", out)


class NarratorInArtistTagTest(unittest.TestCase):
def secrets(self):
Expand Down
Loading