diff --git a/CHANGELOG.md b/CHANGELOG.md index 4097689..5e1ebcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ first release is 3.0.0 because exit codes and the cookie store changed in ways a ## Unreleased +### Fixed +- MAM ranking (and interactive pick) sat under `Config/flags/verbose`, so a run with verbose off left + `bestMAMMatch` unset even when MAM returned snatched hits. `metadata=mam` then filed nothing; + `mam-audible` skipped MAM and foreign-language detection. Ranking now runs the same way as Audible's. + ## 3.0.4 - 2026-09-22 ### Fixed diff --git a/myx_classes.py b/myx_classes.py index f3fae2f..489e054 100644 --- a/myx_classes.py +++ b/myx_classes.py @@ -1115,67 +1115,68 @@ def getMAMBooks(self, cfg, bookFile:BookFile): if (verbose): print(f"Found {len(self.mamMatches)} MAM match(es)\n\n") - if interactive: - #display choices to user to pick from - count = len(books) - if (count == 1): - self.bestMAMMatch=books[0] - - elif (count > 1): - booksFound=[] - choices=[] - for abook in books: - booksFound.append(abook) - - #display - print(f"[{len(booksFound)}] {abook.title}({abook.releaseDate}) by {abook.getAuthors()}, ASIN: {abook.asin}, Language: {abook.language}") - choices.append (len(booksFound)) - - #add none - print(f"[0] None of the above") - choices.append (0) - - choice = myx_utilities.promptChoice (f"Pick a match [0-{len(booksFound)}]: ", choices) - if choice != 0: - if verbose: print(f"You've selected [{choice}] {booksFound[choice-1].title}({booksFound[choice-1].releaseDate}) by {booksFound[choice-1].getAuthors()}, ASIN: {booksFound[choice-1].asin}, Language: {booksFound[choice-1].language}") - self.bestMAMMatch=booksFound[choice-1] + # same shape as _rankAudible: verbose only gates the count line, not ranking + if interactive: + #display choices to user to pick from + count = len(books) + if (count == 1): + self.bestMAMMatch=books[0] + + elif (count > 1): + booksFound=[] + choices=[] + for abook in books: + booksFound.append(abook) + + #display + print(f"[{len(booksFound)}] {abook.title}({abook.releaseDate}) by {abook.getAuthors()}, ASIN: {abook.asin}, Language: {abook.language}") + choices.append (len(booksFound)) + + #add none + print(f"[0] None of the above") + choices.append (0) + + choice = myx_utilities.promptChoice (f"Pick a match [0-{len(booksFound)}]: ", choices) + if choice != 0: + if verbose: print(f"You've selected [{choice}] {booksFound[choice-1].title}({booksFound[choice-1].releaseDate}) by {booksFound[choice-1].getAuthors()}, ASIN: {booksFound[choice-1].asin}, Language: {booksFound[choice-1].language}") + self.bestMAMMatch=booksFound[choice-1] + else: + self.bestMAMMatch=None + + else: + bestMatchRate=0 + #find the best match + print(f"Finding the best MAM match out of {len(books)} results") + targetBook = '|'.join([book.title, book.getAuthors(), book.getSeriesParts()]) + + for abook in books: + #if this book is snatched, include in the match + if abook.snatched: + #the author is known, check if this book is this authors book + #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: + mamBook = '|'.join([abook.getAuthors(), abook.getCleanTitle(), abook.getSeriesParts()]) + if add_narrators: + mamBook = '|'.join([mamBook, abook.getNarrators()]) + elif myx_utilities.isThisMyBookTitle(rankTitle, abook, cfg): + mamBook = '|'.join([abook.getAuthors(), abook.getCleanTitle(), abook.getSeriesParts()]) + if add_narrators: + mamBook = '|'.join([mamBook, abook.getNarrators()]) else: - self.bestMAMMatch=None + print (f"This book doesn't have a matching title or author, checking the next book...") + continue - else: - bestMatchRate=0 - #find the best match - print(f"Finding the best MAM match out of {len(books)} results") - targetBook = '|'.join([book.title, book.getAuthors(), book.getSeriesParts()]) - - for abook in books: - #if this book is snatched, include in the match - if abook.snatched: - #the author is known, check if this book is this authors book - #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: - mamBook = '|'.join([abook.getAuthors(), abook.getCleanTitle(), abook.getSeriesParts()]) - if add_narrators: - mamBook = '|'.join([mamBook, abook.getNarrators()]) - elif myx_utilities.isThisMyBookTitle(rankTitle, abook, cfg): - mamBook = '|'.join([abook.getAuthors(), abook.getCleanTitle(), abook.getSeriesParts()]) - if add_narrators: - mamBook = '|'.join([mamBook, abook.getNarrators()]) - else: - print (f"This book doesn't have a matching title or author, checking the next book...") - continue - - #include this book in the comparison - matchRate=myx_utilities.fuzzymatch(targetBook, mamBook) - abook.matchRate=matchRate[fuzzy_match] - - print(f"\tMatch Rate: {matchRate}\n\tSearch: {targetBook}\n\tResult: {mamBook}\n\tBest Match Rate: {bestMatchRate}\n") - - if (matchRate[fuzzy_match] > bestMatchRate): - bestMatchRate=matchRate[fuzzy_match] - self.bestMAMMatch=abook + #include this book in the comparison + matchRate=myx_utilities.fuzzymatch(targetBook, mamBook) + abook.matchRate=matchRate[fuzzy_match] + + print(f"\tMatch Rate: {matchRate}\n\tSearch: {targetBook}\n\tResult: {mamBook}\n\tBest Match Rate: {bestMatchRate}\n") + + if (matchRate[fuzzy_match] > bestMatchRate): + bestMatchRate=matchRate[fuzzy_match] + self.bestMAMMatch=abook else: #no metadata, get the first match? if len(self.mamMatches): diff --git a/tests/test_matching.py b/tests/test_matching.py index f7bce77..75ae1a5 100644 --- a/tests/test_matching.py +++ b/tests/test_matching.py @@ -613,6 +613,42 @@ def test_different_mam_title_by_the_parsed_author_is_rejected(self): self.assertIn("This book doesn't have a matching title or author", out) +class MamRankingWithoutVerboseTest(unittest.TestCase): + """MAM ranking must run when Config/flags/verbose is off (it used to sit under that print).""" + + def _run(self, name, id3, hits, verbose=0): + with tempfile.TemporaryDirectory() as td: + cfg = FakeConfig(td, **{"Config/flags/verbose": verbose}) + mb = mambook(name, id3) + out = io.StringIO() + with contextlib.redirect_stdout(out), patch("myx_mam.getMAMBook", return_value=hits): + best = mb.getMAMBooks(cfg, mb.files[0]) + return mb, best, out.getvalue() + + def test_snatched_hit_is_accepted_when_verbose_is_off(self): + mb, best, out = self._run( + "Lee Child - Night School", + id3_book("Night School", ["Lee Child"], 400 * 60), + [_mam_book("Night School", ["Lee Child"])], + ) + self.assertIsNotNone(best) + self.assertEqual(best.title, "Night School") + self.assertTrue(mb.matchFound()) + self.assertIn("Finding the best MAM match", out) + self.assertNotIn("Found 1 MAM match(es)", out) + + def test_wrong_title_is_still_rejected_when_verbose_is_off(self): + # unknown artist -> authors come from the release name, so the title gate applies + mb, best, out = self._run( + "Lee Child - The Enemy", + id3_book("The Enemy", ["unknown artist"], 400 * 60), + [_mam_book("Killing Floor", ["Lee Child"])], + ) + self.assertIsNone(best) + self.assertFalse(mb.matchFound()) + self.assertIn("This book doesn't have a matching title or author", out) + + class NarratorInArtistTagTest(unittest.TestCase): def secrets(self): return [product("B0BYRNE000", "The Secret", ["Rhonda Byrne"], 264),