tbr-deal-finder 0.1.6__py3-none-any.whl → 0.1.8__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 +107 -47
- tbr_deal_finder/cli.py +26 -25
- tbr_deal_finder/config.py +20 -2
- tbr_deal_finder/migrations.py +16 -0
- tbr_deal_finder/owned_books.py +18 -0
- tbr_deal_finder/queries/get_active_deals.sql +1 -1
- tbr_deal_finder/queries/get_deals_found_at.sql +1 -1
- tbr_deal_finder/retailer/amazon.py +79 -0
- tbr_deal_finder/retailer/audible.py +47 -97
- tbr_deal_finder/retailer/chirp.py +69 -33
- tbr_deal_finder/retailer/librofm.py +44 -40
- tbr_deal_finder/retailer/models.py +3 -2
- tbr_deal_finder/retailer_deal.py +37 -34
- tbr_deal_finder/tracked_books.py +239 -39
- {tbr_deal_finder-0.1.6.dist-info → tbr_deal_finder-0.1.8.dist-info}/METADATA +8 -6
- tbr_deal_finder-0.1.8.dist-info/RECORD +23 -0
- tbr_deal_finder/library_exports.py +0 -205
- tbr_deal_finder-0.1.6.dist-info/RECORD +0 -22
- {tbr_deal_finder-0.1.6.dist-info → tbr_deal_finder-0.1.8.dist-info}/WHEEL +0 -0
- {tbr_deal_finder-0.1.6.dist-info → tbr_deal_finder-0.1.8.dist-info}/entry_points.txt +0 -0
- {tbr_deal_finder-0.1.6.dist-info → tbr_deal_finder-0.1.8.dist-info}/licenses/LICENSE +0 -0
tbr_deal_finder/book.py
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
import dataclasses
|
2
1
|
import re
|
3
2
|
from datetime import datetime
|
4
3
|
from enum import Enum
|
5
|
-
from typing import
|
4
|
+
from typing import Union
|
6
5
|
|
7
6
|
import click
|
7
|
+
from Levenshtein import ratio
|
8
8
|
from unidecode import unidecode
|
9
9
|
|
10
10
|
from tbr_deal_finder.config import Config
|
@@ -14,40 +14,45 @@ _AUTHOR_RE = re.compile(r'[^a-zA-Z0-9]')
|
|
14
14
|
|
15
15
|
class BookFormat(Enum):
|
16
16
|
AUDIOBOOK = "Audiobook"
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
class Book:
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
self.
|
44
|
-
self.
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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 = title.split(":")[0].split("(")[0].strip()
|
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
|
51
56
|
|
52
57
|
def discount(self) -> int:
|
53
58
|
return int((self.list_price/self.current_price - 1) * 100)
|
@@ -56,6 +61,10 @@ class Book:
|
|
56
61
|
def price_to_string(price: float) -> str:
|
57
62
|
return f"{Config.currency_symbol()}{price:.2f}"
|
58
63
|
|
64
|
+
@property
|
65
|
+
def deal_id(self) -> str:
|
66
|
+
return f"{self.title}__{self.normalized_authors}__{self.format}__{self.retailer}"
|
67
|
+
|
59
68
|
@property
|
60
69
|
def title_id(self) -> str:
|
61
70
|
return f"{self.title}__{self.normalized_authors}__{self.format}"
|
@@ -64,6 +73,22 @@ class Book:
|
|
64
73
|
def full_title_str(self) -> str:
|
65
74
|
return f"{self.title}__{self.normalized_authors}"
|
66
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
|
+
|
67
92
|
def list_price_string(self):
|
68
93
|
return self.price_to_string(self.list_price)
|
69
94
|
|
@@ -76,17 +101,31 @@ class Book:
|
|
76
101
|
title = self.title
|
77
102
|
if len(self.title) > 75:
|
78
103
|
title = f"{title[:75]}..."
|
79
|
-
return f"{title} by {self.authors} - {price} - {self.discount()}% Off at {self.retailer}
|
104
|
+
return f"{title} by {self.authors} - {price} - {self.discount()}% Off at {self.retailer}"
|
80
105
|
|
81
106
|
def dict(self):
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
+
}
|
90
129
|
|
91
130
|
|
92
131
|
def get_deals_found_at(timepoint: datetime) -> list[Book]:
|
@@ -118,6 +157,14 @@ def print_books(books: list[Book]):
|
|
118
157
|
click.echo(str(book))
|
119
158
|
|
120
159
|
|
160
|
+
def get_full_title_str(title: str, authors: Union[list, str]) -> str:
|
161
|
+
return f"{title}__{get_normalized_authors(authors)}"
|
162
|
+
|
163
|
+
|
164
|
+
def get_title_id(title: str, authors: Union[list, str], book_format: BookFormat) -> str:
|
165
|
+
return f"{title}__{get_normalized_authors(authors)}__{book_format.value}"
|
166
|
+
|
167
|
+
|
121
168
|
def get_normalized_authors(authors: Union[str, list[str]]) -> list[str]:
|
122
169
|
if isinstance(authors, str):
|
123
170
|
authors = [i for i in authors.split(",")]
|
@@ -125,5 +172,18 @@ def get_normalized_authors(authors: Union[str, list[str]]) -> list[str]:
|
|
125
172
|
return sorted([_AUTHOR_RE.sub('', unidecode(author)).lower() for author in authors])
|
126
173
|
|
127
174
|
|
128
|
-
def
|
129
|
-
|
175
|
+
def is_matching_authors(a1: list[str], a2: list[str]) -> bool:
|
176
|
+
"""Checks if two normalized authors are matching.
|
177
|
+
Matching here means that they are at least 80% similar using levenshtein distance.
|
178
|
+
|
179
|
+
Score is calculated as follows:
|
180
|
+
1 - (distance / (len1 + len2))
|
181
|
+
|
182
|
+
:param a1:
|
183
|
+
:param a2:
|
184
|
+
:return:
|
185
|
+
"""
|
186
|
+
return any(
|
187
|
+
any(ratio(author1, author2, score_cutoff=.8) for author2 in a2)
|
188
|
+
for author1 in a1
|
189
|
+
)
|
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,
|
@@ -28,12 +28,6 @@ from tbr_deal_finder.utils import (
|
|
28
28
|
def cli():
|
29
29
|
make_migrations()
|
30
30
|
|
31
|
-
# Check that the config exists for all commands ran
|
32
|
-
try:
|
33
|
-
Config.load()
|
34
|
-
except FileNotFoundError:
|
35
|
-
_set_config()
|
36
|
-
|
37
31
|
|
38
32
|
def _add_path(existing_paths: list[str]) -> Union[str, None]:
|
39
33
|
try:
|
@@ -68,24 +62,26 @@ def _set_library_export_paths(config: Config):
|
|
68
62
|
Ensures that only valid, unique paths are added. Updates the config in-place.
|
69
63
|
"""
|
70
64
|
while True:
|
71
|
-
if config.library_export_paths:
|
72
|
-
|
73
|
-
choices = ["Add new path", "Remove path", "Done"]
|
74
|
-
else:
|
75
|
-
choices = ["Add new path", "Done"]
|
76
|
-
|
77
|
-
try:
|
78
|
-
user_selection = questionary.select(
|
79
|
-
"What change would you like to make to your library export paths",
|
80
|
-
choices=choices,
|
81
|
-
).ask()
|
82
|
-
except (KeyError, KeyboardInterrupt, TypeError):
|
83
|
-
return
|
65
|
+
if len(config.library_export_paths) > 0:
|
66
|
+
choices = ["Add new path", "Remove path", "Done"]
|
84
67
|
else:
|
85
|
-
|
86
|
-
|
68
|
+
choices = ["Add new path", "Done"]
|
69
|
+
|
70
|
+
try:
|
71
|
+
user_selection = questionary.select(
|
72
|
+
"What change would you like to make to your library export paths",
|
73
|
+
choices=choices,
|
74
|
+
).ask()
|
75
|
+
except (KeyError, KeyboardInterrupt, TypeError):
|
76
|
+
return
|
87
77
|
|
88
78
|
if user_selection == "Done":
|
79
|
+
if not config.library_export_paths:
|
80
|
+
if not click.confirm(
|
81
|
+
"Don't add a GoodReads or StoryGraph export and use wishlist entirely? "
|
82
|
+
"Note: Wishlist checks will still work even if you add your StoryGraph/GoodReads export."
|
83
|
+
):
|
84
|
+
continue
|
89
85
|
return
|
90
86
|
elif user_selection == "Add new path":
|
91
87
|
if new_path := _add_path(config.library_export_paths):
|
@@ -184,13 +180,18 @@ def _set_config() -> Config:
|
|
184
180
|
def setup():
|
185
181
|
_set_config()
|
186
182
|
|
183
|
+
# Retailers may have changed causing some books to need reprocessing
|
184
|
+
config = Config.load()
|
185
|
+
reprocess_incomplete_tbr_books(config)
|
186
|
+
|
187
187
|
|
188
188
|
@cli.command()
|
189
189
|
def latest_deals():
|
190
190
|
"""Find book deals from your Library export."""
|
191
|
-
|
192
|
-
|
193
|
-
|
191
|
+
try:
|
192
|
+
config = Config.load()
|
193
|
+
except FileNotFoundError:
|
194
|
+
config = _set_config()
|
194
195
|
|
195
196
|
db_conn = get_duckdb_conn()
|
196
197
|
results = execute_query(
|
tbr_deal_finder/config.py
CHANGED
@@ -62,10 +62,16 @@ class Config:
|
|
62
62
|
tracked_retailers_str = parser.get('DEFAULT', 'tracked_retailers')
|
63
63
|
locale = parser.get('DEFAULT', 'locale', fallback="us")
|
64
64
|
cls.set_locale(locale)
|
65
|
+
|
66
|
+
if export_paths_str:
|
67
|
+
library_export_paths = [i.strip() for i in export_paths_str.split(",")]
|
68
|
+
else:
|
69
|
+
library_export_paths = []
|
70
|
+
|
65
71
|
return cls(
|
66
72
|
max_price=parser.getfloat('DEFAULT', 'max_price', fallback=8.0),
|
67
73
|
min_discount=parser.getint('DEFAULT', 'min_discount', fallback=35),
|
68
|
-
library_export_paths=
|
74
|
+
library_export_paths=library_export_paths,
|
69
75
|
tracked_retailers=[i.strip() for i in tracked_retailers_str.split(",")]
|
70
76
|
)
|
71
77
|
|
@@ -77,8 +83,20 @@ class Config:
|
|
77
83
|
def tracked_retailers_str(self) -> str:
|
78
84
|
return ", ".join(self.tracked_retailers)
|
79
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
|
+
|
80
96
|
def set_library_export_paths(self, library_export_paths: Union[str, list[str]]):
|
81
|
-
if
|
97
|
+
if not library_export_paths:
|
98
|
+
self.library_export_paths = []
|
99
|
+
elif isinstance(library_export_paths, str):
|
82
100
|
self.library_export_paths = [i.strip() for i in library_export_paths.split(",")]
|
83
101
|
else:
|
84
102
|
self.library_export_paths = library_export_paths
|
tbr_deal_finder/migrations.py
CHANGED
@@ -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
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from tbr_deal_finder.book import Book
|
2
|
+
from tbr_deal_finder.config import Config
|
3
|
+
from tbr_deal_finder.retailer import RETAILER_MAP
|
4
|
+
from tbr_deal_finder.retailer.models import Retailer
|
5
|
+
|
6
|
+
|
7
|
+
async def get_owned_books(config: Config) -> list[Book]:
|
8
|
+
owned_books = []
|
9
|
+
|
10
|
+
for retailer_str in config.tracked_retailers:
|
11
|
+
retailer: Retailer = RETAILER_MAP[retailer_str]()
|
12
|
+
await retailer.set_auth()
|
13
|
+
|
14
|
+
owned_books.extend(
|
15
|
+
await retailer.get_library(config)
|
16
|
+
)
|
17
|
+
|
18
|
+
return owned_books
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import os.path
|
2
|
+
|
3
|
+
import audible
|
4
|
+
import click
|
5
|
+
from audible.login import build_init_cookies
|
6
|
+
from textwrap import dedent
|
7
|
+
|
8
|
+
from tbr_deal_finder import TBR_DEALS_PATH
|
9
|
+
from tbr_deal_finder.config import Config
|
10
|
+
from tbr_deal_finder.retailer.models import Retailer
|
11
|
+
|
12
|
+
_AUTH_PATH = TBR_DEALS_PATH.joinpath("audible.json")
|
13
|
+
|
14
|
+
|
15
|
+
def login_url_callback(url: str) -> str:
|
16
|
+
"""Helper function for login with external browsers."""
|
17
|
+
|
18
|
+
try:
|
19
|
+
from playwright.sync_api import sync_playwright # type: ignore
|
20
|
+
except ImportError:
|
21
|
+
pass
|
22
|
+
else:
|
23
|
+
with sync_playwright() as p:
|
24
|
+
iphone = p.devices["iPhone 12 Pro"]
|
25
|
+
browser = p.webkit.launch(headless=False)
|
26
|
+
context = browser.new_context(
|
27
|
+
**iphone
|
28
|
+
)
|
29
|
+
cookies = []
|
30
|
+
for name, value in build_init_cookies().items():
|
31
|
+
cookies.append(
|
32
|
+
{
|
33
|
+
"name": name,
|
34
|
+
"value": value,
|
35
|
+
"url": url
|
36
|
+
}
|
37
|
+
)
|
38
|
+
context.add_cookies(cookies)
|
39
|
+
page = browser.new_page()
|
40
|
+
page.goto(url)
|
41
|
+
|
42
|
+
while True:
|
43
|
+
page.wait_for_timeout(600)
|
44
|
+
if "/ap/maplanding" in page.url:
|
45
|
+
response_url = page.url
|
46
|
+
break
|
47
|
+
|
48
|
+
browser.close()
|
49
|
+
return response_url
|
50
|
+
|
51
|
+
message = f"""\
|
52
|
+
Please copy the following url and insert it into a web browser of your choice to log into Amazon.
|
53
|
+
Note: your browser will show you an error page (Page not found). This is expected.
|
54
|
+
|
55
|
+
{url}
|
56
|
+
|
57
|
+
Once you have logged in, please insert the copied url.
|
58
|
+
"""
|
59
|
+
click.echo(dedent(message))
|
60
|
+
return input()
|
61
|
+
|
62
|
+
|
63
|
+
class Amazon(Retailer):
|
64
|
+
_auth: audible.Authenticator = None
|
65
|
+
_client: audible.AsyncClient = None
|
66
|
+
|
67
|
+
async def set_auth(self):
|
68
|
+
if not os.path.exists(_AUTH_PATH):
|
69
|
+
auth = audible.Authenticator.from_login_external(
|
70
|
+
locale=Config.locale,
|
71
|
+
login_url_callback=login_url_callback
|
72
|
+
)
|
73
|
+
|
74
|
+
# Save credentials to file
|
75
|
+
auth.to_file(_AUTH_PATH)
|
76
|
+
|
77
|
+
self._auth = audible.Authenticator.from_file(_AUTH_PATH)
|
78
|
+
self._client = audible.AsyncClient(auth=self._auth)
|
79
|
+
|
@@ -1,74 +1,13 @@
|
|
1
1
|
import asyncio
|
2
2
|
import math
|
3
|
-
import os.path
|
4
|
-
from datetime import datetime
|
5
|
-
from textwrap import dedent
|
6
3
|
import readline # type: ignore
|
7
4
|
|
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
5
|
from tbr_deal_finder.config import Config
|
15
|
-
from tbr_deal_finder.retailer.
|
6
|
+
from tbr_deal_finder.retailer.amazon import Amazon
|
16
7
|
from tbr_deal_finder.book import Book, BookFormat
|
17
8
|
|
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
9
|
|
69
|
-
class Audible(
|
70
|
-
_auth: audible.Authenticator = None
|
71
|
-
_client: audible.AsyncClient = None
|
10
|
+
class Audible(Amazon):
|
72
11
|
|
73
12
|
@property
|
74
13
|
def name(self) -> str:
|
@@ -78,23 +17,9 @@ class Audible(Retailer):
|
|
78
17
|
def format(self) -> BookFormat:
|
79
18
|
return BookFormat.AUDIOBOOK
|
80
19
|
|
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
20
|
async def get_book(
|
95
21
|
self,
|
96
22
|
target: Book,
|
97
|
-
runtime: datetime,
|
98
23
|
semaphore: asyncio.Semaphore
|
99
24
|
) -> Book:
|
100
25
|
title = target.title
|
@@ -114,27 +39,16 @@ class Audible(Retailer):
|
|
114
39
|
for product in match.get("products", []):
|
115
40
|
if product["title"] != title:
|
116
41
|
continue
|
42
|
+
try:
|
43
|
+
target.list_price = product["price"]["list_price"]["base"]
|
44
|
+
target.current_price = product["price"]["lowest_price"]["base"]
|
45
|
+
target.exists = True
|
46
|
+
return target
|
47
|
+
except KeyError:
|
48
|
+
continue
|
117
49
|
|
118
|
-
|
119
|
-
|
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
|
-
)
|
50
|
+
target.exists = False
|
51
|
+
return target
|
138
52
|
|
139
53
|
async def get_wishlist(self, config: Config) -> list[Book]:
|
140
54
|
wishlist_books = []
|
@@ -171,3 +85,39 @@ class Audible(Retailer):
|
|
171
85
|
total_pages = math.ceil(int(response.get("total_results", 1))/page_size)
|
172
86
|
|
173
87
|
return wishlist_books
|
88
|
+
|
89
|
+
async def get_library(self, config: Config) -> list[Book]:
|
90
|
+
library_books = []
|
91
|
+
|
92
|
+
page = 1
|
93
|
+
total_pages = 1
|
94
|
+
page_size = 1000
|
95
|
+
while page <= total_pages:
|
96
|
+
response = await self._client.get(
|
97
|
+
"1.0/library",
|
98
|
+
num_results=page_size,
|
99
|
+
page=page,
|
100
|
+
response_groups=[
|
101
|
+
"contributors, product_attrs, product_desc, product_extended_attrs"
|
102
|
+
]
|
103
|
+
)
|
104
|
+
|
105
|
+
for audiobook in response.get("items", []):
|
106
|
+
authors = [author["name"] for author in audiobook["authors"]]
|
107
|
+
library_books.append(
|
108
|
+
Book(
|
109
|
+
retailer=self.name,
|
110
|
+
title=audiobook["title"],
|
111
|
+
authors=", ".join(authors),
|
112
|
+
list_price=1,
|
113
|
+
current_price=1,
|
114
|
+
timepoint=config.run_time,
|
115
|
+
format=self.format,
|
116
|
+
audiobook_isbn=audiobook["isbn"],
|
117
|
+
)
|
118
|
+
)
|
119
|
+
|
120
|
+
page += 1
|
121
|
+
total_pages = math.ceil(int(response.get("total_results", 1))/page_size)
|
122
|
+
|
123
|
+
return library_books
|