tbr-deal-finder 0.1.7__tar.gz → 0.2.0__tar.gz

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.
Files changed (32) hide show
  1. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/CHANGELOG.md +29 -0
  2. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/PKG-INFO +5 -2
  3. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/README.md +4 -1
  4. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/pyproject.toml +1 -1
  5. tbr_deal_finder-0.2.0/tbr_deal_finder/book.py +206 -0
  6. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/cli.py +5 -3
  7. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/config.py +10 -0
  8. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/migrations.py +16 -0
  9. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/queries/get_active_deals.sql +1 -1
  10. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/queries/get_deals_found_at.sql +1 -1
  11. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/retailer/__init__.py +2 -0
  12. tbr_deal_finder-0.2.0/tbr_deal_finder/retailer/amazon.py +85 -0
  13. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/retailer/audible.py +13 -100
  14. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/retailer/chirp.py +34 -55
  15. tbr_deal_finder-0.2.0/tbr_deal_finder/retailer/kindle.py +141 -0
  16. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/retailer/librofm.py +27 -53
  17. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/retailer/models.py +37 -2
  18. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/retailer_deal.py +9 -19
  19. tbr_deal_finder-0.2.0/tbr_deal_finder/tracked_books.py +339 -0
  20. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/uv.lock +1 -1
  21. tbr_deal_finder-0.1.7/tbr_deal_finder/book.py +0 -155
  22. tbr_deal_finder-0.1.7/tbr_deal_finder/library_exports.py +0 -208
  23. tbr_deal_finder-0.1.7/tbr_deal_finder/tracked_books.py +0 -120
  24. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/.github/workflows/publish-to-pypi.yaml +0 -0
  25. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/.gitignore +0 -0
  26. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/.python-version +0 -0
  27. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/DESIGN.md +0 -0
  28. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/LICENSE +0 -0
  29. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/__init__.py +0 -0
  30. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/owned_books.py +0 -0
  31. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/queries/latest_deal_last_ran_most_recent_success.sql +0 -0
  32. {tbr_deal_finder-0.1.7 → tbr_deal_finder-0.2.0}/tbr_deal_finder/utils.py +0 -0
@@ -1,6 +1,35 @@
1
1
 
2
2
  # Change Log
3
3
 
4
+ ---
5
+
6
+ ## 0.2.0 (August 15, 2025)
7
+
8
+ Notes:
9
+ * Added foundational Kindle support
10
+ * Library support is undecided right now
11
+ * Unable to find the endpoint
12
+ * Wishlist support is undecided right now
13
+ * Unable to find the endpoint
14
+ * Improvements to title matching for Audible & Chirp
15
+ * Improved request performance for Chirp & Libro
16
+
17
+ BUG FIXES:
18
+ * Fixed breaking import on Windows systems
19
+
20
+ ---
21
+
22
+ ## 0.1.8 (August 13, 2025)
23
+
24
+ Notes:
25
+ * Improved performance for tracking on libro
26
+ * Preparing EBook support
27
+
28
+ BUG FIXES:
29
+ * Fixed initial login issue in libro.fm
30
+
31
+ ---
32
+
4
33
  ## 0.1.7 (July 31, 2025)
5
34
 
6
35
  Notes:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tbr-deal-finder
3
- Version: 0.1.7
3
+ Version: 0.2.0
4
4
  Summary: Track price drops and find deals on books in your TBR list across audiobook and ebook formats.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -37,6 +37,9 @@ Track price drops and find deals on books in your TBR (To Be Read) and wishlist
37
37
  * Chirp
38
38
  * Libro.fm
39
39
 
40
+ ### EBooks
41
+ * Kindle
42
+
40
43
  ### Locales
41
44
  * US
42
45
  * CA
@@ -56,7 +59,7 @@ Track price drops and find deals on books in your TBR (To Be Read) and wishlist
56
59
  1. If it's not already on your computer, download Python https://www.python.org/downloads/
57
60
  1. tbr-deal-finder requires Python3.13 or higher
58
61
  2. Optional: Install and use virtualenv
59
- 3. Open your Terminal/Commmand Prompt
62
+ 3. Open your Terminal/Command Prompt
60
63
  4. Run `pip3.13 install tbr-deal-finder`
61
64
 
62
65
  ### UV
@@ -19,6 +19,9 @@ Track price drops and find deals on books in your TBR (To Be Read) and wishlist
19
19
  * Chirp
20
20
  * Libro.fm
21
21
 
22
+ ### EBooks
23
+ * Kindle
24
+
22
25
  ### Locales
23
26
  * US
24
27
  * CA
@@ -38,7 +41,7 @@ Track price drops and find deals on books in your TBR (To Be Read) and wishlist
38
41
  1. If it's not already on your computer, download Python https://www.python.org/downloads/
39
42
  1. tbr-deal-finder requires Python3.13 or higher
40
43
  2. Optional: Install and use virtualenv
41
- 3. Open your Terminal/Commmand Prompt
44
+ 3. Open your Terminal/Command Prompt
42
45
  4. Run `pip3.13 install tbr-deal-finder`
43
46
 
44
47
  ### UV
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tbr-deal-finder"
3
- version = "0.1.7"
3
+ version = "0.2.0"
4
4
  description = "Track price drops and find deals on books in your TBR list across audiobook and ebook formats."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -0,0 +1,206 @@
1
+ import re
2
+ from datetime import datetime
3
+ from enum import Enum
4
+ from typing import Union
5
+
6
+ import click
7
+ from Levenshtein import ratio
8
+ from unidecode import unidecode
9
+
10
+ from tbr_deal_finder.config import Config
11
+ from tbr_deal_finder.utils import get_duckdb_conn, execute_query, get_query_by_name, echo_info
12
+
13
+ _AUTHOR_RE = re.compile(r'[^a-zA-Z0-9]')
14
+
15
+ class BookFormat(Enum):
16
+ AUDIOBOOK = "Audiobook"
17
+ EBOOK = "E-Book"
18
+ NA = "N/A" # When the format doesn't matter
19
+
20
+
21
+ class Book:
22
+
23
+ def __init__(
24
+ self,
25
+ retailer: str,
26
+ title: str,
27
+ authors: str,
28
+ timepoint: datetime,
29
+ format: Union[BookFormat, str],
30
+ list_price: float = 0,
31
+ current_price: float = 0,
32
+ ebook_asin: str = None,
33
+ audiobook_isbn: str = None,
34
+ audiobook_list_price: float = 0,
35
+ deleted: bool = False,
36
+ exists: bool = True,
37
+ ):
38
+ self.retailer = retailer
39
+ self.title = get_normalized_title(title)
40
+ self.authors = authors
41
+ self.timepoint = timepoint
42
+
43
+ self.ebook_asin = ebook_asin
44
+ self.audiobook_isbn = audiobook_isbn
45
+ self.audiobook_list_price = audiobook_list_price
46
+ self.deleted = deleted
47
+ self.exists = exists
48
+
49
+ self.list_price = list_price
50
+ self.current_price = current_price
51
+ self.normalized_authors = get_normalized_authors(authors)
52
+
53
+ if isinstance(format, str):
54
+ format = BookFormat(format)
55
+ self.format = format
56
+
57
+ def discount(self) -> int:
58
+ return int((self.list_price/self.current_price - 1) * 100)
59
+
60
+ @staticmethod
61
+ def price_to_string(price: float) -> str:
62
+ return f"{Config.currency_symbol()}{price:.2f}"
63
+
64
+ @property
65
+ def deal_id(self) -> str:
66
+ return f"{self.title}__{self.normalized_authors}__{self.format}__{self.retailer}"
67
+
68
+ @property
69
+ def title_id(self) -> str:
70
+ return f"{self.title}__{self.normalized_authors}__{self.format}"
71
+
72
+ @property
73
+ def full_title_str(self) -> str:
74
+ return f"{self.title}__{self.normalized_authors}"
75
+
76
+ @property
77
+ def current_price(self) -> float:
78
+ return self._current_price
79
+
80
+ @current_price.setter
81
+ def current_price(self, price: float):
82
+ self._current_price = round(price, 2)
83
+
84
+ @property
85
+ def list_price(self) -> float:
86
+ return self._list_price
87
+
88
+ @list_price.setter
89
+ def list_price(self, price: float):
90
+ self._list_price = round(price, 2)
91
+
92
+ def list_price_string(self):
93
+ return self.price_to_string(self.list_price)
94
+
95
+ def current_price_string(self):
96
+ return self.price_to_string(self.current_price)
97
+
98
+ def __str__(self) -> str:
99
+ price = self.current_price_string()
100
+ book_format = self.format.value
101
+ title = self.title
102
+ if len(self.title) > 75:
103
+ title = f"{title[:75]}..."
104
+ return f"{title} by {self.authors} - {price} - {self.discount()}% Off at {self.retailer}"
105
+
106
+ def dict(self):
107
+ return {
108
+ "retailer": self.retailer,
109
+ "title": self.title,
110
+ "authors": self.authors,
111
+ "list_price": self.list_price,
112
+ "current_price": self.current_price,
113
+ "timepoint": self.timepoint,
114
+ "format": self.format.value,
115
+ "deleted": self.deleted,
116
+ "deal_id": self.deal_id,
117
+ }
118
+
119
+ def tbr_dict(self):
120
+ return {
121
+ "title": self.title,
122
+ "authors": self.authors,
123
+ "format": self.format.value,
124
+ "ebook_asin": self.ebook_asin,
125
+ "audiobook_isbn": self.audiobook_isbn,
126
+ "audiobook_list_price": self.audiobook_list_price,
127
+ "book_id": self.title_id,
128
+ }
129
+
130
+
131
+ def get_deals_found_at(timepoint: datetime) -> list[Book]:
132
+ db_conn = get_duckdb_conn()
133
+ query_response = execute_query(
134
+ db_conn,
135
+ get_query_by_name("get_deals_found_at.sql"),
136
+ {"timepoint": timepoint}
137
+ )
138
+ return [Book(**book) for book in query_response]
139
+
140
+
141
+ def get_active_deals() -> list[Book]:
142
+ db_conn = get_duckdb_conn()
143
+ query_response = execute_query(
144
+ db_conn,
145
+ get_query_by_name("get_active_deals.sql")
146
+ )
147
+ return [Book(**book) for book in query_response]
148
+
149
+
150
+ def print_books(books: list[Book]):
151
+ audiobooks = [book for book in books if book.format == BookFormat.AUDIOBOOK]
152
+ audiobooks = sorted(audiobooks, key=lambda book: book.deal_id)
153
+
154
+ ebooks = [book for book in books if book.format == BookFormat.EBOOK]
155
+ ebooks = sorted(ebooks, key=lambda book: book.deal_id)
156
+
157
+ for books_in_format in [audiobooks, ebooks]:
158
+ if not books_in_format:
159
+ continue
160
+
161
+ init_book = books_in_format[0]
162
+ echo_info(f"\n\n{init_book.format.value} Deals:")
163
+
164
+ prior_title_id = init_book.title_id
165
+ for book in books_in_format:
166
+ if prior_title_id != book.title_id:
167
+ prior_title_id = book.title_id
168
+ click.echo()
169
+
170
+ click.echo(str(book))
171
+
172
+
173
+ def get_full_title_str(title: str, authors: Union[list, str]) -> str:
174
+ return f"{title}__{get_normalized_authors(authors)}"
175
+
176
+
177
+ def get_title_id(title: str, authors: Union[list, str], book_format: BookFormat) -> str:
178
+ return f"{title}__{get_normalized_authors(authors)}__{book_format.value}"
179
+
180
+
181
+ def get_normalized_title(title: str) -> str:
182
+ return title.split(":")[0].split("(")[0].strip()
183
+
184
+
185
+ def get_normalized_authors(authors: Union[str, list[str]]) -> list[str]:
186
+ if isinstance(authors, str):
187
+ authors = [i for i in authors.split(",")]
188
+
189
+ return sorted([_AUTHOR_RE.sub('', unidecode(author)).lower() for author in authors])
190
+
191
+
192
+ def is_matching_authors(a1: list[str], a2: list[str]) -> bool:
193
+ """Checks if two normalized authors are matching.
194
+ Matching here means that they are at least 80% similar using levenshtein distance.
195
+
196
+ Score is calculated as follows:
197
+ 1 - (distance / (len1 + len2))
198
+
199
+ :param a1:
200
+ :param a2:
201
+ :return:
202
+ """
203
+ return any(
204
+ any(ratio(author1, author2, score_cutoff=.8) for author2 in a2)
205
+ for author1 in a1
206
+ )
@@ -9,11 +9,11 @@ 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_enrich_library_exports
13
12
  from tbr_deal_finder.migrations import make_migrations
14
13
  from tbr_deal_finder.book import get_deals_found_at, print_books, get_active_deals
15
14
  from tbr_deal_finder.retailer import RETAILER_MAP
16
15
  from tbr_deal_finder.retailer_deal import get_latest_deals
16
+ from tbr_deal_finder.tracked_books import reprocess_incomplete_tbr_books
17
17
  from tbr_deal_finder.utils import (
18
18
  echo_err,
19
19
  echo_info,
@@ -180,6 +180,10 @@ def _set_config() -> Config:
180
180
  def setup():
181
181
  _set_config()
182
182
 
183
+ # Retailers may have changed causing some books to need reprocessing
184
+ config = Config.load()
185
+ reprocess_incomplete_tbr_books(config)
186
+
183
187
 
184
188
  @cli.command()
185
189
  def latest_deals():
@@ -189,8 +193,6 @@ def latest_deals():
189
193
  except FileNotFoundError:
190
194
  config = _set_config()
191
195
 
192
- asyncio.run(maybe_enrich_library_exports(config))
193
-
194
196
  db_conn = get_duckdb_conn()
195
197
  results = execute_query(
196
198
  db_conn,
@@ -83,6 +83,16 @@ class Config:
83
83
  def tracked_retailers_str(self) -> str:
84
84
  return ", ".join(self.tracked_retailers)
85
85
 
86
+ def is_tracking_format(self, book_format) -> bool:
87
+ from tbr_deal_finder.retailer import RETAILER_MAP
88
+
89
+ for retailer_str in self.tracked_retailers:
90
+ retailer = RETAILER_MAP[retailer_str]()
91
+ if retailer.format == book_format:
92
+ return True
93
+
94
+ return False
95
+
86
96
  def set_library_export_paths(self, library_export_paths: Union[str, list[str]]):
87
97
  if not library_export_paths:
88
98
  self.library_export_paths = []
@@ -46,6 +46,22 @@ _MIGRATIONS = [
46
46
  );
47
47
  """
48
48
  ),
49
+ TableMigration(
50
+ version=1,
51
+ table_name="tbr_book",
52
+ sql="""
53
+ CREATE TABLE tbr_book
54
+ (
55
+ title VARCHAR,
56
+ authors VARCHAR,
57
+ format VARCHAR,
58
+ ebook_asin VARCHAR,
59
+ audiobook_isbn VARCHAR,
60
+ audiobook_list_price FLOAT,
61
+ book_id VARCHAR
62
+ );
63
+ """
64
+ ),
49
65
  ]
50
66
 
51
67
 
@@ -1,4 +1,4 @@
1
- SELECT *
1
+ SELECT * exclude(deal_id)
2
2
  FROM retailer_deal
3
3
  QUALIFY ROW_NUMBER() OVER (PARTITION BY title, authors, retailer, format ORDER BY timepoint DESC) = 1 AND deleted IS NOT TRUE
4
4
  ORDER BY title, authors, retailer, format
@@ -1,4 +1,4 @@
1
- SELECT *
1
+ SELECT * exclude(deal_id)
2
2
  FROM retailer_deal
3
3
  WHERE timepoint = $timepoint AND deleted IS NOT TRUE
4
4
  ORDER BY title, authors, retailer, format
@@ -1,9 +1,11 @@
1
1
  from tbr_deal_finder.retailer.audible import Audible
2
2
  from tbr_deal_finder.retailer.chirp import Chirp
3
+ from tbr_deal_finder.retailer.kindle import Kindle
3
4
  from tbr_deal_finder.retailer.librofm import LibroFM
4
5
 
5
6
  RETAILER_MAP = {
6
7
  "Audible": Audible,
7
8
  "Chirp": Chirp,
8
9
  "Libro.FM": LibroFM,
10
+ "Kindle": Kindle,
9
11
  }
@@ -0,0 +1,85 @@
1
+ import sys
2
+ import os.path
3
+
4
+ import audible
5
+ import click
6
+ from audible.login import build_init_cookies
7
+ from textwrap import dedent
8
+
9
+ if sys.platform != 'win32':
10
+ # Breaks Windows support but required for Mac
11
+ # Untested on Linux
12
+ import readline # type: ignore
13
+
14
+ from tbr_deal_finder import TBR_DEALS_PATH
15
+ from tbr_deal_finder.config import Config
16
+ from tbr_deal_finder.retailer.models import Retailer
17
+
18
+ _AUTH_PATH = TBR_DEALS_PATH.joinpath("audible.json")
19
+
20
+
21
+ def login_url_callback(url: str) -> str:
22
+ """Helper function for login with external browsers."""
23
+
24
+ try:
25
+ from playwright.sync_api import sync_playwright # type: ignore
26
+ except ImportError:
27
+ pass
28
+ else:
29
+ with sync_playwright() as p:
30
+ iphone = p.devices["iPhone 12 Pro"]
31
+ browser = p.webkit.launch(headless=False)
32
+ context = browser.new_context(
33
+ **iphone
34
+ )
35
+ cookies = []
36
+ for name, value in build_init_cookies().items():
37
+ cookies.append(
38
+ {
39
+ "name": name,
40
+ "value": value,
41
+ "url": url
42
+ }
43
+ )
44
+ context.add_cookies(cookies)
45
+ page = browser.new_page()
46
+ page.goto(url)
47
+
48
+ while True:
49
+ page.wait_for_timeout(600)
50
+ if "/ap/maplanding" in page.url:
51
+ response_url = page.url
52
+ break
53
+
54
+ browser.close()
55
+ return response_url
56
+
57
+ message = f"""\
58
+ Please copy the following url and insert it into a web browser of your choice to log into Amazon.
59
+ Note: your browser will show you an error page (Page not found). This is expected.
60
+
61
+ {url}
62
+
63
+ Once you have logged in, please insert the copied url.
64
+ """
65
+ click.echo(dedent(message))
66
+ return input()
67
+
68
+
69
+ class Amazon(Retailer):
70
+ _auth: audible.Authenticator = None
71
+ _client: audible.AsyncClient = None
72
+
73
+ async def set_auth(self):
74
+ if not os.path.exists(_AUTH_PATH):
75
+ auth = audible.Authenticator.from_login_external(
76
+ locale=Config.locale,
77
+ login_url_callback=login_url_callback
78
+ )
79
+
80
+ # Save credentials to file
81
+ auth.to_file(_AUTH_PATH)
82
+
83
+ self._auth = audible.Authenticator.from_file(_AUTH_PATH)
84
+ self._client = audible.AsyncClient(auth=self._auth)
85
+
@@ -1,74 +1,12 @@
1
1
  import asyncio
2
2
  import math
3
- import os.path
4
- from datetime import datetime
5
- from textwrap import dedent
6
- import readline # type: ignore
7
3
 
8
-
9
- import audible
10
- import click
11
- from audible.login import build_init_cookies
12
-
13
- from tbr_deal_finder import TBR_DEALS_PATH
14
4
  from tbr_deal_finder.config import Config
15
- from tbr_deal_finder.retailer.models import Retailer
16
- from tbr_deal_finder.book import Book, BookFormat
17
-
18
- _AUTH_PATH = TBR_DEALS_PATH.joinpath("audible.json")
19
-
20
-
21
- def login_url_callback(url: str) -> str:
22
- """Helper function for login with external browsers."""
23
-
24
- try:
25
- from playwright.sync_api import sync_playwright # type: ignore
26
- except ImportError:
27
- pass
28
- else:
29
- with sync_playwright() as p:
30
- iphone = p.devices["iPhone 12 Pro"]
31
- browser = p.webkit.launch(headless=False)
32
- context = browser.new_context(
33
- **iphone
34
- )
35
- cookies = []
36
- for name, value in build_init_cookies().items():
37
- cookies.append(
38
- {
39
- "name": name,
40
- "value": value,
41
- "url": url
42
- }
43
- )
44
- context.add_cookies(cookies)
45
- page = browser.new_page()
46
- page.goto(url)
47
-
48
- while True:
49
- page.wait_for_timeout(600)
50
- if "/ap/maplanding" in page.url:
51
- response_url = page.url
52
- break
53
-
54
- browser.close()
55
- return response_url
56
-
57
- message = f"""\
58
- Please copy the following url and insert it into a web browser of your choice to log into Amazon.
59
- Note: your browser will show you an error page (Page not found). This is expected.
60
-
61
- {url}
5
+ from tbr_deal_finder.retailer.amazon import Amazon
6
+ from tbr_deal_finder.book import Book, BookFormat, get_normalized_title
62
7
 
63
- Once you have logged in, please insert the copied url.
64
- """
65
- click.echo(dedent(message))
66
- return input()
67
8
 
68
-
69
- class Audible(Retailer):
70
- _auth: audible.Authenticator = None
71
- _client: audible.AsyncClient = None
9
+ class Audible(Amazon):
72
10
 
73
11
  @property
74
12
  def name(self) -> str:
@@ -78,23 +16,9 @@ class Audible(Retailer):
78
16
  def format(self) -> BookFormat:
79
17
  return BookFormat.AUDIOBOOK
80
18
 
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
-
94
19
  async def get_book(
95
20
  self,
96
21
  target: Book,
97
- runtime: datetime,
98
22
  semaphore: asyncio.Semaphore
99
23
  ) -> Book:
100
24
  title = target.title
@@ -112,29 +36,18 @@ class Audible(Retailer):
112
36
  )
113
37
 
114
38
  for product in match.get("products", []):
115
- if product["title"] != title:
39
+ if get_normalized_title(product["title"]) != title:
40
+ continue
41
+ try:
42
+ target.list_price = product["price"]["list_price"]["base"]
43
+ target.current_price = product["price"]["lowest_price"]["base"]
44
+ target.exists = True
45
+ return target
46
+ except KeyError:
116
47
  continue
117
48
 
118
- return Book(
119
- retailer=self.name,
120
- title=title,
121
- authors=authors,
122
- list_price=product["price"]["list_price"]["base"],
123
- current_price=product["price"]["lowest_price"]["base"],
124
- timepoint=runtime,
125
- format=BookFormat.AUDIOBOOK
126
- )
127
-
128
- return Book(
129
- retailer=self.name,
130
- title=title,
131
- authors=authors,
132
- list_price=0,
133
- current_price=0,
134
- timepoint=runtime,
135
- format=BookFormat.AUDIOBOOK,
136
- exists=False,
137
- )
49
+ target.exists = False
50
+ return target
138
51
 
139
52
  async def get_wishlist(self, config: Config) -> list[Book]:
140
53
  wishlist_books = []