tbr-deal-finder 0.1.7__py3-none-any.whl → 0.2.0__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
@@ -1,58 +1,58 @@
1
- import dataclasses
2
1
  import re
3
2
  from datetime import datetime
4
3
  from enum import Enum
5
- from typing import Optional, Union
4
+ from typing import Union
6
5
 
7
6
  import click
8
7
  from Levenshtein import ratio
9
8
  from unidecode import unidecode
10
9
 
11
10
  from tbr_deal_finder.config import Config
12
- 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
13
12
 
14
13
  _AUTHOR_RE = re.compile(r'[^a-zA-Z0-9]')
15
14
 
16
15
  class BookFormat(Enum):
17
16
  AUDIOBOOK = "Audiobook"
18
- NA = "N/A" # When format does not matter
19
-
20
-
21
- @dataclasses.dataclass
22
- class Book:
23
- retailer: str
24
- title: str
25
- authors: str
26
- list_price: float
27
- current_price: float
28
- timepoint: datetime
29
- format: Union[BookFormat, str]
30
-
31
- # Metadata really only used for tracked books.
32
- # See get_tbr_books for more context
33
- audiobook_isbn: str = None
34
- audiobook_list_price: float = 0
35
-
36
- deleted: bool = False
37
-
38
- deal_id: Optional[str] = None
39
- exists: bool = True
40
- normalized_authors: list[str] = None
41
-
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
-
51
- if not self.deal_id:
52
- self.deal_id = f"{self.title}__{self.normalized_authors}__{self.format}__{self.retailer}"
53
-
54
- if isinstance(self.format, str):
55
- self.format = BookFormat(self.format)
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
56
 
57
57
  def discount(self) -> int:
58
58
  return int((self.list_price/self.current_price - 1) * 100)
@@ -61,6 +61,10 @@ class Book:
61
61
  def price_to_string(price: float) -> str:
62
62
  return f"{Config.currency_symbol()}{price:.2f}"
63
63
 
64
+ @property
65
+ def deal_id(self) -> str:
66
+ return f"{self.title}__{self.normalized_authors}__{self.format}__{self.retailer}"
67
+
64
68
  @property
65
69
  def title_id(self) -> str:
66
70
  return f"{self.title}__{self.normalized_authors}__{self.format}"
@@ -69,6 +73,22 @@ class Book:
69
73
  def full_title_str(self) -> str:
70
74
  return f"{self.title}__{self.normalized_authors}"
71
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
+
72
92
  def list_price_string(self):
73
93
  return self.price_to_string(self.list_price)
74
94
 
@@ -81,17 +101,31 @@ class Book:
81
101
  title = self.title
82
102
  if len(self.title) > 75:
83
103
  title = f"{title[:75]}..."
84
- return f"{title} by {self.authors} - {price} - {self.discount()}% Off at {self.retailer} - {book_format}"
104
+ return f"{title} by {self.authors} - {price} - {self.discount()}% Off at {self.retailer}"
85
105
 
86
106
  def dict(self):
87
- response = dataclasses.asdict(self)
88
- response["format"] = self.format.value
89
- del response["audiobook_isbn"]
90
- del response["audiobook_list_price"]
91
- del response["exists"]
92
- del response["normalized_authors"]
93
-
94
- return response
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
+ }
95
129
 
96
130
 
97
131
  def get_deals_found_at(timepoint: datetime) -> list[Book]:
@@ -114,13 +148,26 @@ def get_active_deals() -> list[Book]:
114
148
 
115
149
 
116
150
  def print_books(books: list[Book]):
117
- prior_title_id = books[0].title_id
118
- for book in books:
119
- if prior_title_id != book.title_id:
120
- prior_title_id = book.title_id
121
- click.echo()
151
+ audiobooks = [book for book in books if book.format == BookFormat.AUDIOBOOK]
152
+ audiobooks = sorted(audiobooks, key=lambda book: book.deal_id)
122
153
 
123
- 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))
124
171
 
125
172
 
126
173
  def get_full_title_str(title: str, authors: Union[list, str]) -> str:
@@ -131,6 +178,10 @@ def get_title_id(title: str, authors: Union[list, str], book_format: BookFormat)
131
178
  return f"{title}__{get_normalized_authors(authors)}__{book_format.value}"
132
179
 
133
180
 
181
+ def get_normalized_title(title: str) -> str:
182
+ return title.split(":")[0].split("(")[0].strip()
183
+
184
+
134
185
  def get_normalized_authors(authors: Union[str, list[str]]) -> list[str]:
135
186
  if isinstance(authors, str):
136
187
  authors = [i for i in authors.split(",")]
tbr_deal_finder/cli.py CHANGED
@@ -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,
tbr_deal_finder/config.py CHANGED
@@ -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 = []
@@ -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")
@@ -84,35 +86,26 @@ class Chirp(Retailer):
84
86
  json.dump(response, f)
85
87
 
86
88
  async def get_book(
87
- self, target: Book, runtime: datetime, semaphore: asyncio.Semaphore
89
+ self, target: Book, semaphore: asyncio.Semaphore
88
90
  ) -> Book:
89
91
  title = target.title
90
- authors = target.authors
91
92
  async with semaphore:
92
- async with aiohttp.ClientSession() as http_client:
93
- response = await http_client.request(
94
- "POST",
95
- self._url,
96
- json={
97
- "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}}}}}",
98
- "variables": {"query": title, "filter": "all", "page": 1, "promotionFilter": "default"},
99
- "operationName": "AudiobookSearch"
100
- }
101
- )
102
- 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()
103
104
 
104
105
  audiobooks = response_body["data"]["audiobooks"]["objects"]
105
106
  if not audiobooks:
106
- return Book(
107
- retailer=self.name,
108
- title=title,
109
- authors=authors,
110
- list_price=0,
111
- current_price=0,
112
- timepoint=runtime,
113
- format=BookFormat.AUDIOBOOK,
114
- exists=False,
115
- )
107
+ target.exists = False
108
+ return target
116
109
 
117
110
  for book in audiobooks:
118
111
  if not book["currentProduct"]:
@@ -123,26 +116,12 @@ class Chirp(Retailer):
123
116
  book["displayTitle"] == title
124
117
  and is_matching_authors(target.normalized_authors, normalized_authors)
125
118
  ):
126
- return Book(
127
- retailer=self.name,
128
- title=title,
129
- authors=authors,
130
- list_price=currency_to_float(book["currentProduct"]["listingPrice"]),
131
- current_price=currency_to_float(book["currentProduct"]["discountPrice"]),
132
- timepoint=runtime,
133
- format=BookFormat.AUDIOBOOK,
134
- )
119
+ target.list_price = currency_to_float(book["currentProduct"]["listingPrice"])
120
+ target.current_price = currency_to_float(book["currentProduct"]["discountPrice"])
121
+ return target
135
122
 
136
- return Book(
137
- retailer=self.name,
138
- title=title,
139
- authors=target.authors,
140
- list_price=0,
141
- current_price=0,
142
- timepoint=runtime,
143
- format=BookFormat.AUDIOBOOK,
144
- exists=False,
145
- )
123
+ target.exists = False
124
+ return target
146
125
 
147
126
  async def get_wishlist(self, config: Config) -> list[Book]:
148
127
  wishlist_books = []