diff --git a/src/audible_cli/cmds/cmd_download.py b/src/audible_cli/cmds/cmd_download.py index 3b7a2b1..0104cbc 100644 --- a/src/audible_cli/cmds/cmd_download.py +++ b/src/audible_cli/cmds/cmd_download.py @@ -644,7 +644,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", @@ -757,6 +767,8 @@ async def cli(session, api_client, **params): get_all = params.get("all") is True asins = params.get("asin") titles = params.get("title") + authors = params.get("author") + series = params.get("series") if get_all and (asins or titles): raise click.BadOptionUsage( "--all", @@ -836,7 +848,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, @@ -895,6 +907,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=a) + 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=a) + 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 b8dc839..cc66d6b 100644 --- a/src/audible_cli/models.py +++ b/src/audible_cli/models.py @@ -21,7 +21,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") @@ -104,13 +104,26 @@ 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): + 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): + 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 res = str(res) @@ -468,6 +481,22 @@ def search_item_by_title(self, search_title, p=80): 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): def _prepare_data(self, data: Union[dict, list]) -> list: 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