tbr-deal-finder 0.3.1__py3-none-any.whl → 0.3.3__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.
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
2
 
3
- __VERSION__ = "0.3.1"
3
+ __VERSION__ = "0.3.3"
4
4
 
5
5
  QUERY_PATH = Path(__file__).parent.joinpath("queries")
@@ -43,12 +43,16 @@ class DesktopUpdater:
43
43
  release_data = response.json()
44
44
  latest_version = release_data["tag_name"].lstrip("v")
45
45
  if version.parse(latest_version) > version.parse(self.current_version):
46
- release_url = release_data["html_url"]
46
+ download_url = release_data["html_url"]
47
+ if self.platform == "darwin":
48
+ for asset in release_data["assets"]:
49
+ if asset["browser_download_url"].endswith(".dmg"):
50
+ download_url = asset["browser_download_url"]
51
+
47
52
  return {
48
53
  "version": latest_version,
49
- "download_url": release_url,
54
+ "download_url": download_url,
50
55
  "release_notes": release_data.get("body", ""),
51
- "release_url": release_url
52
56
  }
53
57
 
54
58
  return None
@@ -56,86 +60,6 @@ class DesktopUpdater:
56
60
  except Exception as e:
57
61
  logger.error(f"Failed to check updates for {self.github_repo}: {e}")
58
62
  return None
59
-
60
- def download_update(self, download_url: str, progress_callback=None) -> Optional[Path]:
61
- """
62
- Download the update file.
63
- Returns path to downloaded file or None if failed.
64
- """
65
- try:
66
- response = requests.get(download_url, stream=True, timeout=30)
67
- response.raise_for_status()
68
-
69
- # Determine file extension
70
- filename = download_url.split("/")[-1]
71
- temp_file = Path(tempfile.gettempdir()) / filename
72
-
73
- total_size = int(response.headers.get('content-length', 0))
74
- downloaded = 0
75
-
76
- with open(temp_file, 'wb') as f:
77
- for chunk in response.iter_content(chunk_size=8192):
78
- if chunk:
79
- f.write(chunk)
80
- downloaded += len(chunk)
81
-
82
- if progress_callback and total_size > 0:
83
- progress = (downloaded / total_size) * 100
84
- progress_callback(progress)
85
-
86
- return temp_file
87
-
88
- except Exception as e:
89
- logger.error(f"Failed to download update: {e}")
90
- return None
91
-
92
- def install_update(self, update_file: Path) -> bool:
93
- """
94
- Install the downloaded update.
95
- Platform-specific installation logic.
96
- """
97
- if self.platform == "darwin":
98
- return self._install_macos_update(update_file)
99
- elif self.platform == "windows":
100
- return self._install_windows_update(update_file)
101
- elif self.platform == "linux":
102
- return self._install_linux_update(update_file)
103
- else:
104
- return False
105
-
106
- def _install_macos_update(self, dmg_file: Path) -> bool:
107
- """Install .dmg update on macOS."""
108
- try:
109
- # Open the DMG file - user will need to drag to Applications
110
- subprocess.run(["open", str(dmg_file)], check=True)
111
- return True
112
- except subprocess.CalledProcessError:
113
- return False
114
-
115
- def _install_windows_update(self, exe_file: Path) -> bool:
116
- """Install .exe update on Windows."""
117
- try:
118
- # Run the installer
119
- subprocess.run([str(exe_file)], check=True)
120
- return True
121
- except subprocess.CalledProcessError:
122
- return False
123
-
124
- def _install_linux_update(self, appimage_file: Path) -> bool:
125
- """Install AppImage update on Linux."""
126
- try:
127
- # Make executable and offer to replace current installation
128
- os.chmod(appimage_file, 0o755)
129
-
130
- # For AppImage, we'd typically replace the current file
131
- # This is more complex and might require user permission
132
- return True
133
- except Exception:
134
- return False
135
-
136
- def open_download_page(self, release_url: str):
137
- """Open the GitHub release page in browser."""
138
- webbrowser.open(release_url)
139
63
 
140
64
 
141
65
  # Global instance
@@ -1,6 +1,10 @@
1
1
  import asyncio
2
2
  import os
3
3
  import base64
4
+ import subprocess
5
+ import sys
6
+ from datetime import datetime
7
+ from pathlib import Path
4
8
 
5
9
  import flet as ft
6
10
 
@@ -17,6 +21,7 @@ from tbr_deal_finder.gui.pages.all_deals import AllDealsPage
17
21
  from tbr_deal_finder.gui.pages.latest_deals import LatestDealsPage
18
22
  from tbr_deal_finder.gui.pages.all_books import AllBooksPage
19
23
  from tbr_deal_finder.gui.pages.book_details import BookDetailsPage
24
+ from tbr_deal_finder.utils import get_duckdb_conn, get_latest_deal_last_ran
20
25
 
21
26
 
22
27
  class TBRDealFinderApp:
@@ -26,7 +31,9 @@ class TBRDealFinderApp:
26
31
  self.current_page = "all_deals"
27
32
  self.selected_book = None
28
33
  self.update_info = None # Store update information
29
-
34
+ self.nav_disabled = False # Track navigation disabled state
35
+ self._last_run_time = None
36
+
30
37
  # Initialize pages
31
38
  self.settings_page = SettingsPage(self)
32
39
  self.all_deals_page = AllDealsPage(self)
@@ -258,6 +265,16 @@ class TBRDealFinderApp:
258
265
 
259
266
  def nav_changed(self, e):
260
267
  """Handle navigation rail selection changes"""
268
+ # Prevent navigation if disabled
269
+ if self.nav_disabled:
270
+ # Reset to current page selection to prevent visual change
271
+ current_indices = {"all_deals": 0, "latest_deals": 1, "all_books": 2}
272
+ self.nav_rail.selected_index = current_indices.get(self.current_page, 0)
273
+ # Reapply disabled state after page update
274
+ self.nav_rail.disabled = True
275
+ self.page.update()
276
+ return
277
+
261
278
  destinations = ["all_deals", "latest_deals", "all_books"]
262
279
  if e.control.selected_index < len(destinations):
263
280
  self.current_page = destinations[e.control.selected_index]
@@ -284,10 +301,26 @@ class TBRDealFinderApp:
284
301
  self.all_books_page.refresh_page_state()
285
302
 
286
303
  def refresh_all_pages(self):
287
- """Refresh all pages by clearing their state and reloading data"""
304
+ """Refresh all pages except all_books_page by clearing their state and reloading data"""
288
305
  self.all_deals_page.refresh_page_state()
289
306
  self.latest_deals_page.refresh_page_state()
290
- self.all_books_page.refresh_page_state()
307
+
308
+ def disable_navigation(self):
309
+ """Disable navigation rail during background operations"""
310
+ self.nav_disabled = True
311
+ if hasattr(self, 'nav_rail'):
312
+ self.nav_rail.disabled = True
313
+ self.page.update()
314
+
315
+ def enable_navigation(self):
316
+ """Enable navigation rail after background operations complete"""
317
+ if not self.nav_disabled:
318
+ return
319
+
320
+ self.nav_disabled = False
321
+ if hasattr(self, 'nav_rail'):
322
+ self.nav_rail.disabled = False
323
+ self.page.update()
291
324
 
292
325
  def get_current_page_content(self):
293
326
  """Get content for the current page"""
@@ -309,6 +342,7 @@ class TBRDealFinderApp:
309
342
 
310
343
  def get_config_prompt(self):
311
344
  """Show config setup prompt when no config exists"""
345
+ self.disable_navigation()
312
346
  return ft.Container(
313
347
  content=ft.Column([
314
348
  ft.Icon(ft.Icons.SETTINGS, size=64, color=ft.Colors.GREY_400),
@@ -592,10 +626,6 @@ class TBRDealFinderApp:
592
626
  def close_dialog(e):
593
627
  dialog.open = False
594
628
  self.page.update()
595
-
596
- def view_release_and_close(e):
597
- self.view_release_notes(e)
598
- close_dialog(e)
599
629
 
600
630
  def download_and_close(e):
601
631
  self.download_update(e)
@@ -609,10 +639,13 @@ class TBRDealFinderApp:
609
639
  ], spacing=10),
610
640
  content=ft.Column([
611
641
  ft.Text(f"Version {self.update_info['version']} is now available!"),
612
- ft.Text("Would you like to download the update?", color=ft.Colors.GREY_600)
613
- ], spacing=10, tight=True),
642
+ ft.Divider(),
643
+ ft.Text(
644
+ self.update_info.get('release_notes', 'No release notes available.'),
645
+ selectable=True
646
+ ),
647
+ ], scroll=ft.ScrollMode.AUTO, spacing=10, tight=True),
614
648
  actions=[
615
- ft.TextButton("View Release Notes", on_click=view_release_and_close),
616
649
  ft.ElevatedButton("Download Update", on_click=download_and_close),
617
650
  ft.TextButton("Later", on_click=close_dialog),
618
651
  ],
@@ -646,24 +679,36 @@ class TBRDealFinderApp:
646
679
  dialog.open = True
647
680
  self.page.update()
648
681
 
649
- def view_release_notes(self, e):
650
- """Open release notes in browser."""
651
- if self.update_info:
652
- import webbrowser
653
- webbrowser.open(self.update_info['release_url'])
654
-
655
682
  def download_update(self, e):
656
683
  """Handle update download."""
657
684
  if not self.update_info or not self.update_info.get('download_url'):
658
685
  return
659
-
660
- # For now, open download URL in browser
661
- # In a more advanced implementation, you could download in-app
662
- import webbrowser
663
- webbrowser.open(self.update_info['download_url'])
664
-
665
- # Show download instructions
666
- self.show_download_instructions()
686
+
687
+ if sys.platform == "darwin":
688
+ dmg_path = Path(
689
+ f"~/Downloads/TBR-Deal-Finder-{self.update_info['version']}-mac.dmg"
690
+ ).expanduser()
691
+
692
+ # Show download instructions
693
+ self.show_download_instructions()
694
+
695
+ if not dmg_path.exists():
696
+ # Using curl or urllib to download to prevent Mac warning
697
+ subprocess.run([
698
+ "curl", "-L",
699
+ self.update_info['download_url'],
700
+ "-o", dmg_path
701
+ ])
702
+
703
+ subprocess.run(["open", dmg_path])
704
+ else:
705
+ # For now, open download URL in browser
706
+ # In a more advanced implementation, you could download in-app
707
+ import webbrowser
708
+ webbrowser.open(self.update_info['download_url'])
709
+
710
+ # Show download instructions
711
+ self.show_download_instructions()
667
712
 
668
713
  def show_download_instructions(self):
669
714
  """Show instructions for installing the downloaded update."""
@@ -672,7 +717,7 @@ class TBRDealFinderApp:
672
717
  self.page.update()
673
718
 
674
719
  instructions = {
675
- "darwin": "1. Download will start in your browser\n2. Open the downloaded .dmg file\n3. Drag the app to Applications folder\n4. Restart the application",
720
+ "darwin": "1. Wait for the update to download (can take a minute or 2)\n2. Close TBR Deal Finder\n3. Once the installer opens, drag the app to Applications folder\n3. When prompted, select Replace\n4. Restart the application",
676
721
  "windows": "1. Download will start in your browser\n2. Run the downloaded .exe installer\n3. Follow the installation wizard\n4. Restart the application",
677
722
  }
678
723
 
@@ -703,12 +748,20 @@ class TBRDealFinderApp:
703
748
  dialog.open = True
704
749
  self.page.update()
705
750
 
706
-
707
-
708
751
  def check_for_updates_button(self):
709
752
  """Check for updates when button is clicked."""
710
753
  self.check_for_updates_manual()
711
754
 
755
+ def update_last_run_time(self):
756
+ db_conn = get_duckdb_conn()
757
+ self._last_run_time = get_latest_deal_last_ran(db_conn)
758
+
759
+ def get_last_run_time(self) -> datetime:
760
+ if not self._last_run_time:
761
+ self.update_last_run_time()
762
+
763
+ return self._last_run_time
764
+
712
765
 
713
766
  def main():
714
767
  """Main entry point for the GUI application"""
@@ -39,6 +39,9 @@ class AllBooksPage(BaseBookPage):
39
39
 
40
40
  async def _async_load_items(self):
41
41
  """Load TBR books asynchronously using Flet's async support"""
42
+ # Disable navigation during the loading operation
43
+ self.app.disable_navigation()
44
+
42
45
  try:
43
46
  # Run the async operation directly
44
47
  await self.app.auth_all_configured_retailers()
@@ -50,6 +53,10 @@ class AllBooksPage(BaseBookPage):
50
53
  self.filtered_items = []
51
54
  finally:
52
55
  self.set_loading(False)
56
+
57
+ # Re-enable navigation after the operation completes
58
+ self.app.enable_navigation()
59
+
53
60
  # Update the page to reflect the loaded data
54
61
  self.app.page.update()
55
62
 
@@ -142,8 +142,7 @@ class BaseBookPage(ABC):
142
142
  item_tiles.append(tile)
143
143
 
144
144
  return ft.Container(
145
- content=ft.ListView(item_tiles, spacing=5),
146
- height=700,
145
+ content=ft.Column(item_tiles, spacing=5),
147
146
  border=ft.border.all(1, ft.Colors.OUTLINE),
148
147
  border_radius=8,
149
148
  padding=10
@@ -11,7 +11,7 @@ from tbr_deal_finder.utils import get_duckdb_conn, execute_query, float_to_curre
11
11
  logger = logging.getLogger(__name__)
12
12
 
13
13
 
14
- def build_book_price_section(historical_data: list[dict]) -> ft.Column:
14
+ def build_book_price_section(max_dt: datetime, historical_data: list[dict]) -> ft.Column:
15
15
  retailer_data = dict()
16
16
  available_colors = [
17
17
  ft.Colors.AMBER,
@@ -27,7 +27,6 @@ def build_book_price_section(historical_data: list[dict]) -> ft.Column:
27
27
  min_price = None
28
28
  max_price = None
29
29
  min_time = None
30
- max_dt = datetime.now()
31
30
  max_time = max_dt.timestamp()
32
31
 
33
32
  for record in historical_data:
@@ -461,7 +460,7 @@ class BookDetailsPage:
461
460
  )
462
461
 
463
462
  # Create the chart
464
- chart_fig = build_book_price_section(self.historical_data)
463
+ chart_fig = build_book_price_section(self.app.get_last_run_time(), self.historical_data)
465
464
 
466
465
  return ft.Container(
467
466
  content=ft.Column([
@@ -10,7 +10,6 @@ from tbr_deal_finder.gui.pages.base_book_page import BaseBookPage
10
10
  class LatestDealsPage(BaseBookPage):
11
11
  def __init__(self, app):
12
12
  super().__init__(app, 4)
13
- self.last_run_time = None
14
13
 
15
14
  def get_page_title(self) -> str:
16
15
  return "Latest Deals"
@@ -27,7 +26,7 @@ class LatestDealsPage(BaseBookPage):
27
26
 
28
27
  def build(self):
29
28
  """Build the latest deals page with custom header"""
30
- self.check_last_run()
29
+ self.app.update_last_run_time()
31
30
 
32
31
  # Custom header with run button
33
32
  header = self.build_header()
@@ -73,15 +72,16 @@ class LatestDealsPage(BaseBookPage):
73
72
  def build_header(self):
74
73
  """Build the header with run button and status"""
75
74
  can_run = self.can_run_latest_deals()
76
-
77
- if not can_run and self.last_run_time:
78
- next_run_time = self.last_run_time + timedelta(hours=8)
75
+ last_run_time = self.app.get_last_run_time()
76
+
77
+ if not can_run and last_run_time:
78
+ next_run_time = last_run_time + timedelta(hours=8)
79
79
  time_remaining = next_run_time - datetime.now()
80
80
  hours_remaining = max(0, int(time_remaining.total_seconds() / 3600))
81
81
  status_text = f"Next run available in {hours_remaining} hours"
82
82
  status_color = ft.Colors.ORANGE
83
- elif self.last_run_time:
84
- status_text = f"Last run: {self.last_run_time.strftime('%Y-%m-%d %H:%M')}"
83
+ elif last_run_time:
84
+ status_text = f"Last run: {last_run_time.strftime('%Y-%m-%d %H:%M')}"
85
85
  status_color = ft.Colors.GREEN
86
86
  else:
87
87
  status_text = "No previous runs"
@@ -194,11 +194,13 @@ class LatestDealsPage(BaseBookPage):
194
194
 
195
195
  def load_items(self):
196
196
  """Load deals found at the last run time"""
197
- if self.last_run_time:
197
+ last_run_time = self.app.get_last_run_time()
198
+
199
+ if last_run_time:
198
200
  try:
199
201
  self.items = [
200
202
  book
201
- for book in get_deals_found_at(self.last_run_time)
203
+ for book in get_deals_found_at(last_run_time)
202
204
  if is_qualifying_deal(self.app.config, book)
203
205
  ]
204
206
  self.apply_filters()
@@ -237,19 +239,15 @@ class LatestDealsPage(BaseBookPage):
237
239
  )
238
240
  )
239
241
 
240
- def check_last_run(self):
241
- """Check when deals were last run"""
242
- if not self.last_run_time:
243
- db_conn = get_duckdb_conn()
244
- self.last_run_time = get_latest_deal_last_ran(db_conn)
245
-
246
242
  def can_run_latest_deals(self) -> bool:
247
243
  """Check if latest deals can be run (8 hour cooldown)"""
248
- if not self.last_run_time:
244
+ last_run_time = self.app.get_last_run_time()
245
+
246
+ if not last_run_time:
249
247
  return True
250
248
 
251
249
  min_age = datetime.now() - timedelta(hours=8)
252
- return self.last_run_time < min_age
250
+ return last_run_time < min_age
253
251
 
254
252
  def run_latest_deals(self, e):
255
253
  """Run the latest deals check"""
@@ -274,6 +272,9 @@ class LatestDealsPage(BaseBookPage):
274
272
  self.progress_container.visible = True
275
273
  self.run_button.disabled = True
276
274
 
275
+ # Disable navigation during the operation
276
+ self.app.disable_navigation()
277
+
277
278
  # Update the page to show loading state
278
279
  self.app.page.update()
279
280
 
@@ -283,7 +284,7 @@ class LatestDealsPage(BaseBookPage):
283
284
 
284
285
  if success:
285
286
  # Update the run time and load new deals
286
- self.last_run_time = self.app.config.run_time
287
+ self.app.update_last_run_time()
287
288
  self.load_items()
288
289
  self.show_success(f"Found {len(self.items)} new deals!")
289
290
  else:
@@ -297,7 +298,10 @@ class LatestDealsPage(BaseBookPage):
297
298
  self.is_loading = False
298
299
  self.progress_container.visible = False
299
300
  self.run_button.disabled = False
300
- self.check_last_run() # Refresh the status
301
+ self.app.update_last_run_time() # Refresh the status
302
+
303
+ # Re-enable navigation after the operation
304
+ self.app.enable_navigation()
301
305
 
302
306
  # Update the page to reset loading state
303
307
  self.app.page.update()
@@ -366,5 +370,5 @@ class LatestDealsPage(BaseBookPage):
366
370
  self.format_dropdown.value = "All"
367
371
 
368
372
  # Check last run and reload data
369
- self.check_last_run()
373
+ self.app.update_last_run_time()
370
374
  self.load_items()
@@ -309,6 +309,7 @@ class SettingsPage:
309
309
  clear_unknown_books()
310
310
 
311
311
  self.show_success("Configuration saved successfully!")
312
+ self.app.enable_navigation()
312
313
  self.app.config_updated(self.config)
313
314
 
314
315
  except Exception as ex:
@@ -2,16 +2,14 @@ import asyncio
2
2
  import copy
3
3
  from collections import defaultdict
4
4
 
5
- import click
6
5
  import pandas as pd
7
- from tqdm.asyncio import tqdm_asyncio
8
6
 
9
7
  from tbr_deal_finder.book import Book, get_active_deals, BookFormat
10
8
  from tbr_deal_finder.config import Config
11
9
  from tbr_deal_finder.tracked_books import get_tbr_books, get_unknown_books, set_unknown_books
12
10
  from tbr_deal_finder.retailer import RETAILER_MAP
13
11
  from tbr_deal_finder.retailer.models import Retailer
14
- from tbr_deal_finder.utils import get_duckdb_conn, echo_info, echo_err, is_gui_env
12
+ from tbr_deal_finder.utils import get_duckdb_conn, echo_info, echo_err
15
13
 
16
14
 
17
15
  def update_retailer_deal_table(config: Config, new_deals: list[Book]):
@@ -67,6 +65,13 @@ async def _get_books(
67
65
  Returns:
68
66
  List of Book objects with updated pricing and availability
69
67
  """
68
+
69
+ echo_info(f"Getting deals from {retailer.name}")
70
+ books = _get_retailer_relevant_tbr_books(
71
+ retailer,
72
+ books,
73
+ )
74
+
70
75
  semaphore = asyncio.Semaphore(retailer.max_concurrency)
71
76
  response = []
72
77
  unknown_books = []
@@ -81,11 +86,7 @@ async def _get_books(
81
86
  if book.deal_id not in ignored_deal_ids
82
87
  ]
83
88
 
84
- if is_gui_env():
85
- results = await asyncio.gather(*tasks)
86
- else:
87
- results = await tqdm_asyncio.gather(*tasks, desc=f"Getting latest prices from {retailer.name}")
88
-
89
+ results = await asyncio.gather(*tasks)
89
90
  for book in results:
90
91
  if not book:
91
92
  """Cases where we know the retailer has the book but it's not coming back.
@@ -99,9 +100,7 @@ async def _get_books(
99
100
  elif not book.exists:
100
101
  unknown_books.append(book)
101
102
 
102
- click.echo()
103
- for book in unknown_books:
104
- echo_info(f"{book.title} by {book.authors} not found")
103
+ echo_info(f"Finished getting deals from {retailer.name}")
105
104
 
106
105
  return response, unknown_books
107
106
 
@@ -184,34 +183,26 @@ async def _get_latest_deals(config: Config):
184
183
  ignore_books: list[Book] = get_unknown_books(config)
185
184
  ignored_deal_ids: set[str] = {book.deal_id for book in ignore_books}
186
185
 
186
+ tasks = []
187
187
  for retailer_str in config.tracked_retailers:
188
188
  retailer = RETAILER_MAP[retailer_str]()
189
189
  await retailer.set_auth()
190
190
 
191
- relevant_tbr_books = _get_retailer_relevant_tbr_books(
192
- retailer,
193
- tbr_books,
191
+ tasks.append(
192
+ _get_books(
193
+ config,
194
+ retailer,
195
+ tbr_books,
196
+ ignored_deal_ids
197
+ )
194
198
  )
195
199
 
196
- echo_info(f"Getting deals from {retailer.name}")
197
- click.echo("\n---------------")
198
- retailer_books, u_books = await _get_books(
199
- config,
200
- retailer,
201
- relevant_tbr_books,
202
- ignored_deal_ids
203
- )
200
+ results = await asyncio.gather(*tasks)
201
+ for retailer_books, u_books in results:
204
202
  books.extend(retailer_books)
205
203
  unknown_books.extend(u_books)
206
- click.echo("---------------\n")
207
204
 
208
205
  _apply_proper_list_prices(books)
209
-
210
- books = [
211
- book
212
- for book in books
213
- ]
214
-
215
206
  update_retailer_deal_table(config, books)
216
207
  set_unknown_books(config, unknown_books)
217
208
 
tbr_deal_finder/utils.py CHANGED
@@ -27,25 +27,34 @@ def get_data_dir() -> Path:
27
27
  app_name = "TBR Deal Finder"
28
28
 
29
29
  if custom_path := os.getenv("TBR_DEAL_FINDER_CUSTOM_PATH"):
30
- path = Path(custom_path)
31
-
32
- elif not is_gui_env():
33
- path = Path.home() / ".tbr_deal_finder"
34
-
35
- elif sys.platform == "win32":
36
- # Windows: C:\Users\Username\AppData\Local\AppAuthor\AppName
37
- base = os.environ.get("LOCALAPPDATA", os.path.expanduser("~\\AppData\\Local"))
38
- path = Path(base) / app_author / app_name
39
-
40
- elif sys.platform == "darwin":
41
- # macOS: ~/Library/Application Support/AppName
42
- path = Path.home() / "Library" / "Application Support" / app_name
43
-
44
- else: # Linux and others
45
- # Linux: ~/.local/share/appname (following XDG spec)
46
- xdg_data_home = os.environ.get("XDG_DATA_HOME",
47
- os.path.expanduser("~/.local/share"))
48
- path = Path(xdg_data_home) / app_name.lower()
30
+ path = Path(custom_path).expanduser()
31
+ else:
32
+ cli_path = Path.home() / ".tbr_deal_finder"
33
+ if sys.platform == "win32":
34
+ # Windows: C:\Users\Username\AppData\Local\AppAuthor\AppName
35
+ base = os.environ.get("LOCALAPPDATA", os.path.expanduser("~\\AppData\\Local"))
36
+ gui_path = Path(base) / app_author / app_name
37
+
38
+ elif sys.platform == "darwin":
39
+ # macOS: ~/Library/Application Support/AppName
40
+ gui_path = Path.home() / "Library" / "Application Support" / app_name
41
+
42
+ else: # Linux and others
43
+ # Linux: ~/.local/share/appname (following XDG spec)
44
+ xdg_data_home = os.environ.get("XDG_DATA_HOME",
45
+ os.path.expanduser("~/.local/share"))
46
+ gui_path = Path(xdg_data_home) / app_name.lower()
47
+
48
+ if is_gui_env():
49
+ path = gui_path
50
+ if cli_path.exists() and not path.exists():
51
+ # Use the cli path if it exists and the gui path does not
52
+ path = cli_path
53
+ else:
54
+ path = cli_path
55
+ if gui_path.exists() and not path.exists():
56
+ # Use the gui path if it exists and the cli path does not
57
+ path = gui_path
49
58
 
50
59
  # Create directory if it doesn't exist
51
60
  path.mkdir(parents=True, exist_ok=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tbr-deal-finder
3
- Version: 0.3.1
3
+ Version: 0.3.3
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
@@ -1,24 +1,24 @@
1
- tbr_deal_finder/__init__.py,sha256=iYLaqaUWltN1HqI41ELPAw8KoYbwf9AbF4iXz4we5xw,104
1
+ tbr_deal_finder/__init__.py,sha256=Yq_k2n4fUg7OMmkDUL-qq_1rUK2wu1KPureT3sJEKx4,104
2
2
  tbr_deal_finder/__main__.py,sha256=b2-3WiGIno_XVusUDijQXa2MthKahFz6CVH-hIBe7es,165
3
3
  tbr_deal_finder/book.py,sha256=WbvDEeI923iQX_eIsC9H7y-qKVPkfCqwjBCqChssFCM,6740
4
4
  tbr_deal_finder/cli.py,sha256=16vZnWS9TTWPhIjZsjsf7OY7Btb0ULOfws5EkUefbRY,7089
5
5
  tbr_deal_finder/config.py,sha256=pocHMEoJVIoiuRzEtKDAmsGmiiLgD-lN6n6TU9gI8Lc,3982
6
- tbr_deal_finder/desktop_updater.py,sha256=VCD4MW1ckx6yyq0N_CJrOth8wePqPhTjhvdJTKsUVkw,5123
6
+ tbr_deal_finder/desktop_updater.py,sha256=-1hNJt1uAkxGo4fUf1IT_BQc9vcRR3mCBDHcYsO-_DU,2285
7
7
  tbr_deal_finder/migrations.py,sha256=fO7r2JbWb6YG0CsPqauakwvbKaEFPxqX1PP8c8N03Wc,4951
8
8
  tbr_deal_finder/owned_books.py,sha256=Cf1VeiSg7XBi_TXptJfy5sO1mEgMMQWbJ_P6SzAx0nQ,516
9
- tbr_deal_finder/retailer_deal.py,sha256=S5E7XHYx-L-31jvuIJ_vaaTCeTSxCjosLFkdA-fBc1o,7525
9
+ tbr_deal_finder/retailer_deal.py,sha256=l2n-79kSNZfPh73PZBbSE6tkNYYCaHPEehIPcelDPeY,7214
10
10
  tbr_deal_finder/tracked_books.py,sha256=u3KfBNlwvsEwTfM5TAJVLbiTmm1lTe2k70JJOszQz1k,12714
11
- tbr_deal_finder/utils.py,sha256=mWqn-AUniLG0If3Hm2fwvRf2tycA-47oHtd3HKFfzbU,3159
11
+ tbr_deal_finder/utils.py,sha256=LJYRNPRO3XBFIlodGzxCxlxiQ9viNYGU5QjNnjz4qMA,3635
12
12
  tbr_deal_finder/version_check.py,sha256=tzuKWngnjSdjPAOdyuPk2ym6Lv3LyeObJAfgsBIH9YQ,1217
13
13
  tbr_deal_finder/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- tbr_deal_finder/gui/main.py,sha256=I2YNyZkIln6bEl1E2aHqHd3eOJurgMBW-gZMN1MQAOU,26413
14
+ tbr_deal_finder/gui/main.py,sha256=msFtiEVd7um3Ck-dlMhjSUHG2GjSyeVuJSDsLGFarJs,28420
15
15
  tbr_deal_finder/gui/pages/__init__.py,sha256=1xc9Ib3oX_hqxLdhZvbCv3AhwOmVBSklZZC6CAcvoMU,20
16
- tbr_deal_finder/gui/pages/all_books.py,sha256=FG9mHofSmNUiYY9EeOBZeWUAAAxhvkTTWLoK1tJJrSc,3418
16
+ tbr_deal_finder/gui/pages/all_books.py,sha256=uQguQ9_NjM5kq_D5DbOWFB9om1lOdPk1iTcjH0yl9bg,3655
17
17
  tbr_deal_finder/gui/pages/all_deals.py,sha256=rY08w4XXFc_Jbd39BPJ6fO00vstuuu2YHvZterHx5ZI,2389
18
- tbr_deal_finder/gui/pages/base_book_page.py,sha256=Car1OSX571xBz_WHCMYZ4f5-Nd0FLB5MC_xC41g5aFU,10491
19
- tbr_deal_finder/gui/pages/book_details.py,sha256=POknUa9yNjfqhC6eXuw7RtaRcFtQj_CdvJ8mK2r6DDo,22047
20
- tbr_deal_finder/gui/pages/latest_deals.py,sha256=IuUg2mJEMqoDz4EoMtcfJFCvvSMrem7raJdkjDP5xvE,13813
21
- tbr_deal_finder/gui/pages/settings.py,sha256=5kr6FXdp6s7OalOEfRgzZOhjrzILiw4CgT3nIOhaHYk,14316
18
+ tbr_deal_finder/gui/pages/base_book_page.py,sha256=kmHjJE4eIGrMhXtBWePn_eXfO6qJ36kIoQ7hE1xB3ic,10465
19
+ tbr_deal_finder/gui/pages/book_details.py,sha256=8ZqONLeqZ_1TGzmkJrem00VBFT3VcczEtEIzyCiHyro,22067
20
+ tbr_deal_finder/gui/pages/latest_deals.py,sha256=XFTzmWjaw8grmbSF_KTGMKFOJ2TUY2kpLljyVdte93k,13896
21
+ tbr_deal_finder/gui/pages/settings.py,sha256=gdQXi508yd_oZ5lj0mxuDL5qSxmH256LwPi5gTo2Cqg,14357
22
22
  tbr_deal_finder/queries/get_active_deals.sql,sha256=nh0F1lRV6YVrUV7gsQpjsgfXmN9R0peBeMHRifjgpUM,212
23
23
  tbr_deal_finder/queries/get_deals_found_at.sql,sha256=KqrtQk7FS4Hf74RyL1r-oD2D-RJz1urrxKxkwlvjAro,139
24
24
  tbr_deal_finder/queries/latest_deal_last_ran_most_recent_success.sql,sha256=W4cNMAHtcW2DzQyPL8SHHFcbVZQKVK2VfTzazxC3LJU,107
@@ -31,8 +31,8 @@ tbr_deal_finder/retailer/chirp.py,sha256=GR8yeXp-1DHMktepy5TecHslrUibpLM7LfueIuT
31
31
  tbr_deal_finder/retailer/kindle.py,sha256=kA4SO2kl2SvJHADkBYyYMgq_bditStbiTiW7piQPAFI,5282
32
32
  tbr_deal_finder/retailer/librofm.py,sha256=dn1kaJEQ9g_AOAFNKhUtFLxYxQZa6RAHgXhT_dx-O3k,7514
33
33
  tbr_deal_finder/retailer/models.py,sha256=56xTwcLcw3bFcvTDOb85TktqtksvvyY95hZBbp9-5mY,3340
34
- tbr_deal_finder-0.3.1.dist-info/METADATA,sha256=4RkxKZB-76ZNwWBIkhZhmPmcU3_otzi1_BQUccI_9gE,3448
35
- tbr_deal_finder-0.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
- tbr_deal_finder-0.3.1.dist-info/entry_points.txt,sha256=xjeRw7aX_jbX1ERC--IgYIa2oLNgeRefsMbKeTAVb70,112
37
- tbr_deal_finder-0.3.1.dist-info/licenses/LICENSE,sha256=rNc0wNPn4d4HHu6ZheJzeUaz_FbJ4rj2Dr2FjAivkNg,1064
38
- tbr_deal_finder-0.3.1.dist-info/RECORD,,
34
+ tbr_deal_finder-0.3.3.dist-info/METADATA,sha256=VEp2T5WOIRAR_c5lc3y5YI8GJaaEiUuacKdl2oBQCdw,3448
35
+ tbr_deal_finder-0.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
+ tbr_deal_finder-0.3.3.dist-info/entry_points.txt,sha256=xjeRw7aX_jbX1ERC--IgYIa2oLNgeRefsMbKeTAVb70,112
37
+ tbr_deal_finder-0.3.3.dist-info/licenses/LICENSE,sha256=rNc0wNPn4d4HHu6ZheJzeUaz_FbJ4rj2Dr2FjAivkNg,1064
38
+ tbr_deal_finder-0.3.3.dist-info/RECORD,,