tbr-deal-finder 0.2.0__tar.gz → 0.2.1__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.
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/CHANGELOG.md +15 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/PKG-INFO +2 -2
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/README.md +1 -1
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/pyproject.toml +1 -1
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/book.py +13 -1
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/cli.py +2 -1
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/config.py +1 -1
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/migrations.py +26 -0
- tbr_deal_finder-0.2.1/tbr_deal_finder/queries/latest_unknown_book_sync.sql +5 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/retailer/amazon.py +4 -4
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/retailer/kindle.py +37 -25
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/retailer/librofm.py +5 -1
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/retailer_deal.py +24 -7
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/tracked_books.py +63 -1
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/uv.lock +1 -1
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/.github/workflows/publish-to-pypi.yaml +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/.gitignore +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/.python-version +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/DESIGN.md +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/LICENSE +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/__init__.py +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/owned_books.py +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/queries/get_active_deals.sql +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/queries/get_deals_found_at.sql +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/queries/latest_deal_last_ran_most_recent_success.sql +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/retailer/__init__.py +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/retailer/audible.py +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/retailer/chirp.py +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/retailer/models.py +0 -0
- {tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/utils.py +0 -0
@@ -3,6 +3,20 @@
|
|
3
3
|
|
4
4
|
---
|
5
5
|
|
6
|
+
## 0.2.1 (August 25, 2025)
|
7
|
+
|
8
|
+
Notes:
|
9
|
+
* Added Kindle Library support
|
10
|
+
* Wishlist support is looking unlikely
|
11
|
+
* Running into auth issues on the only viable endpoint https://www.amazon.com/kindle-reader-api
|
12
|
+
* No longer attempting to retrieve details on books not previously found on every run
|
13
|
+
* Full check is now performed weekly or when a change has been made to the user config
|
14
|
+
|
15
|
+
BUG FIXES:
|
16
|
+
* Failed Libro login no longer causing crash
|
17
|
+
|
18
|
+
---
|
19
|
+
|
6
20
|
## 0.2.0 (August 15, 2025)
|
7
21
|
|
8
22
|
Notes:
|
@@ -16,6 +30,7 @@ Notes:
|
|
16
30
|
|
17
31
|
BUG FIXES:
|
18
32
|
* Fixed breaking import on Windows systems
|
33
|
+
* Fixed displayed discount percent
|
19
34
|
|
20
35
|
---
|
21
36
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: tbr-deal-finder
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.1
|
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
|
@@ -23,7 +23,7 @@ Track price drops and find deals on books in your TBR (To Be Read) and wishlist
|
|
23
23
|
## Features
|
24
24
|
- Use your StoryGraph exports, Goodreads exports, and custom csvs (spreadsheet) to track book deals
|
25
25
|
- Supports multiple of the library exports above
|
26
|
-
- Tracks deals on the wishlist of all your configured retailers like audible
|
26
|
+
- Tracks deals on the wishlist of all your configured retailers like audible (excluding kindle)
|
27
27
|
- Supports multiple locales and currencies
|
28
28
|
- Find the latest and active deals from supported sellers
|
29
29
|
- Simple CLI interface for setup and usage
|
@@ -5,7 +5,7 @@ Track price drops and find deals on books in your TBR (To Be Read) and wishlist
|
|
5
5
|
## Features
|
6
6
|
- Use your StoryGraph exports, Goodreads exports, and custom csvs (spreadsheet) to track book deals
|
7
7
|
- Supports multiple of the library exports above
|
8
|
-
- Tracks deals on the wishlist of all your configured retailers like audible
|
8
|
+
- Tracks deals on the wishlist of all your configured retailers like audible (excluding kindle)
|
9
9
|
- Supports multiple locales and currencies
|
10
10
|
- Find the latest and active deals from supported sellers
|
11
11
|
- Simple CLI interface for setup and usage
|
@@ -55,7 +55,10 @@ class Book:
|
|
55
55
|
self.format = format
|
56
56
|
|
57
57
|
def discount(self) -> int:
|
58
|
-
|
58
|
+
if not self.current_price:
|
59
|
+
return 100
|
60
|
+
|
61
|
+
return int((1 - self.current_price/self.list_price) * 100)
|
59
62
|
|
60
63
|
@staticmethod
|
61
64
|
def price_to_string(price: float) -> str:
|
@@ -127,6 +130,15 @@ class Book:
|
|
127
130
|
"book_id": self.title_id,
|
128
131
|
}
|
129
132
|
|
133
|
+
def unknown_book_dict(self):
|
134
|
+
return {
|
135
|
+
"retailer": self.retailer,
|
136
|
+
"title": self.title,
|
137
|
+
"authors": self.authors,
|
138
|
+
"format": self.format.value,
|
139
|
+
"book_id": self.deal_id,
|
140
|
+
}
|
141
|
+
|
130
142
|
|
131
143
|
def get_deals_found_at(timepoint: datetime) -> list[Book]:
|
132
144
|
db_conn = get_duckdb_conn()
|
@@ -13,7 +13,7 @@ from tbr_deal_finder.migrations import make_migrations
|
|
13
13
|
from tbr_deal_finder.book import get_deals_found_at, print_books, get_active_deals
|
14
14
|
from tbr_deal_finder.retailer import RETAILER_MAP
|
15
15
|
from tbr_deal_finder.retailer_deal import get_latest_deals
|
16
|
-
from tbr_deal_finder.tracked_books import reprocess_incomplete_tbr_books
|
16
|
+
from tbr_deal_finder.tracked_books import reprocess_incomplete_tbr_books, clear_unknown_books
|
17
17
|
from tbr_deal_finder.utils import (
|
18
18
|
echo_err,
|
19
19
|
echo_info,
|
@@ -183,6 +183,7 @@ def setup():
|
|
183
183
|
# Retailers may have changed causing some books to need reprocessing
|
184
184
|
config = Config.load()
|
185
185
|
reprocess_incomplete_tbr_books(config)
|
186
|
+
clear_unknown_books()
|
186
187
|
|
187
188
|
|
188
189
|
@cli.command()
|
@@ -26,7 +26,7 @@ class Config:
|
|
26
26
|
library_export_paths: list[str]
|
27
27
|
tracked_retailers: list[str]
|
28
28
|
max_price: float = 8.0
|
29
|
-
min_discount: int =
|
29
|
+
min_discount: int = 30
|
30
30
|
run_time: datetime = datetime.now()
|
31
31
|
|
32
32
|
locale: str = "us" # This will be set as a class attribute below
|
@@ -62,6 +62,32 @@ _MIGRATIONS = [
|
|
62
62
|
);
|
63
63
|
"""
|
64
64
|
),
|
65
|
+
TableMigration(
|
66
|
+
version=1,
|
67
|
+
table_name="unknown_book",
|
68
|
+
sql="""
|
69
|
+
CREATE TABLE unknown_book
|
70
|
+
(
|
71
|
+
retailer VARCHAR,
|
72
|
+
title VARCHAR,
|
73
|
+
authors VARCHAR,
|
74
|
+
format VARCHAR,
|
75
|
+
book_id VARCHAR
|
76
|
+
);
|
77
|
+
"""
|
78
|
+
),
|
79
|
+
TableMigration(
|
80
|
+
version=1,
|
81
|
+
table_name="unknown_book_run_history",
|
82
|
+
sql="""
|
83
|
+
CREATE TABLE unknown_book_run_history
|
84
|
+
(
|
85
|
+
timepoint TIMESTAMP_NS,
|
86
|
+
ran_successfully BOOLEAN,
|
87
|
+
details VARCHAR
|
88
|
+
);
|
89
|
+
"""
|
90
|
+
),
|
65
91
|
]
|
66
92
|
|
67
93
|
|
@@ -15,7 +15,7 @@ from tbr_deal_finder import TBR_DEALS_PATH
|
|
15
15
|
from tbr_deal_finder.config import Config
|
16
16
|
from tbr_deal_finder.retailer.models import Retailer
|
17
17
|
|
18
|
-
|
18
|
+
AUTH_PATH = TBR_DEALS_PATH.joinpath("audible.json")
|
19
19
|
|
20
20
|
|
21
21
|
def login_url_callback(url: str) -> str:
|
@@ -71,15 +71,15 @@ class Amazon(Retailer):
|
|
71
71
|
_client: audible.AsyncClient = None
|
72
72
|
|
73
73
|
async def set_auth(self):
|
74
|
-
if not os.path.exists(
|
74
|
+
if not os.path.exists(AUTH_PATH):
|
75
75
|
auth = audible.Authenticator.from_login_external(
|
76
76
|
locale=Config.locale,
|
77
77
|
login_url_callback=login_url_callback
|
78
78
|
)
|
79
79
|
|
80
80
|
# Save credentials to file
|
81
|
-
auth.to_file(
|
81
|
+
auth.to_file(AUTH_PATH)
|
82
82
|
|
83
|
-
self._auth = audible.Authenticator.from_file(
|
83
|
+
self._auth = audible.Authenticator.from_file(AUTH_PATH)
|
84
84
|
self._client = audible.AsyncClient(auth=self._auth)
|
85
85
|
|
@@ -1,13 +1,17 @@
|
|
1
1
|
import asyncio
|
2
|
+
import json
|
2
3
|
import readline # type: ignore
|
3
4
|
|
4
5
|
from tbr_deal_finder.config import Config
|
5
|
-
from tbr_deal_finder.retailer.amazon import Amazon
|
6
|
+
from tbr_deal_finder.retailer.amazon import Amazon, AUTH_PATH
|
6
7
|
from tbr_deal_finder.book import Book, BookFormat, get_normalized_title, get_normalized_authors, is_matching_authors
|
7
8
|
|
8
9
|
|
9
10
|
class Kindle(Amazon):
|
10
11
|
|
12
|
+
def __init__(self):
|
13
|
+
self._headers = {}
|
14
|
+
|
11
15
|
@property
|
12
16
|
def name(self) -> str:
|
13
17
|
return "Kindle"
|
@@ -19,6 +23,25 @@ class Kindle(Amazon):
|
|
19
23
|
def _get_base_url(self) -> str:
|
20
24
|
return f"https://www.amazon.{self._auth.locale.domain}"
|
21
25
|
|
26
|
+
def _get_read_base_url(self) -> str:
|
27
|
+
return f"https://read.amazon.{self._auth.locale.domain}"
|
28
|
+
|
29
|
+
async def set_auth(self):
|
30
|
+
await super().set_auth()
|
31
|
+
|
32
|
+
with open(AUTH_PATH, "r") as f:
|
33
|
+
auth_info = json.load(f)
|
34
|
+
|
35
|
+
cookies = auth_info["website_cookies"]
|
36
|
+
cookies["x-access-token"] = auth_info["access_token"]
|
37
|
+
|
38
|
+
self._headers = {
|
39
|
+
"User-Agent": "Mozilla/5.0 (Linux; Android 10; Kindle) AppleWebKit/537.3",
|
40
|
+
"Accept": "application/json, */*",
|
41
|
+
"Cookie": "; ".join([f"{k}={v}" for k, v in cookies.items()])
|
42
|
+
}
|
43
|
+
|
44
|
+
|
22
45
|
async def get_book_asin(
|
23
46
|
self,
|
24
47
|
target: Book,
|
@@ -90,44 +113,28 @@ class Kindle(Amazon):
|
|
90
113
|
return []
|
91
114
|
|
92
115
|
async def get_library(self, config: Config) -> list[Book]:
|
93
|
-
|
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 = []
|
116
|
+
books = []
|
108
117
|
pagination_token = 0
|
109
|
-
|
118
|
+
url = f"{self._get_read_base_url()}/kindle-library/search"
|
110
119
|
|
111
|
-
while
|
120
|
+
while True:
|
112
121
|
optional_params = {}
|
113
122
|
if pagination_token:
|
114
123
|
optional_params["paginationToken"] = pagination_token
|
115
124
|
|
116
125
|
response = await self._client.get(
|
117
|
-
|
126
|
+
url,
|
127
|
+
headers=self._headers,
|
118
128
|
query="",
|
119
129
|
libraryType="BOOKS",
|
120
130
|
sortType="recency",
|
121
131
|
resourceType="EBOOK",
|
122
|
-
querySize=
|
132
|
+
querySize=50,
|
123
133
|
**optional_params
|
124
134
|
)
|
125
135
|
|
126
|
-
if "paginationToken" in response:
|
127
|
-
total_pages = int(response["paginationToken"])
|
128
|
-
|
129
136
|
for book in response["itemsList"]:
|
130
|
-
|
137
|
+
books.append(
|
131
138
|
Book(
|
132
139
|
retailer=self.name,
|
133
140
|
title = book["title"],
|
@@ -138,4 +145,9 @@ class Kindle(Amazon):
|
|
138
145
|
)
|
139
146
|
)
|
140
147
|
|
141
|
-
|
148
|
+
if "paginationToken" in response:
|
149
|
+
pagination_token = int(response["paginationToken"])
|
150
|
+
else:
|
151
|
+
break
|
152
|
+
|
153
|
+
return books
|
@@ -10,7 +10,7 @@ from tbr_deal_finder import TBR_DEALS_PATH
|
|
10
10
|
from tbr_deal_finder.config import Config
|
11
11
|
from tbr_deal_finder.retailer.models import AioHttpSession, Retailer
|
12
12
|
from tbr_deal_finder.book import Book, BookFormat, get_normalized_authors, is_matching_authors, get_normalized_title
|
13
|
-
from tbr_deal_finder.utils import currency_to_float
|
13
|
+
from tbr_deal_finder.utils import currency_to_float, echo_err
|
14
14
|
|
15
15
|
|
16
16
|
class LibroFM(AioHttpSession, Retailer):
|
@@ -77,6 +77,10 @@ class LibroFM(AioHttpSession, Retailer):
|
|
77
77
|
"password": click.prompt("Libro FM Password", hide_input=True),
|
78
78
|
}
|
79
79
|
)
|
80
|
+
if "access_token" not in response:
|
81
|
+
echo_err("Login failed. Try again.")
|
82
|
+
await self.set_auth()
|
83
|
+
|
80
84
|
self.auth_token = response["access_token"]
|
81
85
|
with open(auth_path, "w") as f:
|
82
86
|
json.dump(response, f)
|
@@ -8,7 +8,7 @@ from tqdm.asyncio import tqdm_asyncio
|
|
8
8
|
|
9
9
|
from tbr_deal_finder.book import Book, get_active_deals, BookFormat
|
10
10
|
from tbr_deal_finder.config import Config
|
11
|
-
from tbr_deal_finder.tracked_books import get_tbr_books
|
11
|
+
from tbr_deal_finder.tracked_books import get_tbr_books, get_unknown_books, set_unknown_books
|
12
12
|
from tbr_deal_finder.retailer import RETAILER_MAP
|
13
13
|
from tbr_deal_finder.retailer.models import Retailer
|
14
14
|
from tbr_deal_finder.utils import get_duckdb_conn, echo_warning, echo_info
|
@@ -55,7 +55,12 @@ def update_retailer_deal_table(config: Config, new_deals: list[Book]):
|
|
55
55
|
db_conn.unregister("_df")
|
56
56
|
|
57
57
|
|
58
|
-
async def _get_books(
|
58
|
+
async def _get_books(
|
59
|
+
config,
|
60
|
+
retailer: Retailer,
|
61
|
+
books: list[Book],
|
62
|
+
ignored_deal_ids: set[str],
|
63
|
+
) -> tuple[list[Book], list[Book]]:
|
59
64
|
"""Get Books with limited concurrency.
|
60
65
|
|
61
66
|
- Creates semaphore to limit concurrent requests.
|
@@ -72,7 +77,7 @@ async def _get_books(config, retailer: Retailer, books: list[Book]) -> list[Book
|
|
72
77
|
"""
|
73
78
|
semaphore = asyncio.Semaphore(10)
|
74
79
|
response = []
|
75
|
-
|
80
|
+
unknown_books = []
|
76
81
|
books = [copy.deepcopy(book) for book in books]
|
77
82
|
for book in books:
|
78
83
|
book.retailer = retailer.name
|
@@ -81,19 +86,20 @@ async def _get_books(config, retailer: Retailer, books: list[Book]) -> list[Book
|
|
81
86
|
tasks = [
|
82
87
|
retailer.get_book(book, semaphore)
|
83
88
|
for book in books
|
89
|
+
if book.deal_id not in ignored_deal_ids
|
84
90
|
]
|
85
91
|
results = await tqdm_asyncio.gather(*tasks, desc=f"Getting latest prices from {retailer.name}")
|
86
92
|
for book in results:
|
87
93
|
if book.exists:
|
88
94
|
response.append(book)
|
89
95
|
elif not book.exists:
|
90
|
-
|
96
|
+
unknown_books.append(book)
|
91
97
|
|
92
98
|
click.echo()
|
93
|
-
for book in
|
99
|
+
for book in unknown_books:
|
94
100
|
echo_info(f"{book.title} by {book.authors} not found")
|
95
101
|
|
96
|
-
return response
|
102
|
+
return response, unknown_books
|
97
103
|
|
98
104
|
|
99
105
|
def _apply_proper_list_prices(books: list[Book]):
|
@@ -169,7 +175,10 @@ async def get_latest_deals(config: Config):
|
|
169
175
|
"""
|
170
176
|
|
171
177
|
books: list[Book] = []
|
178
|
+
unknown_books: list[Book] = []
|
172
179
|
tbr_books = await get_tbr_books(config)
|
180
|
+
ignore_books: list[Book] = get_unknown_books(config)
|
181
|
+
ignored_deal_ids: set[str] = {book.deal_id for book in ignore_books}
|
173
182
|
|
174
183
|
for retailer_str in config.tracked_retailers:
|
175
184
|
retailer = RETAILER_MAP[retailer_str]()
|
@@ -182,7 +191,14 @@ async def get_latest_deals(config: Config):
|
|
182
191
|
|
183
192
|
echo_info(f"Getting deals from {retailer.name}")
|
184
193
|
click.echo("\n---------------")
|
185
|
-
|
194
|
+
retailer_books, u_books = await _get_books(
|
195
|
+
config,
|
196
|
+
retailer,
|
197
|
+
relevant_tbr_books,
|
198
|
+
ignored_deal_ids
|
199
|
+
)
|
200
|
+
books.extend(retailer_books)
|
201
|
+
unknown_books.extend(u_books)
|
186
202
|
click.echo("---------------\n")
|
187
203
|
|
188
204
|
_apply_proper_list_prices(books)
|
@@ -194,3 +210,4 @@ async def get_latest_deals(config: Config):
|
|
194
210
|
]
|
195
211
|
|
196
212
|
update_retailer_deal_table(config, books)
|
213
|
+
set_unknown_books(config, unknown_books)
|
@@ -1,7 +1,9 @@
|
|
1
1
|
import asyncio
|
2
2
|
import copy
|
3
3
|
import csv
|
4
|
+
import functools
|
4
5
|
from collections import defaultdict
|
6
|
+
from datetime import datetime, timedelta
|
5
7
|
from typing import Callable, Awaitable, Optional
|
6
8
|
|
7
9
|
import pandas as pd
|
@@ -12,7 +14,7 @@ from tbr_deal_finder.owned_books import get_owned_books
|
|
12
14
|
from tbr_deal_finder.retailer import Chirp, RETAILER_MAP, LibroFM, Kindle
|
13
15
|
from tbr_deal_finder.config import Config
|
14
16
|
from tbr_deal_finder.retailer.models import Retailer
|
15
|
-
from tbr_deal_finder.utils import execute_query, get_duckdb_conn
|
17
|
+
from tbr_deal_finder.utils import execute_query, get_duckdb_conn, get_query_by_name
|
16
18
|
|
17
19
|
|
18
20
|
def _library_export_tbr_books(config: Config, tbr_book_map: dict[str: Book]):
|
@@ -214,6 +216,66 @@ async def _maybe_set_audiobook_isbn(config: Config, new_tbr_books: list[Book]):
|
|
214
216
|
)
|
215
217
|
|
216
218
|
|
219
|
+
@functools.cache
|
220
|
+
def unknown_books_requires_sync() -> bool:
|
221
|
+
db_conn = get_duckdb_conn()
|
222
|
+
results = execute_query(
|
223
|
+
db_conn,
|
224
|
+
get_query_by_name("latest_unknown_book_sync.sql")
|
225
|
+
)
|
226
|
+
if not results:
|
227
|
+
return True
|
228
|
+
|
229
|
+
sync_last_ran = results[0]["timepoint"]
|
230
|
+
return datetime.now() - timedelta(days=7) > sync_last_ran
|
231
|
+
|
232
|
+
|
233
|
+
def clear_unknown_books():
|
234
|
+
db_conn = get_duckdb_conn()
|
235
|
+
db_conn.execute(
|
236
|
+
"DELETE FROM unknown_book"
|
237
|
+
)
|
238
|
+
db_conn.execute(
|
239
|
+
"DELETE FROM unknown_book_run_history"
|
240
|
+
)
|
241
|
+
|
242
|
+
|
243
|
+
def set_unknown_books(config: Config, unknown_books: list[Book]):
|
244
|
+
if not unknown_books_requires_sync():
|
245
|
+
return
|
246
|
+
|
247
|
+
db_conn = get_duckdb_conn()
|
248
|
+
db_conn.execute(
|
249
|
+
"INSERT INTO unknown_book_run_history (timepoint, ran_successfully, details) VALUES (?, ?, ?)",
|
250
|
+
[config.run_time, True, ""]
|
251
|
+
)
|
252
|
+
|
253
|
+
db_conn.execute(
|
254
|
+
"DELETE FROM unknown_book"
|
255
|
+
)
|
256
|
+
if not unknown_books:
|
257
|
+
return
|
258
|
+
|
259
|
+
df = pd.DataFrame([book.unknown_book_dict() for book in unknown_books])
|
260
|
+
db_conn = get_duckdb_conn()
|
261
|
+
db_conn.register("_df", df)
|
262
|
+
db_conn.execute("INSERT INTO unknown_book SELECT * FROM _df;")
|
263
|
+
db_conn.unregister("_df")
|
264
|
+
|
265
|
+
|
266
|
+
def get_unknown_books(config: Config) -> list[Book]:
|
267
|
+
if unknown_books_requires_sync():
|
268
|
+
return []
|
269
|
+
|
270
|
+
db_conn = get_duckdb_conn()
|
271
|
+
unknown_book_data = execute_query(
|
272
|
+
db_conn,
|
273
|
+
"SELECT * EXCLUDE(book_id) FROM unknown_book"
|
274
|
+
)
|
275
|
+
|
276
|
+
return [Book(timepoint=config.run_time, **b) for b in unknown_book_data]
|
277
|
+
|
278
|
+
|
217
279
|
async def _maybe_set_ebook_asin(config: Config, new_tbr_books: list[Book]):
|
218
280
|
"""To get the price from kindle for a book, you need its asin
|
219
281
|
"""
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/queries/get_active_deals.sql
RENAMED
File without changes
|
{tbr_deal_finder-0.2.0 → tbr_deal_finder-0.2.1}/tbr_deal_finder/queries/get_deals_found_at.sql
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|