From ceff0b1d487cb926731f371116bd04427bb78726 Mon Sep 17 00:00:00 2001 From: Richard Bateman Date: Mon, 12 Feb 2024 19:43:14 -0700 Subject: [PATCH 1/5] Created feature branch author_and_series_lookup From ad28dfbe6129b4f5642cb123b2b196b01b6d81fc Mon Sep 17 00:00:00 2001 From: Richard Bateman Date: Mon, 12 Feb 2024 19:45:35 -0700 Subject: [PATCH 2/5] Add support for lookup by series, author -- author not working --- src/audible_cli/cmds/cmd_download.py | 75 ++++++++++++++++++++++++++-- src/audible_cli/models.py | 34 +++++++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/src/audible_cli/cmds/cmd_download.py b/src/audible_cli/cmds/cmd_download.py index 20282c4..9b7f7bf 100644 --- a/src/audible_cli/cmds/cmd_download.py +++ b/src/audible_cli/cmds/cmd_download.py @@ -641,7 +641,17 @@ def display_counter(): @click.option( "--title", "-t", multiple=True, - help="tile of the audiobook (partial search)" + help="title of the audiobook (partial search)" +) +@click.option( + "--author", "-a", + multiple=True, + help="author of the audiobook (partial search)" +) +@click.option( + "--series", "-s", + multiple=True, + help="series of the audiobook (partial search)" ) @click.option( "--aax", @@ -748,10 +758,13 @@ async def cli(session, api_client, **params): get_all = params.get("all") is True asins = params.get("asin") titles = params.get("title") - if get_all and (asins or titles): - logger.error("Do not mix *asin* or *title* option with *all* option.") + authors = params.get("author") + series = params.get("series") + if get_all and (asins or titles or authors or series): + logger.error("Do not mix *asin*, *title*, *series*, or *author* options with the *all* option.") click.Abort() + # what to download get_aax = params.get("aax") get_aaxc = params.get("aaxc") @@ -869,6 +882,62 @@ async def cli(session, api_client, **params): f"Skip title {title}: Not found in library" ) + for author in authors: + match = library.search_item_by_author(author) + full_match = [i for i in match if i[1] == 100] + + if match: + if no_confirm: + [jobs.append(i[0].asin) for i in full_match or match] + else: + choices = [] + for i in full_match or match: + a = i[0].asin + t = i[0].full_title + l = ", ".join(a["name"] for a in i[0].authors) + c = questionary.Choice(title=f"{a} # {t} by {l}", value=l) + choices.append(c) + + answer = await questionary.checkbox( + f"Found the following matches for '{author}'. Which you want to download?", + choices=choices + ).unsafe_ask_async() + if answer is not None: + [jobs.append(i) for i in answer] + + else: + logger.error( + f"Skip author {author}: Not found in library" + ) + + for s in series: + match = library.search_item_by_series(s) + full_match = [i for i in match if i[1] == 100] + + if match: + if no_confirm: + [jobs.append(i[0].asin) for i in full_match or match] + else: + choices = [] + for i in full_match or match: + a = i[0].asin + t = i[0].full_title + l = ", ".join(s["title"] for s in i[0].series) + c = questionary.Choice(title=f"{a} # {t} in {l}", value=l) + choices.append(c) + + answer = await questionary.checkbox( + f"Found the following matches for '{s}'. Which you want to download?", + choices=choices + ).unsafe_ask_async() + if answer is not None: + [jobs.append(i) for i in answer] + + else: + logger.error( + f"Skip series {s}: Not found in library" + ) + # set queue global QUEUE QUEUE = asyncio.Queue() diff --git a/src/audible_cli/models.py b/src/audible_cli/models.py index f9d2c44..75a800e 100644 --- a/src/audible_cli/models.py +++ b/src/audible_cli/models.py @@ -110,6 +110,26 @@ def substring_in_title(self, substring, p=100): accuracy = self.substring_in_title_accuracy(substring) return accuracy >= p + def substring_in_authors_accuracy(self, substring): + max_accuracy = 0 + authors = self.authors or [] + for author in authors: + name = author["name"] + match = LongestSubString(substring, name) + max_accuracy = max(max_accuracy, match.percentage) + + return round(max_accuracy, 2) + + def substring_in_series_accuracy(self, substring): + max_accuracy = 0 + seriesList = self.series or [] + for series in seriesList: + name = series["title"] + match = LongestSubString(substring, name) + max_accuracy = max(max_accuracy, match.percentage) + + return round(max_accuracy, 2) + def get_cover_url(self, res: Union[str, int] = 500): images = self.product_images res = str(res) @@ -460,6 +480,20 @@ def search_item_by_title(self, search_title, p=80): match.append([i, accuracy]) if accuracy >= p else "" return match + def search_item_by_author(self, search_author, p=80): + match = [] + for i in self._data: + accuracy = i.substring_in_authors_accuracy(search_author) + match.append([i, accuracy]) if accuracy >= p else "" + + return match + def search_item_by_series(self, search_series, p=80): + match = [] + for i in self._data: + accuracy = i.substring_in_series_accuracy(search_series) + match.append([i, accuracy]) if accuracy >= p else "" + + return match class Library(BaseList): From 69811e31ab6d386152e2f1a5fe876b4e4a800285 Mon Sep 17 00:00:00 2001 From: Richard Bateman Date: Tue, 13 Feb 2024 08:32:29 -0700 Subject: [PATCH 3/5] bugfix: add `contributors` to response groups to fix lookup by author --- src/audible_cli/cmds/cmd_download.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/audible_cli/cmds/cmd_download.py b/src/audible_cli/cmds/cmd_download.py index 9b7f7bf..39d8337 100644 --- a/src/audible_cli/cmds/cmd_download.py +++ b/src/audible_cli/cmds/cmd_download.py @@ -824,7 +824,7 @@ async def cli(session, api_client, **params): image_sizes=", ".join(cover_sizes), bunch_size=bunch_size, response_groups=( - "product_desc, media, product_attrs, relationships, " + "product_desc, media, product_attrs, relationships, contributors, " "series, customer_rights, pdf_url" ), start_date=start_date, From 491d91b0184b358567f298599ed7fa69fe6eba72 Mon Sep 17 00:00:00 2001 From: Richard Bateman Date: Tue, 13 Feb 2024 11:07:13 -0700 Subject: [PATCH 4/5] bugfix: download by authors/series doesn't work when you select something --- src/audible_cli/cmds/cmd_download.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/audible_cli/cmds/cmd_download.py b/src/audible_cli/cmds/cmd_download.py index 39d8337..7fb0a61 100644 --- a/src/audible_cli/cmds/cmd_download.py +++ b/src/audible_cli/cmds/cmd_download.py @@ -895,7 +895,7 @@ async def cli(session, api_client, **params): a = i[0].asin t = i[0].full_title l = ", ".join(a["name"] for a in i[0].authors) - c = questionary.Choice(title=f"{a} # {t} by {l}", value=l) + c = questionary.Choice(title=f"{a} # {t} by {l}", value=a) choices.append(c) answer = await questionary.checkbox( @@ -923,7 +923,7 @@ async def cli(session, api_client, **params): a = i[0].asin t = i[0].full_title l = ", ".join(s["title"] for s in i[0].series) - c = questionary.Choice(title=f"{a} # {t} in {l}", value=l) + c = questionary.Choice(title=f"{a} # {t} in {l}", value=a) choices.append(c) answer = await questionary.checkbox( From 2b12c289143c85ec8c38a8ce47ee7bd5966891ab Mon Sep 17 00:00:00 2001 From: mkb79 Date: Thu, 22 Feb 2024 15:46:43 +0100 Subject: [PATCH 5/5] refactor: rework find substrings --- src/audible_cli/models.py | 33 ++++++++++++++------------------- src/audible_cli/utils.py | 7 +++++++ 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/audible_cli/models.py b/src/audible_cli/models.py index 75a800e..4108a14 100644 --- a/src/audible_cli/models.py +++ b/src/audible_cli/models.py @@ -20,7 +20,7 @@ NotDownloadableAsAAX, ItemNotPublished ) -from .utils import full_response_callback, LongestSubString +from .utils import full_response_callback, substring_in_list_accuracy logger = logging.getLogger("audible_cli.models") @@ -103,32 +103,25 @@ def create_base_filename(self, mode: str): return base_filename def substring_in_title_accuracy(self, substring): - match = LongestSubString(substring, self.full_title) - return round(match.percentage, 2) + return substring_in_list_accuracy(substring, [self.full_title]) def substring_in_title(self, substring, p=100): accuracy = self.substring_in_title_accuracy(substring) return accuracy >= p def substring_in_authors_accuracy(self, substring): - max_accuracy = 0 - authors = self.authors or [] - for author in authors: - name = author["name"] - match = LongestSubString(substring, name) - max_accuracy = max(max_accuracy, match.percentage) - - return round(max_accuracy, 2) + if not self.authors: + return 0 + + authors = [author["name"] for author in self.authors] + return substring_in_list_accuracy(substring, authors) def substring_in_series_accuracy(self, substring): - max_accuracy = 0 - seriesList = self.series or [] - for series in seriesList: - name = series["title"] - match = LongestSubString(substring, name) - max_accuracy = max(max_accuracy, match.percentage) - - return round(max_accuracy, 2) + if not self.series: + return 0 + + series = [serie["title"] for serie in self.series] + return substring_in_list_accuracy(substring, series) def get_cover_url(self, res: Union[str, int] = 500): images = self.product_images @@ -480,6 +473,7 @@ def search_item_by_title(self, search_title, p=80): match.append([i, accuracy]) if accuracy >= p else "" return match + def search_item_by_author(self, search_author, p=80): match = [] for i in self._data: @@ -487,6 +481,7 @@ def search_item_by_author(self, search_author, p=80): match.append([i, accuracy]) if accuracy >= p else "" return match + def search_item_by_series(self, search_series, p=80): match = [] for i in self._data: diff --git a/src/audible_cli/utils.py b/src/audible_cli/utils.py index bc7f6e8..b265430 100644 --- a/src/audible_cli/utils.py +++ b/src/audible_cli/utils.py @@ -146,6 +146,13 @@ def longest_match(self): def percentage(self): return self._match.size / len(self._search_for) * 100 + def rounded_percentage(self, digits: int = 2): + return round(self.percentage, digits) + + +def substring_in_list_accuracy(s: str, l: List[str], d: int = 2) -> float: + return max(map(lambda x: LongestSubString(s, x).rounded_percentage(d), l)) + def asin_in_library(asin, library): items = library.get("items") or library