tbr-deal-finder 0.1.6__tar.gz → 0.1.8__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 (33) hide show
  1. tbr_deal_finder-0.1.8/CHANGELOG.md +52 -0
  2. tbr_deal_finder-0.1.8/DESIGN.md +113 -0
  3. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/PKG-INFO +8 -6
  4. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/README.md +6 -5
  5. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/pyproject.toml +2 -1
  6. tbr_deal_finder-0.1.8/tbr_deal_finder/book.py +189 -0
  7. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/tbr_deal_finder/cli.py +26 -25
  8. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/tbr_deal_finder/config.py +20 -2
  9. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/tbr_deal_finder/migrations.py +16 -0
  10. tbr_deal_finder-0.1.8/tbr_deal_finder/owned_books.py +18 -0
  11. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/tbr_deal_finder/queries/get_active_deals.sql +1 -1
  12. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/tbr_deal_finder/queries/get_deals_found_at.sql +1 -1
  13. tbr_deal_finder-0.1.8/tbr_deal_finder/retailer/amazon.py +79 -0
  14. tbr_deal_finder-0.1.8/tbr_deal_finder/retailer/audible.py +123 -0
  15. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/tbr_deal_finder/retailer/chirp.py +69 -33
  16. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/tbr_deal_finder/retailer/librofm.py +44 -40
  17. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/tbr_deal_finder/retailer/models.py +3 -2
  18. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/tbr_deal_finder/retailer_deal.py +37 -34
  19. tbr_deal_finder-0.1.8/tbr_deal_finder/tracked_books.py +320 -0
  20. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/uv.lock +52 -1
  21. tbr_deal_finder-0.1.6/CHANGELOG.md +0 -21
  22. tbr_deal_finder-0.1.6/tbr_deal_finder/book.py +0 -129
  23. tbr_deal_finder-0.1.6/tbr_deal_finder/library_exports.py +0 -205
  24. tbr_deal_finder-0.1.6/tbr_deal_finder/retailer/audible.py +0 -173
  25. tbr_deal_finder-0.1.6/tbr_deal_finder/tracked_books.py +0 -120
  26. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/.github/workflows/publish-to-pypi.yaml +0 -0
  27. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/.gitignore +0 -0
  28. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/.python-version +0 -0
  29. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/LICENSE +0 -0
  30. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/tbr_deal_finder/__init__.py +0 -0
  31. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/tbr_deal_finder/queries/latest_deal_last_ran_most_recent_success.sql +0 -0
  32. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/tbr_deal_finder/retailer/__init__.py +0 -0
  33. {tbr_deal_finder-0.1.6 → tbr_deal_finder-0.1.8}/tbr_deal_finder/utils.py +0 -0
@@ -0,0 +1,52 @@
1
+
2
+ # Change Log
3
+
4
+ ---
5
+
6
+ ## 0.1.8 (August 13, 2025)
7
+
8
+ Notes:
9
+ * Improved performance for tracking on libro
10
+ * Preparing EBook support
11
+
12
+ BUG FIXES:
13
+ * Fixed initial login issue in libro.fm
14
+
15
+ ---
16
+
17
+ ## 0.1.7 (July 31, 2025)
18
+
19
+ Notes:
20
+ * tbr-deal-finder no longer shows deals on books you own in the same format.
21
+ * Example: You own Dune on Audible so it won't show on Audible, Libro, or Chirp. It will show on Kindle (you don't own the ebook)
22
+ * Improvements when attempting to match authors
23
+ * Chirp
24
+ * Libro.FM
25
+ * Users no longer need to provide an export and can instead just track deals on their wishlist
26
+
27
+ BUG FIXES:
28
+ * Fixed wishlist pagination in libro.fm
29
+ * Fixed issue forcing user to go through setup twice when running the setup command
30
+
31
+ ---
32
+
33
+ ## 0.1.6 (July 30, 2025)
34
+
35
+ Notes:
36
+ * tbr-deal-finder now also tracks deals on the books in your wishlist. Works for all retailers.
37
+
38
+ BUG FIXES:
39
+ * Fixed issue where no deals would display if libro is the only tracked audiobook retailer.
40
+ * Fixed retailer cli setup forcing a user to select at least two audiobook retailers.
41
+
42
+ ---
43
+
44
+ ## 0.1.5 (July 30, 2025)
45
+
46
+ Notes:
47
+ * Added formatting to select messages to make the messages purpose clearer.
48
+
49
+ BUG FIXES:
50
+ * Fixed issue getting books from libro and chirp too aggressively
51
+ * User must now track deals for at least one retailer
52
+
@@ -0,0 +1,113 @@
1
+ # TBR Deal Finder Design
2
+
3
+ ## Overview
4
+ TBR Deal Finder is a CLI application that helps users track price drops and find deals on books in their To-Be-Read (TBR) list across various digital book retailers. The application is built with a modular architecture that separates data ingestion, processing, and output concerns.
5
+
6
+ ## Terms
7
+ ### Digital Book
8
+ A book in electronic format, accessible via digital devices. Includes:
9
+ - Audiobooks
10
+ - E-books
11
+
12
+ ### Digital Book Retailer
13
+ Platforms that sell or distribute digital books, such as:
14
+ - Libro.fm (audiobooks)
15
+ - Audible (audiobooks)
16
+ - Kindle (e-books)
17
+ - Chirp (discounted audiobooks)
18
+
19
+ ### Library Export
20
+ A data file containing a user's reading list and status. Sources include:
21
+ - **Automated Exports**:
22
+ - The StoryGraph
23
+ - Goodreads
24
+ - **Manual CSVs**: Custom spreadsheets following the required format
25
+
26
+ ### TBR (To Be Read)
27
+ A user's personal reading list of books they plan to read in the future.
28
+
29
+ ## Core Components
30
+
31
+ ### 1. Data Ingestion Layer
32
+ - **Library Exports**: Handles importing book data from multiple sources:
33
+ - The StoryGraph exports
34
+ - Goodreads exports
35
+ - Custom CSV files
36
+ - **Retailer APIs**: Interfaces with various digital book retailers to fetch current pricing and availability
37
+
38
+ ### 2. Core Data Model
39
+
40
+ #### `Book` Class
41
+ The central data structure that represents a book across the application:
42
+ - **Purpose**: Serves as a consistent contract between different components
43
+ - **Key Attributes**:
44
+ - Title and author information
45
+ - Format (Audiobook, Ebook, etc.)
46
+ - Pricing information (list price, current price)
47
+ - Retailer-specific metadata
48
+ - Timestamp for deal tracking
49
+
50
+ ### 3. Retailer Interface
51
+
52
+ #### `Retailer` Base Class
53
+ Abstract base class that defines the interface for all retailer implementations:
54
+ - **Core Methods**:
55
+ - `get_book()`: Fetches book details from the retailer
56
+ - `get_wishlist()`: Retrieves the user's wishlist
57
+ - `get_library()`: Gets the user's purchased/owned books
58
+ - **Current Implementations**:
59
+ - Audible
60
+ - Libro.fm
61
+ - Chirp
62
+ - Kindle (planned)
63
+
64
+ ### 4. Processing Pipeline
65
+ 1. **Data Collection**:
66
+ - Load TBR lists from configured sources
67
+ - Fetch owned books from retailers
68
+ - Retrieve current deals and pricing
69
+
70
+ 2. **Matching Logic**:
71
+ - Matches TBR books with retailer inventory
72
+ - Filters out owned books
73
+ - Identifies price drops and deals
74
+
75
+ 3. **Output Generation**:
76
+ - Formats results for CLI display
77
+ - Highlights best deals
78
+ - Provides purchase links
79
+
80
+ ## Data Flow
81
+
82
+ ```mermaid
83
+ graph TD
84
+ A[Library Exports] -->|CSV/JSON| B[Book Objects]
85
+ C[Retailer APIs] -->|API Calls| B
86
+ B --> D[Deal Processing]
87
+ D --> E[CLI Output]
88
+ F[User Configuration] -->|Settings| C
89
+ F -->|Preferences| D
90
+ ```
91
+
92
+ ## Key Design Decisions
93
+
94
+ ### 1. Extensibility
95
+ - Retailer implementations are pluggable through the `Retailer` base class
96
+ - Support for multiple export formats and retailer APIs
97
+
98
+ ### 2. Performance
99
+ - Asynchronous I/O for API calls
100
+ - Caching of retailer responses
101
+ - Efficient data structures for book matching
102
+
103
+ ### 3. User Experience
104
+ - Clear, concise command-line interface
105
+ - Configurable output formats
106
+ - Progress indicators for long-running operations
107
+
108
+ ## Future Considerations
109
+ - Support for more retailer APIs
110
+ - Email notifications for price drops
111
+ - Web interface for easier configuration
112
+ - Integration with library lending services
113
+ - Price history tracking
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tbr-deal-finder
3
- Version: 0.1.6
3
+ Version: 0.1.8
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
@@ -9,6 +9,7 @@ Requires-Dist: aiohttp>=3.12.14
9
9
  Requires-Dist: audible==0.8.2
10
10
  Requires-Dist: click>=8.2.1
11
11
  Requires-Dist: duckdb>=1.3.2
12
+ Requires-Dist: levenshtein>=0.27.1
12
13
  Requires-Dist: pandas>=2.3.1
13
14
  Requires-Dist: questionary>=2.1.0
14
15
  Requires-Dist: tqdm>=4.67.1
@@ -17,16 +18,17 @@ Description-Content-Type: text/markdown
17
18
 
18
19
  # tbr-deal-finder
19
20
 
20
- Track price drops and find deals on books in your TBR (To Be Read) list across audiobook and ebook formats.
21
+ Track price drops and find deals on books in your TBR (To Be Read) and wishlist across digital book retailers.
21
22
 
22
23
  ## Features
23
- - Uses your StoryGraph exports, Goodreads exports, and custom csvs (spreadsheet) to track book deals
24
+ - Use your StoryGraph exports, Goodreads exports, and custom csvs (spreadsheet) to track book deals
24
25
  - Supports multiple of the library exports above
25
26
  - Tracks deals on the wishlist of all your configured retailers like audible
26
27
  - Supports multiple locales and currencies
27
- - Finds the latest and active deals from supported sellers
28
+ - Find the latest and active deals from supported sellers
28
29
  - Simple CLI interface for setup and usage
29
30
  - Only get notified for new deals or view all active deals
31
+ - Filters out books you already own to prevent purchasing the same book on multiple retailers
30
32
 
31
33
  ## Support
32
34
 
@@ -54,7 +56,7 @@ Track price drops and find deals on books in your TBR (To Be Read) list across a
54
56
  1. If it's not already on your computer, download Python https://www.python.org/downloads/
55
57
  1. tbr-deal-finder requires Python3.13 or higher
56
58
  2. Optional: Install and use virtualenv
57
- 3. Open your Terminal/Commmand Prompt
59
+ 3. Open your Terminal/Command Prompt
58
60
  4. Run `pip3.13 install tbr-deal-finder`
59
61
 
60
62
  ### UV
@@ -67,7 +69,7 @@ Track price drops and find deals on books in your TBR (To Be Read) list across a
67
69
  https://docs.astral.sh/uv/getting-started/installation/
68
70
 
69
71
  ## Configuration
70
- This tool relies on the csv generated by the app you use to track your TBRs.
72
+ This tool can use the csv generated by the app you use to track your TBRs.
71
73
  Here are the steps to get your export.
72
74
 
73
75
  ### StoryGraph
@@ -1,15 +1,16 @@
1
1
  # tbr-deal-finder
2
2
 
3
- Track price drops and find deals on books in your TBR (To Be Read) list across audiobook and ebook formats.
3
+ Track price drops and find deals on books in your TBR (To Be Read) and wishlist across digital book retailers.
4
4
 
5
5
  ## Features
6
- - Uses your StoryGraph exports, Goodreads exports, and custom csvs (spreadsheet) to track book deals
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
8
  - Tracks deals on the wishlist of all your configured retailers like audible
9
9
  - Supports multiple locales and currencies
10
- - Finds the latest and active deals from supported sellers
10
+ - Find the latest and active deals from supported sellers
11
11
  - Simple CLI interface for setup and usage
12
12
  - Only get notified for new deals or view all active deals
13
+ - Filters out books you already own to prevent purchasing the same book on multiple retailers
13
14
 
14
15
  ## Support
15
16
 
@@ -37,7 +38,7 @@ Track price drops and find deals on books in your TBR (To Be Read) list across a
37
38
  1. If it's not already on your computer, download Python https://www.python.org/downloads/
38
39
  1. tbr-deal-finder requires Python3.13 or higher
39
40
  2. Optional: Install and use virtualenv
40
- 3. Open your Terminal/Commmand Prompt
41
+ 3. Open your Terminal/Command Prompt
41
42
  4. Run `pip3.13 install tbr-deal-finder`
42
43
 
43
44
  ### UV
@@ -50,7 +51,7 @@ Track price drops and find deals on books in your TBR (To Be Read) list across a
50
51
  https://docs.astral.sh/uv/getting-started/installation/
51
52
 
52
53
  ## Configuration
53
- This tool relies on the csv generated by the app you use to track your TBRs.
54
+ This tool can use the csv generated by the app you use to track your TBRs.
54
55
  Here are the steps to get your export.
55
56
 
56
57
  ### StoryGraph
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tbr-deal-finder"
3
- version = "0.1.6"
3
+ version = "0.1.8"
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"
@@ -10,6 +10,7 @@ dependencies = [
10
10
  "audible==0.8.2",
11
11
  "click>=8.2.1",
12
12
  "duckdb>=1.3.2",
13
+ "levenshtein>=0.27.1",
13
14
  "pandas>=2.3.1",
14
15
  "questionary>=2.1.0",
15
16
  "tqdm>=4.67.1",
@@ -0,0 +1,189 @@
1
+ import re
2
+ from datetime import datetime
3
+ from enum import Enum
4
+ from typing import Union
5
+
6
+ import click
7
+ from Levenshtein import ratio
8
+ from unidecode import unidecode
9
+
10
+ from tbr_deal_finder.config import Config
11
+ from tbr_deal_finder.utils import get_duckdb_conn, execute_query, get_query_by_name
12
+
13
+ _AUTHOR_RE = re.compile(r'[^a-zA-Z0-9]')
14
+
15
+ class BookFormat(Enum):
16
+ AUDIOBOOK = "Audiobook"
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
56
+
57
+ def discount(self) -> int:
58
+ return int((self.list_price/self.current_price - 1) * 100)
59
+
60
+ @staticmethod
61
+ def price_to_string(price: float) -> str:
62
+ return f"{Config.currency_symbol()}{price:.2f}"
63
+
64
+ @property
65
+ def deal_id(self) -> str:
66
+ return f"{self.title}__{self.normalized_authors}__{self.format}__{self.retailer}"
67
+
68
+ @property
69
+ def title_id(self) -> str:
70
+ return f"{self.title}__{self.normalized_authors}__{self.format}"
71
+
72
+ @property
73
+ def full_title_str(self) -> str:
74
+ return f"{self.title}__{self.normalized_authors}"
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
+
92
+ def list_price_string(self):
93
+ return self.price_to_string(self.list_price)
94
+
95
+ def current_price_string(self):
96
+ return self.price_to_string(self.current_price)
97
+
98
+ def __str__(self) -> str:
99
+ price = self.current_price_string()
100
+ book_format = self.format.value
101
+ title = self.title
102
+ if len(self.title) > 75:
103
+ title = f"{title[:75]}..."
104
+ return f"{title} by {self.authors} - {price} - {self.discount()}% Off at {self.retailer}"
105
+
106
+ def dict(self):
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
+ }
129
+
130
+
131
+ def get_deals_found_at(timepoint: datetime) -> list[Book]:
132
+ db_conn = get_duckdb_conn()
133
+ query_response = execute_query(
134
+ db_conn,
135
+ get_query_by_name("get_deals_found_at.sql"),
136
+ {"timepoint": timepoint}
137
+ )
138
+ return [Book(**book) for book in query_response]
139
+
140
+
141
+ def get_active_deals() -> list[Book]:
142
+ db_conn = get_duckdb_conn()
143
+ query_response = execute_query(
144
+ db_conn,
145
+ get_query_by_name("get_active_deals.sql")
146
+ )
147
+ return [Book(**book) for book in query_response]
148
+
149
+
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()
156
+
157
+ click.echo(str(book))
158
+
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
+
168
+ def get_normalized_authors(authors: Union[str, list[str]]) -> list[str]:
169
+ if isinstance(authors, str):
170
+ authors = [i for i in authors.split(",")]
171
+
172
+ return sorted([_AUTHOR_RE.sub('', unidecode(author)).lower() for author in authors])
173
+
174
+
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
+ )
@@ -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
- if len(config.library_export_paths) > 1:
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
- click.echo("Add your library export path.")
86
- user_selection = "Add new path"
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
- config = Config.load()
192
-
193
- asyncio.run(maybe_enrich_library_exports(config))
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(
@@ -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=[i.strip() for i in export_paths_str.split(",")],
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 isinstance(library_export_paths, str):
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
@@ -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
@@ -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