tbr-deal-finder 0.1.5__py3-none-any.whl → 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tbr_deal_finder/book.py CHANGED
@@ -5,6 +5,7 @@ from enum import Enum
5
5
  from typing import Optional, Union
6
6
 
7
7
  import click
8
+ from Levenshtein import ratio
8
9
  from unidecode import unidecode
9
10
 
10
11
  from tbr_deal_finder.config import Config
@@ -26,7 +27,11 @@ class Book:
26
27
  current_price: float
27
28
  timepoint: datetime
28
29
  format: Union[BookFormat, str]
30
+
31
+ # Metadata really only used for tracked books.
32
+ # See get_tbr_books for more context
29
33
  audiobook_isbn: str = None
34
+ audiobook_list_price: float = 0
30
35
 
31
36
  deleted: bool = False
32
37
 
@@ -35,16 +40,20 @@ class Book:
35
40
  normalized_authors: list[str] = None
36
41
 
37
42
  def __post_init__(self):
43
+ self.current_price = round(self.current_price, 2)
44
+ self.list_price = round(self.list_price, 2)
45
+ self.normalized_authors = get_normalized_authors(self.authors)
46
+
47
+ # Strip the title down to its most basic repr
48
+ # Improves hit rate on retailers
49
+ self.title = self.title.split(":")[0].split("(")[0].strip()
50
+
38
51
  if not self.deal_id:
39
- self.deal_id = f"{self.title}__{self.normalized_authors}__{self.retailer}__{self.format}"
52
+ self.deal_id = f"{self.title}__{self.normalized_authors}__{self.format}__{self.retailer}"
40
53
 
41
54
  if isinstance(self.format, str):
42
55
  self.format = BookFormat(self.format)
43
56
 
44
- self.current_price = round(self.current_price, 2)
45
- self.list_price = round(self.list_price, 2)
46
- self.normalized_authors = get_normalized_authors(self.authors)
47
-
48
57
  def discount(self) -> int:
49
58
  return int((self.list_price/self.current_price - 1) * 100)
50
59
 
@@ -56,6 +65,10 @@ class Book:
56
65
  def title_id(self) -> str:
57
66
  return f"{self.title}__{self.normalized_authors}__{self.format}"
58
67
 
68
+ @property
69
+ def full_title_str(self) -> str:
70
+ return f"{self.title}__{self.normalized_authors}"
71
+
59
72
  def list_price_string(self):
60
73
  return self.price_to_string(self.list_price)
61
74
 
@@ -74,6 +87,7 @@ class Book:
74
87
  response = dataclasses.asdict(self)
75
88
  response["format"] = self.format.value
76
89
  del response["audiobook_isbn"]
90
+ del response["audiobook_list_price"]
77
91
  del response["exists"]
78
92
  del response["normalized_authors"]
79
93
 
@@ -109,8 +123,33 @@ def print_books(books: list[Book]):
109
123
  click.echo(str(book))
110
124
 
111
125
 
126
+ def get_full_title_str(title: str, authors: Union[list, str]) -> str:
127
+ return f"{title}__{get_normalized_authors(authors)}"
128
+
129
+
130
+ def get_title_id(title: str, authors: Union[list, str], book_format: BookFormat) -> str:
131
+ return f"{title}__{get_normalized_authors(authors)}__{book_format.value}"
132
+
133
+
112
134
  def get_normalized_authors(authors: Union[str, list[str]]) -> list[str]:
113
135
  if isinstance(authors, str):
114
136
  authors = [i for i in authors.split(",")]
115
137
 
116
138
  return sorted([_AUTHOR_RE.sub('', unidecode(author)).lower() for author in authors])
139
+
140
+
141
+ def is_matching_authors(a1: list[str], a2: list[str]) -> bool:
142
+ """Checks if two normalized authors are matching.
143
+ Matching here means that they are at least 80% similar using levenshtein distance.
144
+
145
+ Score is calculated as follows:
146
+ 1 - (distance / (len1 + len2))
147
+
148
+ :param a1:
149
+ :param a2:
150
+ :return:
151
+ """
152
+ return any(
153
+ any(ratio(author1, author2, score_cutoff=.8) for author2 in a2)
154
+ for author1 in a1
155
+ )
tbr_deal_finder/cli.py CHANGED
@@ -9,7 +9,7 @@ import click
9
9
  import questionary
10
10
 
11
11
  from tbr_deal_finder.config import Config
12
- from tbr_deal_finder.library_exports import maybe_set_library_export_audiobook_isbn
12
+ from tbr_deal_finder.library_exports import maybe_enrich_library_exports
13
13
  from tbr_deal_finder.migrations import make_migrations
14
14
  from tbr_deal_finder.book import get_deals_found_at, print_books, get_active_deals
15
15
  from tbr_deal_finder.retailer import RETAILER_MAP
@@ -28,12 +28,6 @@ from tbr_deal_finder.utils import (
28
28
  def cli():
29
29
  make_migrations()
30
30
 
31
- # Check that the config exists for all commands ran
32
- try:
33
- Config.load()
34
- except FileNotFoundError:
35
- _set_config()
36
-
37
31
 
38
32
  def _add_path(existing_paths: list[str]) -> Union[str, None]:
39
33
  try:
@@ -68,24 +62,26 @@ def _set_library_export_paths(config: Config):
68
62
  Ensures that only valid, unique paths are added. Updates the config in-place.
69
63
  """
70
64
  while True:
71
- if config.library_export_paths:
72
- if len(config.library_export_paths) > 1:
73
- choices = ["Add new path", "Remove path", "Done"]
74
- else:
75
- choices = ["Add new path", "Done"]
76
-
77
- try:
78
- user_selection = questionary.select(
79
- "What change would you like to make to your library export paths",
80
- choices=choices,
81
- ).ask()
82
- except (KeyError, KeyboardInterrupt, TypeError):
83
- return
65
+ if len(config.library_export_paths) > 0:
66
+ choices = ["Add new path", "Remove path", "Done"]
84
67
  else:
85
- click.echo("Add your library export path.")
86
- user_selection = "Add new path"
68
+ choices = ["Add new path", "Done"]
69
+
70
+ try:
71
+ user_selection = questionary.select(
72
+ "What change would you like to make to your library export paths",
73
+ choices=choices,
74
+ ).ask()
75
+ except (KeyError, KeyboardInterrupt, TypeError):
76
+ return
87
77
 
88
78
  if user_selection == "Done":
79
+ if not config.library_export_paths:
80
+ if not click.confirm(
81
+ "Don't add a GoodReads or StoryGraph export and use wishlist entirely? "
82
+ "Note: Wishlist checks will still work even if you add your StoryGraph/GoodReads export."
83
+ ):
84
+ continue
89
85
  return
90
86
  elif user_selection == "Add new path":
91
87
  if new_path := _add_path(config.library_export_paths):
@@ -124,15 +120,20 @@ def _set_locale(config: Config):
124
120
 
125
121
 
126
122
  def _set_tracked_retailers(config: Config):
123
+ if not config.tracked_retailers:
124
+ echo_info(
125
+ "If you haven't heard of it, Chirp doesn't charge a subscription and has some great deals. \n"
126
+ "Note: I don't work for Chirp and this isn't a paid plug."
127
+ )
128
+
127
129
  while True:
128
130
  user_response = questionary.checkbox(
129
- "Select the retailers you want to check deals for.\n"
130
- "Tip: Chirp doesn't have a subscription and can have good deals. I'd recommend checking it.\n",
131
+ "Select the retailers you want to check deals for.\n",
131
132
  choices=[
132
133
  questionary.Choice(retailer, checked=retailer in config.tracked_retailers)
133
134
  for retailer in RETAILER_MAP.keys()
134
135
  ]).ask()
135
- if len(user_response) > 1:
136
+ if len(user_response) > 0:
136
137
  break
137
138
  else:
138
139
  echo_err("You must track deals for at least one retailer.")
@@ -183,9 +184,12 @@ def setup():
183
184
  @cli.command()
184
185
  def latest_deals():
185
186
  """Find book deals from your Library export."""
186
- config = Config.load()
187
+ try:
188
+ config = Config.load()
189
+ except FileNotFoundError:
190
+ config = _set_config()
187
191
 
188
- asyncio.run(maybe_set_library_export_audiobook_isbn(config))
192
+ asyncio.run(maybe_enrich_library_exports(config))
189
193
 
190
194
  db_conn = get_duckdb_conn()
191
195
  results = execute_query(
tbr_deal_finder/config.py CHANGED
@@ -62,10 +62,16 @@ class Config:
62
62
  tracked_retailers_str = parser.get('DEFAULT', 'tracked_retailers')
63
63
  locale = parser.get('DEFAULT', 'locale', fallback="us")
64
64
  cls.set_locale(locale)
65
+
66
+ if export_paths_str:
67
+ library_export_paths = [i.strip() for i in export_paths_str.split(",")]
68
+ else:
69
+ library_export_paths = []
70
+
65
71
  return cls(
66
72
  max_price=parser.getfloat('DEFAULT', 'max_price', fallback=8.0),
67
73
  min_discount=parser.getint('DEFAULT', 'min_discount', fallback=35),
68
- library_export_paths=[i.strip() for i in export_paths_str.split(",")],
74
+ library_export_paths=library_export_paths,
69
75
  tracked_retailers=[i.strip() for i in tracked_retailers_str.split(",")]
70
76
  )
71
77
 
@@ -78,7 +84,9 @@ class Config:
78
84
  return ", ".join(self.tracked_retailers)
79
85
 
80
86
  def set_library_export_paths(self, library_export_paths: Union[str, list[str]]):
81
- if isinstance(library_export_paths, str):
87
+ if not library_export_paths:
88
+ self.library_export_paths = []
89
+ elif isinstance(library_export_paths, str):
82
90
  self.library_export_paths = [i.strip() for i in library_export_paths.split(",")]
83
91
  else:
84
92
  self.library_export_paths = library_export_paths
@@ -2,15 +2,17 @@ import asyncio
2
2
  import csv
3
3
  import shutil
4
4
  import tempfile
5
+ from datetime import datetime
6
+ from typing import Callable, Awaitable, Optional
5
7
 
6
8
  from tqdm.asyncio import tqdm_asyncio
7
9
 
8
- from tbr_deal_finder.book import Book, BookFormat
10
+ from tbr_deal_finder.book import Book, BookFormat, get_full_title_str
9
11
  from tbr_deal_finder.config import Config
10
- from tbr_deal_finder.retailer.librofm import LibroFM
12
+ from tbr_deal_finder.retailer import LibroFM, Chirp
11
13
 
12
14
 
13
- def _get_book_authors(book: dict) -> str:
15
+ def get_book_authors(book: dict) -> str:
14
16
  if authors := book.get('Authors'):
15
17
  return authors
16
18
 
@@ -21,12 +23,12 @@ def _get_book_authors(book: dict) -> str:
21
23
  return authors
22
24
 
23
25
 
24
- def _get_book_title(book: dict) -> str:
26
+ def get_book_title(book: dict) -> str:
25
27
  title = book['Title']
26
28
  return title.split("(")[0].strip()
27
29
 
28
30
 
29
- def _is_tbr_book(book: dict) -> bool:
31
+ def is_tbr_book(book: dict) -> bool:
30
32
  if "Read Status" in book:
31
33
  return book["Read Status"] == "to-read"
32
34
  elif "Bookshelves" in book:
@@ -35,65 +37,59 @@ def _is_tbr_book(book: dict) -> bool:
35
37
  return True
36
38
 
37
39
 
38
- def get_tbr_books(config: Config) -> list[Book]:
39
- tbr_book_map: dict[str: Book] = {}
40
- for library_export_path in config.library_export_paths:
41
-
42
- with open(library_export_path, 'r', newline='', encoding='utf-8') as file:
43
- # Use csv.DictReader to get dictionaries with column headers
44
- for book_dict in csv.DictReader(file):
45
- if not _is_tbr_book(book_dict):
46
- continue
47
-
48
- title = _get_book_title(book_dict)
49
- authors = _get_book_authors(book_dict)
50
- key = f'{title}__{authors}'
51
-
52
- if key in tbr_book_map:
53
- continue
54
-
55
- tbr_book_map[key] = Book(
56
- retailer="N/A",
57
- title=title,
58
- authors=authors,
59
- list_price=0,
60
- current_price=0,
61
- timepoint=config.run_time,
62
- format=BookFormat.NA,
63
- audiobook_isbn=book_dict["audiobook_isbn"],
64
- )
65
- return list(tbr_book_map.values())
66
-
40
+ def requires_audiobook_list_price_default(config: Config) -> bool:
41
+ return bool(
42
+ "Libro.FM" in config.tracked_retailers
43
+ and "Audible" not in config.tracked_retailers
44
+ and "Chirp" not in config.tracked_retailers
45
+ )
67
46
 
68
- async def maybe_set_library_export_audiobook_isbn(config: Config):
69
- """To get the price from Libro.fm for a book you need its ISBN
70
47
 
71
- As opposed to trying to get that every time latest-deals is run
72
- we're just updating the export csv once to include the ISBN.
48
+ async def _maybe_set_column_for_library_exports(
49
+ config: Config,
50
+ attr_name: str,
51
+ get_book_callable: Callable[[Book, datetime, asyncio.Semaphore], Awaitable[Book]],
52
+ column_name: Optional[str] = None,
53
+ ):
54
+ """Adds a new column to all library exports that are missing it.
55
+ Uses get_book_callable to set the column value if a matching record couldn't be found
56
+ on that column in any other library export file.
57
+
58
+ :param config:
59
+ :param attr_name:
60
+ :param get_book_callable:
61
+ :param column_name:
62
+ :return:
73
63
  """
74
-
75
- if "Libro.FM" not in config.tracked_retailers:
64
+ if not config.library_export_paths:
76
65
  return
77
66
 
78
- books_requiring_check_map = dict()
79
- book_to_isbn_map = dict()
67
+ if not column_name:
68
+ column_name = attr_name
80
69
 
70
+ books_requiring_check_map = dict()
71
+ book_to_col_val_map = dict()
81
72
 
73
+ # Iterate all library export paths
82
74
  for library_export_path in config.library_export_paths:
83
75
  with open(library_export_path, 'r', newline='', encoding='utf-8') as file:
84
76
  # Use csv.DictReader to get dictionaries with column headers
85
77
  for book_dict in csv.DictReader(file):
86
- if not _is_tbr_book(book_dict):
78
+ if not is_tbr_book(book_dict):
87
79
  continue
88
80
 
89
- title = _get_book_title(book_dict)
90
- authors = _get_book_authors(book_dict)
91
- key = f'{title}__{authors}'
81
+ title = get_book_title(book_dict)
82
+ authors = get_book_authors(book_dict)
83
+ key = get_full_title_str(title, authors)
92
84
 
93
- if "audiobook_isbn" in book_dict:
94
- book_to_isbn_map[key] = book_dict["audiobook_isbn"]
85
+ if column_name in book_dict:
86
+ # Keep state of value for this book/key
87
+ # in the event another export has the same book but the value is not set
88
+ book_to_col_val_map[key] = book_dict[column_name]
89
+ # Value has been found so a check no longer needs to be performed
95
90
  books_requiring_check_map.pop(key, None)
96
- elif key not in book_to_isbn_map:
91
+ elif key not in book_to_col_val_map:
92
+ # Not found, add the book to those requiring the column val to be set
97
93
  books_requiring_check_map[key] = Book(
98
94
  retailer="N/A",
99
95
  title=title,
@@ -105,27 +101,33 @@ async def maybe_set_library_export_audiobook_isbn(config: Config):
105
101
  )
106
102
 
107
103
  if not books_requiring_check_map:
104
+ # Everything was resolved, nothing else to do
108
105
  return
109
106
 
110
- libro_fm = LibroFM()
111
- # Setting it lower to be a good user of libro on their more expensive search call
112
- semaphore = asyncio.Semaphore(3)
107
+ semaphore = asyncio.Semaphore(5)
108
+ human_readable_name = attr_name.replace("_", " ").title()
113
109
 
114
- # Set the audiobook isbn for Book instances in books_requiring_check_map
115
- await tqdm_asyncio.gather(
110
+ # Get books with the appropriate transform applied
111
+ # Responsibility is on the callable here
112
+ enriched_books = await tqdm_asyncio.gather(
116
113
  *[
117
- libro_fm.get_book_isbn(book, semaphore) for book in books_requiring_check_map.values()
114
+ get_book_callable(book, config.run_time, semaphore) for book in books_requiring_check_map.values()
118
115
  ],
119
- desc="Getting required audiobook ISBN info"
116
+ desc=f"Getting required {human_readable_name} info"
120
117
  )
118
+ updated_book_map = {
119
+ b.full_title_str: b
120
+ for b in enriched_books
121
+ }
122
+
121
123
 
122
- # Go back and now add the audiobook_isbn
124
+ # Go back and now add the new column where it hasn't been set
123
125
  for library_export_path in config.library_export_paths:
124
126
  with open(library_export_path, 'r', newline='', encoding='utf-8') as file:
125
127
  reader = csv.DictReader(file)
126
- field_names = list(reader.fieldnames) + ["audiobook_isbn"]
128
+ field_names = list(reader.fieldnames) + [column_name]
127
129
  file_content = [book_dict for book_dict in reader]
128
- if not file_content or "audiobook_isbn" in file_content[0]:
130
+ if not file_content or column_name in file_content[0]:
129
131
  continue
130
132
 
131
133
  with tempfile.NamedTemporaryFile(mode='w', delete=False, newline='') as temp_file:
@@ -134,22 +136,73 @@ async def maybe_set_library_export_audiobook_isbn(config: Config):
134
136
  writer.writeheader()
135
137
 
136
138
  for book_dict in file_content:
137
- if _is_tbr_book(book_dict):
138
- title = _get_book_title(book_dict)
139
- authors = _get_book_authors(book_dict)
140
- key = f'{title}__{authors}'
141
-
142
- if key in book_to_isbn_map:
143
- audiobook_isbn = book_to_isbn_map[key]
139
+ if is_tbr_book(book_dict):
140
+ title = get_book_title(book_dict)
141
+ authors = get_book_authors(book_dict)
142
+ key = get_full_title_str(title, authors)
143
+
144
+ if key in book_to_col_val_map:
145
+ col_val = book_to_col_val_map[key]
146
+ elif key in updated_book_map:
147
+ book = updated_book_map[key]
148
+ col_val = getattr(book, attr_name)
144
149
  else:
145
- book = books_requiring_check_map[key]
146
- audiobook_isbn = book.audiobook_isbn
150
+ col_val = ""
147
151
 
148
- book_dict["audiobook_isbn"] = audiobook_isbn
152
+ book_dict[column_name] = col_val
149
153
  else:
150
- book_dict["audiobook_isbn"] = ""
154
+ book_dict[column_name] = ""
151
155
 
152
156
  writer.writerow(book_dict)
153
157
 
154
158
  shutil.move(temp_filename, library_export_path)
155
159
 
160
+
161
+ async def _maybe_set_library_export_audiobook_isbn(config: Config):
162
+ """To get the price from Libro.fm for a book, you need its ISBN
163
+
164
+ As opposed to trying to get that every time latest-deals is run
165
+ we're just updating the export csv once to include the ISBN.
166
+
167
+ Unfortunately, we do have to get it at run time for wishlists.
168
+ """
169
+ if "Libro.FM" not in config.tracked_retailers:
170
+ return
171
+
172
+ libro_fm = LibroFM()
173
+ await libro_fm.set_auth()
174
+
175
+ await _maybe_set_column_for_library_exports(
176
+ config,
177
+ "audiobook_isbn",
178
+ libro_fm.get_book_isbn,
179
+ )
180
+
181
+
182
+ async def _maybe_set_library_export_audiobook_list_price(config: Config):
183
+ """Set a default list price for audiobooks
184
+
185
+ Only set if not currently set and the only audiobook retailer is Libro.FM
186
+ Libro.FM doesn't include the actual default price in its response, so this grabs the price reported by Chirp.
187
+ Chirp doesn't require a login to get this price info making it ideal in this instance.
188
+
189
+ :param config:
190
+ :return:
191
+ """
192
+ if not requires_audiobook_list_price_default(config):
193
+ return
194
+
195
+ chirp = Chirp()
196
+ await chirp.set_auth()
197
+
198
+ await _maybe_set_column_for_library_exports(
199
+ config,
200
+ "list_price",
201
+ chirp.get_book,
202
+ "audiobook_list_price"
203
+ )
204
+
205
+
206
+ async def maybe_enrich_library_exports(config: Config):
207
+ await _maybe_set_library_export_audiobook_isbn(config)
208
+ await _maybe_set_library_export_audiobook_list_price(config)
@@ -0,0 +1,18 @@
1
+ from tbr_deal_finder.book import Book
2
+ from tbr_deal_finder.config import Config
3
+ from tbr_deal_finder.retailer import RETAILER_MAP
4
+ from tbr_deal_finder.retailer.models import Retailer
5
+
6
+
7
+ async def get_owned_books(config: Config) -> list[Book]:
8
+ owned_books = []
9
+
10
+ for retailer_str in config.tracked_retailers:
11
+ retailer: Retailer = RETAILER_MAP[retailer_str]()
12
+ await retailer.set_auth()
13
+
14
+ owned_books.extend(
15
+ await retailer.get_library(config)
16
+ )
17
+
18
+ return owned_books
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import math
2
3
  import os.path
3
4
  from datetime import datetime
4
5
  from textwrap import dedent
@@ -22,11 +23,9 @@ def login_url_callback(url: str) -> str:
22
23
 
23
24
  try:
24
25
  from playwright.sync_api import sync_playwright # type: ignore
25
- use_playwright = True
26
26
  except ImportError:
27
- use_playwright = False
28
-
29
- if use_playwright:
27
+ pass
28
+ else:
30
29
  with sync_playwright() as p:
31
30
  iphone = p.devices["iPhone 12 Pro"]
32
31
  browser = p.webkit.launch(headless=False)
@@ -75,6 +74,23 @@ class Audible(Retailer):
75
74
  def name(self) -> str:
76
75
  return "Audible"
77
76
 
77
+ @property
78
+ def format(self) -> BookFormat:
79
+ return BookFormat.AUDIOBOOK
80
+
81
+ async def set_auth(self):
82
+ if not os.path.exists(_AUTH_PATH):
83
+ auth = audible.Authenticator.from_login_external(
84
+ locale=Config.locale,
85
+ login_url_callback=login_url_callback
86
+ )
87
+
88
+ # Save credentials to file
89
+ auth.to_file(_AUTH_PATH)
90
+
91
+ self._auth = audible.Authenticator.from_file(_AUTH_PATH)
92
+ self._client = audible.AsyncClient(auth=self._auth)
93
+
78
94
  async def get_book(
79
95
  self,
80
96
  target: Book,
@@ -95,19 +111,7 @@ class Audible(Retailer):
95
111
  ]
96
112
  )
97
113
 
98
- if not match["products"]:
99
- return Book(
100
- retailer=self.name,
101
- title=title,
102
- authors=authors,
103
- list_price=0,
104
- current_price=0,
105
- timepoint=runtime,
106
- format=BookFormat.AUDIOBOOK,
107
- exists=False,
108
- )
109
-
110
- for product in match["products"]:
114
+ for product in match.get("products", []):
111
115
  if product["title"] != title:
112
116
  continue
113
117
 
@@ -132,15 +136,74 @@ class Audible(Retailer):
132
136
  exists=False,
133
137
  )
134
138
 
135
- async def set_auth(self):
136
- if not os.path.exists(_AUTH_PATH):
137
- auth = audible.Authenticator.from_login_external(
138
- locale=Config.locale,
139
- login_url_callback=login_url_callback
139
+ async def get_wishlist(self, config: Config) -> list[Book]:
140
+ wishlist_books = []
141
+
142
+ page = 0
143
+ total_pages = 1
144
+ page_size = 50
145
+ while page < total_pages:
146
+ response = await self._client.get(
147
+ "1.0/wishlist",
148
+ num_results=page_size,
149
+ page=page,
150
+ response_groups=[
151
+ "contributors, product_attrs, product_desc, product_extended_attrs"
152
+ ]
140
153
  )
141
154
 
142
- # Save credentials to file
143
- auth.to_file(_AUTH_PATH)
155
+ for audiobook in response.get("products", []):
156
+ authors = [author["name"] for author in audiobook["authors"]]
157
+ wishlist_books.append(
158
+ Book(
159
+ retailer=self.name,
160
+ title=audiobook["title"],
161
+ authors=", ".join(authors),
162
+ list_price=1,
163
+ current_price=1,
164
+ timepoint=config.run_time,
165
+ format=self.format,
166
+ audiobook_isbn=audiobook["isbn"],
167
+ )
168
+ )
144
169
 
145
- self._auth = audible.Authenticator.from_file(_AUTH_PATH)
146
- self._client = audible.AsyncClient(auth=self._auth)
170
+ page += 1
171
+ total_pages = math.ceil(int(response.get("total_results", 1))/page_size)
172
+
173
+ return wishlist_books
174
+
175
+ async def get_library(self, config: Config) -> list[Book]:
176
+ library_books = []
177
+
178
+ page = 1
179
+ total_pages = 1
180
+ page_size = 1000
181
+ while page <= total_pages:
182
+ response = await self._client.get(
183
+ "1.0/library",
184
+ num_results=page_size,
185
+ page=page,
186
+ response_groups=[
187
+ "contributors, product_attrs, product_desc, product_extended_attrs"
188
+ ]
189
+ )
190
+
191
+ for audiobook in response.get("items", []):
192
+ authors = [author["name"] for author in audiobook["authors"]]
193
+ library_books.append(
194
+ Book(
195
+ retailer=self.name,
196
+ title=audiobook["title"],
197
+ authors=", ".join(authors),
198
+ list_price=1,
199
+ current_price=1,
200
+ timepoint=config.run_time,
201
+ format=self.format,
202
+ audiobook_isbn=audiobook["isbn"],
203
+ )
204
+ )
205
+
206
+ page += 1
207
+ total_pages = math.ceil(int(response.get("total_results", 1))/page_size)
208
+
209
+ return library_books