tbr-deal-finder 0.1.8__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 +25 -8
- tbr_deal_finder/retailer/__init__.py +2 -0
- tbr_deal_finder/retailer/amazon.py +6 -0
- tbr_deal_finder/retailer/audible.py +2 -3
- tbr_deal_finder/retailer/chirp.py +26 -24
- tbr_deal_finder/retailer/kindle.py +141 -0
- tbr_deal_finder/retailer/librofm.py +17 -16
- tbr_deal_finder/retailer/models.py +36 -0
- tbr_deal_finder/tracked_books.py +24 -5
- {tbr_deal_finder-0.1.8.dist-info → tbr_deal_finder-0.2.0.dist-info}/METADATA +4 -1
- tbr_deal_finder-0.2.0.dist-info/RECORD +24 -0
- tbr_deal_finder-0.1.8.dist-info/RECORD +0 -23
- {tbr_deal_finder-0.1.8.dist-info → tbr_deal_finder-0.2.0.dist-info}/WHEEL +0 -0
- {tbr_deal_finder-0.1.8.dist-info → tbr_deal_finder-0.2.0.dist-info}/entry_points.txt +0 -0
- {tbr_deal_finder-0.1.8.dist-info → tbr_deal_finder-0.2.0.dist-info}/licenses/LICENSE +0 -0
tbr_deal_finder/book.py
CHANGED
@@ -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
|
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
|
-
|
152
|
-
|
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
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
+
|
tbr_deal_finder/tracked_books.py
CHANGED
@@ -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)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: tbr-deal-finder
|
3
|
-
Version: 0.
|
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
|
@@ -0,0 +1,24 @@
|
|
1
|
+
tbr_deal_finder/__init__.py,sha256=WCoj0GZrRiCQlrpkLTw1VUeJmX-RtBLdLqnFYn1Es_4,208
|
2
|
+
tbr_deal_finder/book.py,sha256=vCvkjU98mI0Z7WW_Z3GppnI4aem9ht-flB8HB4RCujQ,6107
|
3
|
+
tbr_deal_finder/cli.py,sha256=C4F2rbPrfYNqlmolx08ZHDCcFJuiPbkc4ECXUO25kmI,7446
|
4
|
+
tbr_deal_finder/config.py,sha256=-TtZLv4kVBf56xPkgAdKXeVRV0qw8MZ53XHBQ1HnVX8,3978
|
5
|
+
tbr_deal_finder/migrations.py,sha256=_ZxUXzGyEFYlPlpzMvViDVPZJc5BNOiixj150U8HRFc,4224
|
6
|
+
tbr_deal_finder/owned_books.py,sha256=Cf1VeiSg7XBi_TXptJfy5sO1mEgMMQWbJ_P6SzAx0nQ,516
|
7
|
+
tbr_deal_finder/retailer_deal.py,sha256=jv32WSOtxVxCkxTCLkOqSkcHGHhWfbD4nSxY42Cqk38,6422
|
8
|
+
tbr_deal_finder/tracked_books.py,sha256=TdVD5kIgdkDoQNwBIsOikrvFiQiIezmSYf64F5cBN0o,10856
|
9
|
+
tbr_deal_finder/utils.py,sha256=_4wdGFDtqCdMyoMnwTDiHgCR4WQLAcQr8LlZZZUcq6E,1357
|
10
|
+
tbr_deal_finder/queries/get_active_deals.sql,sha256=nh0F1lRV6YVrUV7gsQpjsgfXmN9R0peBeMHRifjgpUM,212
|
11
|
+
tbr_deal_finder/queries/get_deals_found_at.sql,sha256=KqrtQk7FS4Hf74RyL1r-oD2D-RJz1urrxKxkwlvjAro,139
|
12
|
+
tbr_deal_finder/queries/latest_deal_last_ran_most_recent_success.sql,sha256=W4cNMAHtcW2DzQyPL8SHHFcbVZQKVK2VfTzazxC3LJU,107
|
13
|
+
tbr_deal_finder/retailer/__init__.py,sha256=OD6jUYV8LaURxqHnZq-aiFi7OdWG6qWznRlF_g246lo,316
|
14
|
+
tbr_deal_finder/retailer/amazon.py,sha256=rYJlBT4JsOg7QVIhJ8a7xJKUjczP_RMK2m-ddH7FjlQ,2472
|
15
|
+
tbr_deal_finder/retailer/audible.py,sha256=qwXDKc1W8vGGhqvU2YI7hNfD1rHz2yL-8foXstxb8t8,3991
|
16
|
+
tbr_deal_finder/retailer/chirp.py,sha256=f_O-6X9duR_gBT8UWDxDr-KQYSRFOOiYOX9Az2pCi6Y,9183
|
17
|
+
tbr_deal_finder/retailer/kindle.py,sha256=ELdKSzKMCmZWw8TjGHihuyZcgqwVygi94mZFOYQ61qY,4489
|
18
|
+
tbr_deal_finder/retailer/librofm.py,sha256=Oi9UlkyCqdI-PSRApGSCv_TWP7yWwvYEADOZleAOSmM,6357
|
19
|
+
tbr_deal_finder/retailer/models.py,sha256=xm99ngt_Ze7yyEwttddkpwL7xjy0YfcFAduV6Rsx63M,2510
|
20
|
+
tbr_deal_finder-0.2.0.dist-info/METADATA,sha256=ZS-Q4WHHC3Y7BGbSC7j6ClKizcdVMSVfHJ2KpbV7DXk,4363
|
21
|
+
tbr_deal_finder-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
22
|
+
tbr_deal_finder-0.2.0.dist-info/entry_points.txt,sha256=y_KG1k8xVCY8gngSZ-na2bkK-tTLUdOc_qZ9Djwldv0,60
|
23
|
+
tbr_deal_finder-0.2.0.dist-info/licenses/LICENSE,sha256=rNc0wNPn4d4HHu6ZheJzeUaz_FbJ4rj2Dr2FjAivkNg,1064
|
24
|
+
tbr_deal_finder-0.2.0.dist-info/RECORD,,
|
@@ -1,23 +0,0 @@
|
|
1
|
-
tbr_deal_finder/__init__.py,sha256=WCoj0GZrRiCQlrpkLTw1VUeJmX-RtBLdLqnFYn1Es_4,208
|
2
|
-
tbr_deal_finder/book.py,sha256=MYdZ7WeSo9hWy9Af7T5U3-7zPgi5qdMPqlNtRYPMLck,5492
|
3
|
-
tbr_deal_finder/cli.py,sha256=C4F2rbPrfYNqlmolx08ZHDCcFJuiPbkc4ECXUO25kmI,7446
|
4
|
-
tbr_deal_finder/config.py,sha256=-TtZLv4kVBf56xPkgAdKXeVRV0qw8MZ53XHBQ1HnVX8,3978
|
5
|
-
tbr_deal_finder/migrations.py,sha256=_ZxUXzGyEFYlPlpzMvViDVPZJc5BNOiixj150U8HRFc,4224
|
6
|
-
tbr_deal_finder/owned_books.py,sha256=Cf1VeiSg7XBi_TXptJfy5sO1mEgMMQWbJ_P6SzAx0nQ,516
|
7
|
-
tbr_deal_finder/retailer_deal.py,sha256=jv32WSOtxVxCkxTCLkOqSkcHGHhWfbD4nSxY42Cqk38,6422
|
8
|
-
tbr_deal_finder/tracked_books.py,sha256=SOmygViADjr8fa3RiB4dn_bWjIpiAOBNQK37MHOu7nE,10407
|
9
|
-
tbr_deal_finder/utils.py,sha256=_4wdGFDtqCdMyoMnwTDiHgCR4WQLAcQr8LlZZZUcq6E,1357
|
10
|
-
tbr_deal_finder/queries/get_active_deals.sql,sha256=nh0F1lRV6YVrUV7gsQpjsgfXmN9R0peBeMHRifjgpUM,212
|
11
|
-
tbr_deal_finder/queries/get_deals_found_at.sql,sha256=KqrtQk7FS4Hf74RyL1r-oD2D-RJz1urrxKxkwlvjAro,139
|
12
|
-
tbr_deal_finder/queries/latest_deal_last_ran_most_recent_success.sql,sha256=W4cNMAHtcW2DzQyPL8SHHFcbVZQKVK2VfTzazxC3LJU,107
|
13
|
-
tbr_deal_finder/retailer/__init__.py,sha256=WePMSN7vi4EL_uPiAH6ogNNE-kRQe4OHT4CYGTKvBSk,243
|
14
|
-
tbr_deal_finder/retailer/amazon.py,sha256=-U28l3LmVCbOvK0SIXG6MAfYgoRUA--enNQJBxA_NHM,2322
|
15
|
-
tbr_deal_finder/retailer/audible.py,sha256=1zM95teGth4UPyK5ZogLM9SIQxVd3kTgjTEiglnQKR8,3979
|
16
|
-
tbr_deal_finder/retailer/chirp.py,sha256=s5aj3NwMA1GSymmTylnoWghQsqMp7hFcMdnoA1TRsv4,9241
|
17
|
-
tbr_deal_finder/retailer/librofm.py,sha256=ARkUlxMS3OCgfKosX_XKVmMfLcIIDRPzqqmVp0V9IZA,6327
|
18
|
-
tbr_deal_finder/retailer/models.py,sha256=I_U8VC5AOozGfPecW7FW8eRoTWEOxnMIHPNbuOJWcjw,1463
|
19
|
-
tbr_deal_finder-0.1.8.dist-info/METADATA,sha256=4urAR2x-ABDN0OMscyowYP8Gqc_RXlVsYJoaRxpF7m8,4342
|
20
|
-
tbr_deal_finder-0.1.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
21
|
-
tbr_deal_finder-0.1.8.dist-info/entry_points.txt,sha256=y_KG1k8xVCY8gngSZ-na2bkK-tTLUdOc_qZ9Djwldv0,60
|
22
|
-
tbr_deal_finder-0.1.8.dist-info/licenses/LICENSE,sha256=rNc0wNPn4d4HHu6ZheJzeUaz_FbJ4rj2Dr2FjAivkNg,1064
|
23
|
-
tbr_deal_finder-0.1.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|