tbr-deal-finder 0.1.8__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 (29) hide show
  1. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/CHANGELOG.md +16 -0
  2. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/PKG-INFO +4 -1
  3. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/README.md +3 -0
  4. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/pyproject.toml +1 -1
  5. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/book.py +25 -8
  6. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/retailer/__init__.py +2 -0
  7. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/retailer/amazon.py +6 -0
  8. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/retailer/audible.py +2 -3
  9. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/retailer/chirp.py +26 -24
  10. tbr_deal_finder-0.2.0/tbr_deal_finder/retailer/kindle.py +141 -0
  11. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/retailer/librofm.py +17 -16
  12. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/retailer/models.py +36 -0
  13. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/tracked_books.py +24 -5
  14. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/uv.lock +1 -1
  15. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/.github/workflows/publish-to-pypi.yaml +0 -0
  16. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/.gitignore +0 -0
  17. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/.python-version +0 -0
  18. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/DESIGN.md +0 -0
  19. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/LICENSE +0 -0
  20. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/__init__.py +0 -0
  21. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/cli.py +0 -0
  22. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/config.py +0 -0
  23. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/migrations.py +0 -0
  24. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/owned_books.py +0 -0
  25. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/queries/get_active_deals.sql +0 -0
  26. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/queries/get_deals_found_at.sql +0 -0
  27. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/queries/latest_deal_last_ran_most_recent_success.sql +0 -0
  28. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/retailer_deal.py +0 -0
  29. {tbr_deal_finder-0.1.8 → tbr_deal_finder-0.2.0}/tbr_deal_finder/utils.py +0 -0
@@ -3,6 +3,22 @@
3
3
 
4
4
  ---
5
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
+
6
22
  ## 0.1.8 (August 13, 2025)
7
23
 
8
24
  Notes:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tbr-deal-finder
3
- Version: 0.1.8
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tbr-deal-finder"
3
- version = "0.1.8"
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"
@@ -8,7 +8,7 @@ from Levenshtein import ratio
8
8
  from unidecode import unidecode
9
9
 
10
10
  from tbr_deal_finder.config import Config
11
- from tbr_deal_finder.utils import get_duckdb_conn, execute_query, get_query_by_name
11
+ from tbr_deal_finder.utils import get_duckdb_conn, execute_query, get_query_by_name, echo_info
12
12
 
13
13
  _AUTHOR_RE = re.compile(r'[^a-zA-Z0-9]')
14
14
 
@@ -36,7 +36,7 @@ class Book:
36
36
  exists: bool = True,
37
37
  ):
38
38
  self.retailer = retailer
39
- self.title = title.split(":")[0].split("(")[0].strip()
39
+ self.title = get_normalized_title(title)
40
40
  self.authors = authors
41
41
  self.timepoint = timepoint
42
42
 
@@ -148,13 +148,26 @@ def get_active_deals() -> list[Book]:
148
148
 
149
149
 
150
150
  def print_books(books: list[Book]):
151
- prior_title_id = books[0].title_id
152
- for book in books:
153
- if prior_title_id != book.title_id:
154
- prior_title_id = book.title_id
155
- click.echo()
151
+ audiobooks = [book for book in books if book.format == BookFormat.AUDIOBOOK]
152
+ audiobooks = sorted(audiobooks, key=lambda book: book.deal_id)
156
153
 
157
- click.echo(str(book))
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))
158
171
 
159
172
 
160
173
  def get_full_title_str(title: str, authors: Union[list, str]) -> str:
@@ -165,6 +178,10 @@ def get_title_id(title: str, authors: Union[list, str], book_format: BookFormat)
165
178
  return f"{title}__{get_normalized_authors(authors)}__{book_format.value}"
166
179
 
167
180
 
181
+ def get_normalized_title(title: str) -> str:
182
+ return title.split(":")[0].split("(")[0].strip()
183
+
184
+
168
185
  def get_normalized_authors(authors: Union[str, list[str]]) -> list[str]:
169
186
  if isinstance(authors, str):
170
187
  authors = [i for i in authors.split(",")]
@@ -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
  }
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  import os.path
2
3
 
3
4
  import audible
@@ -5,6 +6,11 @@ import click
5
6
  from audible.login import build_init_cookies
6
7
  from textwrap import dedent
7
8
 
9
+ if sys.platform != 'win32':
10
+ # Breaks Windows support but required for Mac
11
+ # Untested on Linux
12
+ import readline # type: ignore
13
+
8
14
  from tbr_deal_finder import TBR_DEALS_PATH
9
15
  from tbr_deal_finder.config import Config
10
16
  from tbr_deal_finder.retailer.models import Retailer
@@ -1,10 +1,9 @@
1
1
  import asyncio
2
2
  import math
3
- import readline # type: ignore
4
3
 
5
4
  from tbr_deal_finder.config import Config
6
5
  from tbr_deal_finder.retailer.amazon import Amazon
7
- from tbr_deal_finder.book import Book, BookFormat
6
+ from tbr_deal_finder.book import Book, BookFormat, get_normalized_title
8
7
 
9
8
 
10
9
  class Audible(Amazon):
@@ -37,7 +36,7 @@ class Audible(Amazon):
37
36
  )
38
37
 
39
38
  for product in match.get("products", []):
40
- if product["title"] != title:
39
+ if get_normalized_title(product["title"]) != title:
41
40
  continue
42
41
  try:
43
42
  target.list_price = product["price"]["list_price"]["base"]
@@ -9,17 +9,19 @@ import click
9
9
 
10
10
  from tbr_deal_finder import TBR_DEALS_PATH
11
11
  from tbr_deal_finder.config import Config
12
- from tbr_deal_finder.retailer.models import Retailer
12
+ from tbr_deal_finder.retailer.models import AioHttpSession, Retailer
13
13
  from tbr_deal_finder.book import Book, BookFormat, get_normalized_authors, is_matching_authors
14
14
  from tbr_deal_finder.utils import currency_to_float, echo_err
15
15
 
16
16
 
17
- class Chirp(Retailer):
17
+ class Chirp(AioHttpSession, Retailer):
18
18
  # Static because url for other locales just redirects to .com
19
19
  _url: str = "https://api.chirpbooks.com/api/graphql"
20
20
  USER_AGENT = "ChirpBooks/5.13.9 (Android)"
21
21
 
22
22
  def __init__(self):
23
+ super().__init__()
24
+
23
25
  self.auth_token = None
24
26
 
25
27
  @property
@@ -38,17 +40,17 @@ class Chirp(Retailer):
38
40
  if self.auth_token:
39
41
  headers["authorization"] = f"Bearer {self.auth_token}"
40
42
 
41
- async with aiohttp.ClientSession() as http_client:
42
- response = await http_client.request(
43
- request_type.upper(),
44
- self._url,
45
- headers=headers,
46
- **kwargs
47
- )
48
- if response.ok:
49
- return await response.json()
50
- else:
51
- return {}
43
+ session = await self._get_session()
44
+ response = await session.request(
45
+ request_type.upper(),
46
+ self._url,
47
+ headers=headers,
48
+ **kwargs
49
+ )
50
+ if response.ok:
51
+ return await response.json()
52
+ else:
53
+ return {}
52
54
 
53
55
  async def set_auth(self):
54
56
  auth_path = TBR_DEALS_PATH.joinpath("chirp.json")
@@ -88,17 +90,17 @@ class Chirp(Retailer):
88
90
  ) -> Book:
89
91
  title = target.title
90
92
  async with semaphore:
91
- async with aiohttp.ClientSession() as http_client:
92
- response = await http_client.request(
93
- "POST",
94
- self._url,
95
- json={
96
- "query": "fragment audiobookFields on Audiobook{id averageRating coverUrl displayAuthors displayTitle ratingsCount url allAuthors{name slug url}} fragment audiobookWithShoppingCartAndUserAudiobookFields on Audiobook{...audiobookFields currentUserShoppingCartItem{id}currentUserWishlistItem{id}currentUserUserAudiobook{id}currentUserHasAuthorFollow{id}} fragment productFields on Product{discountPrice id isFreeListing listingPrice purchaseUrl savingsPercent showListingPrice timeLeft bannerType} query AudiobookSearch($query:String!,$promotionFilter:String,$filter:String,$page:Int,$pageSize:Int){audiobooks(query:$query,promotionFilter:$promotionFilter,filter:$filter,page:$page,pageSize:$pageSize){totalCount objects(page:$page,pageSize:$pageSize){... on Audiobook{...audiobookWithShoppingCartAndUserAudiobookFields futureSaleDate currentProduct{...productFields}}}}}",
97
- "variables": {"query": title, "filter": "all", "page": 1, "promotionFilter": "default"},
98
- "operationName": "AudiobookSearch"
99
- }
100
- )
101
- response_body = await response.json()
93
+ session = await self._get_session()
94
+ response = await session.request(
95
+ "POST",
96
+ self._url,
97
+ json={
98
+ "query": "fragment audiobookFields on Audiobook{id averageRating coverUrl displayAuthors displayTitle ratingsCount url allAuthors{name slug url}} fragment audiobookWithShoppingCartAndUserAudiobookFields on Audiobook{...audiobookFields currentUserShoppingCartItem{id}currentUserWishlistItem{id}currentUserUserAudiobook{id}currentUserHasAuthorFollow{id}} fragment productFields on Product{discountPrice id isFreeListing listingPrice purchaseUrl savingsPercent showListingPrice timeLeft bannerType} query AudiobookSearch($query:String!,$promotionFilter:String,$filter:String,$page:Int,$pageSize:Int){audiobooks(query:$query,promotionFilter:$promotionFilter,filter:$filter,page:$page,pageSize:$pageSize){totalCount objects(page:$page,pageSize:$pageSize){... on Audiobook{...audiobookWithShoppingCartAndUserAudiobookFields futureSaleDate currentProduct{...productFields}}}}}",
99
+ "variables": {"query": title, "filter": "all", "page": 1, "promotionFilter": "default"},
100
+ "operationName": "AudiobookSearch"
101
+ }
102
+ )
103
+ response_body = await response.json()
102
104
 
103
105
  audiobooks = response_body["data"]["audiobooks"]["objects"]
104
106
  if not audiobooks:
@@ -0,0 +1,141 @@
1
+ import asyncio
2
+ import readline # type: ignore
3
+
4
+ from tbr_deal_finder.config import Config
5
+ from tbr_deal_finder.retailer.amazon import Amazon
6
+ from tbr_deal_finder.book import Book, BookFormat, get_normalized_title, get_normalized_authors, is_matching_authors
7
+
8
+
9
+ class Kindle(Amazon):
10
+
11
+ @property
12
+ def name(self) -> str:
13
+ return "Kindle"
14
+
15
+ @property
16
+ def format(self) -> BookFormat:
17
+ return BookFormat.EBOOK
18
+
19
+ def _get_base_url(self) -> str:
20
+ return f"https://www.amazon.{self._auth.locale.domain}"
21
+
22
+ async def get_book_asin(
23
+ self,
24
+ target: Book,
25
+ semaphore: asyncio.Semaphore
26
+ ) -> Book:
27
+ title = target.title
28
+ async with semaphore:
29
+ match = await self._client.get(
30
+ f"{self._get_base_url()}/kindle-dbs/kws?userCode=AndroidKin&deviceType=A3VNNDO1I14V03&node=2671536011&excludedNodes=&page=1&size=20&autoSpellCheck=1&rank=r",
31
+ query=title,
32
+ )
33
+
34
+ for product in match.get("items", []):
35
+ normalized_authors = get_normalized_authors(product["authors"])
36
+ if (
37
+ get_normalized_title(product["title"]) != title
38
+ or not is_matching_authors(target.normalized_authors, normalized_authors)
39
+ ):
40
+ continue
41
+ try:
42
+ target.ebook_asin = product["asin"]
43
+ break
44
+ except KeyError:
45
+ continue
46
+
47
+ return target
48
+
49
+ async def get_book(
50
+ self,
51
+ target: Book,
52
+ semaphore: asyncio.Semaphore
53
+ ) -> Book:
54
+ target.exists = False
55
+
56
+ if not target.ebook_asin:
57
+ return target
58
+
59
+ asin = target.ebook_asin
60
+ async with semaphore:
61
+ match = await self._client.get(
62
+ f"{self._get_base_url()}/api/bifrost/offers/batch/v1/{asin}?ref_=KindleDeepLinkOffers",
63
+ headers={"x-client-id": "kindle-android-deeplink"},
64
+ )
65
+ products = match.get("resources", [])
66
+ if not products:
67
+ return target
68
+
69
+ actions = products[0].get("personalizedActionOutput", {}).get("personalizedActions", [])
70
+ if not actions:
71
+ return target
72
+
73
+ for action in actions:
74
+ if "printListPrice" in action["offer"]:
75
+ target.list_price = action["offer"]["printListPrice"]["value"]
76
+ target.current_price = action["offer"]["digitalPrice"]["value"]
77
+ target.exists = True
78
+ break
79
+
80
+ return target
81
+
82
+ async def get_wishlist(self, config: Config) -> list[Book]:
83
+ """Not currently supported
84
+
85
+ Getting this info is proving to be a nightmare
86
+
87
+ :param config:
88
+ :return:
89
+ """
90
+ return []
91
+
92
+ async def get_library(self, config: Config) -> list[Book]:
93
+ """Not currently supported
94
+
95
+ Getting this info is proving to be a nightmare
96
+
97
+ :param config:
98
+ :return:
99
+ """
100
+ return []
101
+
102
+ async def _get_library_attempt(self, config: Config) -> list[Book]:
103
+ """This should work, but it's returning a redirect
104
+
105
+ The user is already authenticated at this point, so I'm not sure what's happening
106
+ """
107
+ response = []
108
+ pagination_token = 0
109
+ total_pages = 1
110
+
111
+ while pagination_token < total_pages:
112
+ optional_params = {}
113
+ if pagination_token:
114
+ optional_params["paginationToken"] = pagination_token
115
+
116
+ response = await self._client.get(
117
+ "https://read.amazon.com/kindle-library/search",
118
+ query="",
119
+ libraryType="BOOKS",
120
+ sortType="recency",
121
+ resourceType="EBOOK",
122
+ querySize=5,
123
+ **optional_params
124
+ )
125
+
126
+ if "paginationToken" in response:
127
+ total_pages = int(response["paginationToken"])
128
+
129
+ for book in response["itemsList"]:
130
+ response.append(
131
+ Book(
132
+ retailer=self.name,
133
+ title = book["title"],
134
+ authors = book["authors"][0],
135
+ format=self.format,
136
+ timepoint=config.run_time,
137
+ ebook_asin=book["asin"],
138
+ )
139
+ )
140
+
141
+ return response
@@ -4,17 +4,16 @@ import os
4
4
  import urllib.parse
5
5
  from datetime import datetime, timedelta
6
6
 
7
- import aiohttp
8
7
  import click
9
8
 
10
9
  from tbr_deal_finder import TBR_DEALS_PATH
11
10
  from tbr_deal_finder.config import Config
12
- from tbr_deal_finder.retailer.models import Retailer
13
- from tbr_deal_finder.book import Book, BookFormat, get_normalized_authors, is_matching_authors
11
+ from tbr_deal_finder.retailer.models import AioHttpSession, Retailer
12
+ from tbr_deal_finder.book import Book, BookFormat, get_normalized_authors, is_matching_authors, get_normalized_title
14
13
  from tbr_deal_finder.utils import currency_to_float
15
14
 
16
15
 
17
- class LibroFM(Retailer):
16
+ class LibroFM(AioHttpSession, Retailer):
18
17
  BASE_URL = "https://libro.fm"
19
18
  USER_AGENT = "okhttp/3.14.9"
20
19
  USER_AGENT_DOWNLOAD = (
@@ -27,6 +26,8 @@ class LibroFM(Retailer):
27
26
  )
28
27
 
29
28
  def __init__(self):
29
+ super().__init__()
30
+
30
31
  self.auth_token = None
31
32
 
32
33
  @property
@@ -44,17 +45,17 @@ class LibroFM(Retailer):
44
45
  if self.auth_token:
45
46
  headers["authorization"] = f"Bearer {self.auth_token}"
46
47
 
47
- async with aiohttp.ClientSession() as http_client:
48
- response = await http_client.request(
49
- request_type.upper(),
50
- url,
51
- headers=headers,
52
- **kwargs
53
- )
54
- if response.ok:
55
- return await response.json()
56
- else:
57
- return {}
48
+ session = await self._get_session()
49
+ response = await session.request(
50
+ request_type.upper(),
51
+ url,
52
+ headers=headers,
53
+ **kwargs
54
+ )
55
+ if response.ok:
56
+ return await response.json()
57
+ else:
58
+ return {}
58
59
 
59
60
  async def set_auth(self):
60
61
  auth_path = TBR_DEALS_PATH.joinpath("libro_fm.json")
@@ -100,7 +101,7 @@ class LibroFM(Retailer):
100
101
  normalized_authors = get_normalized_authors(b["authors"])
101
102
 
102
103
  if (
103
- title == b["title"]
104
+ title == get_normalized_title(b["title"])
104
105
  and is_matching_authors(book.normalized_authors, normalized_authors)
105
106
  ):
106
107
  book.audiobook_isbn = b["isbn"]
@@ -1,6 +1,8 @@
1
1
  import abc
2
2
  import asyncio
3
3
 
4
+ import aiohttp
5
+
4
6
  from tbr_deal_finder.book import Book, BookFormat
5
7
  from tbr_deal_finder.config import Config
6
8
 
@@ -52,3 +54,37 @@ class Retailer(abc.ABC):
52
54
  async def get_library(self, config: Config) -> list[Book]:
53
55
  raise NotImplementedError
54
56
 
57
+
58
+ class AioHttpSession:
59
+
60
+ def __init__(self):
61
+ self._session = None
62
+
63
+ async def _get_session(self) -> aiohttp.ClientSession:
64
+ """Get or create the session."""
65
+ if self._session is None or self._session.closed:
66
+ self._session = aiohttp.ClientSession()
67
+ return self._session
68
+
69
+ async def close(self):
70
+ """Close the session when done."""
71
+ if self._session and not self._session.closed:
72
+ await self._session.close()
73
+
74
+ async def __aenter__(self):
75
+ """Support async context manager."""
76
+ return self
77
+
78
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
79
+ """Cleanup on context manager exit."""
80
+ await self.close()
81
+
82
+ def __del__(self):
83
+ """Attempt to close session on garbage collection."""
84
+ if self._session and not self._session.closed:
85
+ try:
86
+ asyncio.create_task(self._session.close())
87
+ except RuntimeError:
88
+ # Event loop might be closed
89
+ pass
90
+
@@ -9,7 +9,7 @@ from tqdm.asyncio import tqdm_asyncio
9
9
 
10
10
  from tbr_deal_finder.book import Book, BookFormat, get_title_id
11
11
  from tbr_deal_finder.owned_books import get_owned_books
12
- from tbr_deal_finder.retailer import Chirp, RETAILER_MAP, LibroFM
12
+ from tbr_deal_finder.retailer import Chirp, RETAILER_MAP, LibroFM, Kindle
13
13
  from tbr_deal_finder.config import Config
14
14
  from tbr_deal_finder.retailer.models import Retailer
15
15
  from tbr_deal_finder.utils import execute_query, get_duckdb_conn
@@ -194,10 +194,6 @@ async def _maybe_set_audiobook_list_price(config: Config, new_tbr_books: list[Bo
194
194
 
195
195
  async def _maybe_set_audiobook_isbn(config: Config, new_tbr_books: list[Book]):
196
196
  """To get the price from Libro.fm for a book, you need its ISBN
197
-
198
- As opposed to trying to get that every time latest-deals is run
199
- we're just updating the export csv once to include the ISBN.
200
-
201
197
  """
202
198
  if "Libro.FM" not in config.tracked_retailers:
203
199
  return
@@ -218,6 +214,28 @@ async def _maybe_set_audiobook_isbn(config: Config, new_tbr_books: list[Book]):
218
214
  )
219
215
 
220
216
 
217
+ async def _maybe_set_ebook_asin(config: Config, new_tbr_books: list[Book]):
218
+ """To get the price from kindle for a book, you need its asin
219
+ """
220
+ if "Kindle" not in config.tracked_retailers:
221
+ return
222
+
223
+ kindle = Kindle()
224
+ await kindle.set_auth()
225
+
226
+ relevant_tbr_books = [
227
+ book
228
+ for book in new_tbr_books
229
+ if book.format in [BookFormat.EBOOK, BookFormat.NA]
230
+ ]
231
+
232
+ await _set_tbr_book_attr(
233
+ relevant_tbr_books,
234
+ "ebook_asin",
235
+ kindle.get_book_asin,
236
+ )
237
+
238
+
221
239
  def get_book_authors(book: dict) -> str:
222
240
  if authors := book.get('Authors'):
223
241
  return authors
@@ -301,6 +319,7 @@ async def sync_tbr_books(config: Config):
301
319
 
302
320
  await _maybe_set_audiobook_list_price(config, new_tbr_books)
303
321
  await _maybe_set_audiobook_isbn(config, new_tbr_books)
322
+ await _maybe_set_ebook_asin(config, new_tbr_books)
304
323
 
305
324
  df = pd.DataFrame([book.tbr_dict() for book in new_tbr_books])
306
325
  db_conn.register("_df", df)
@@ -612,7 +612,7 @@ wheels = [
612
612
 
613
613
  [[package]]
614
614
  name = "tbr-deal-finder"
615
- version = "0.1.8"
615
+ version = "0.2.0"
616
616
  source = { editable = "." }
617
617
  dependencies = [
618
618
  { name = "aiohttp" },
File without changes