tbr-deal-finder 0.3.2__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.2"
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:
@@ -27,7 +32,8 @@ class TBRDealFinderApp:
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
30
-
35
+ self._last_run_time = None
36
+
31
37
  # Initialize pages
32
38
  self.settings_page = SettingsPage(self)
33
39
  self.all_deals_page = AllDealsPage(self)
@@ -295,10 +301,9 @@ class TBRDealFinderApp:
295
301
  self.all_books_page.refresh_page_state()
296
302
 
297
303
  def refresh_all_pages(self):
298
- """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"""
299
305
  self.all_deals_page.refresh_page_state()
300
306
  self.latest_deals_page.refresh_page_state()
301
- self.all_books_page.refresh_page_state()
302
307
 
303
308
  def disable_navigation(self):
304
309
  """Disable navigation rail during background operations"""
@@ -621,10 +626,6 @@ class TBRDealFinderApp:
621
626
  def close_dialog(e):
622
627
  dialog.open = False
623
628
  self.page.update()
624
-
625
- def view_release_and_close(e):
626
- self.view_release_notes(e)
627
- close_dialog(e)
628
629
 
629
630
  def download_and_close(e):
630
631
  self.download_update(e)
@@ -638,10 +639,13 @@ class TBRDealFinderApp:
638
639
  ], spacing=10),
639
640
  content=ft.Column([
640
641
  ft.Text(f"Version {self.update_info['version']} is now available!"),
641
- ft.Text("Would you like to download the update?", color=ft.Colors.GREY_600)
642
- ], 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),
643
648
  actions=[
644
- ft.TextButton("View Release Notes", on_click=view_release_and_close),
645
649
  ft.ElevatedButton("Download Update", on_click=download_and_close),
646
650
  ft.TextButton("Later", on_click=close_dialog),
647
651
  ],
@@ -675,24 +679,36 @@ class TBRDealFinderApp:
675
679
  dialog.open = True
676
680
  self.page.update()
677
681
 
678
- def view_release_notes(self, e):
679
- """Open release notes in browser."""
680
- if self.update_info:
681
- import webbrowser
682
- webbrowser.open(self.update_info['release_url'])
683
-
684
682
  def download_update(self, e):
685
683
  """Handle update download."""
686
684
  if not self.update_info or not self.update_info.get('download_url'):
687
685
  return
688
-
689
- # For now, open download URL in browser
690
- # In a more advanced implementation, you could download in-app
691
- import webbrowser
692
- webbrowser.open(self.update_info['download_url'])
693
-
694
- # Show download instructions
695
- 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()
696
712
 
697
713
  def show_download_instructions(self):
698
714
  """Show instructions for installing the downloaded update."""
@@ -701,7 +717,7 @@ class TBRDealFinderApp:
701
717
  self.page.update()
702
718
 
703
719
  instructions = {
704
- "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",
705
721
  "windows": "1. Download will start in your browser\n2. Run the downloaded .exe installer\n3. Follow the installation wizard\n4. Restart the application",
706
722
  }
707
723
 
@@ -732,12 +748,20 @@ class TBRDealFinderApp:
732
748
  dialog.open = True
733
749
  self.page.update()
734
750
 
735
-
736
-
737
751
  def check_for_updates_button(self):
738
752
  """Check for updates when button is clicked."""
739
753
  self.check_for_updates_manual()
740
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
+
741
765
 
742
766
  def main():
743
767
  """Main entry point for the GUI application"""
@@ -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"""
@@ -286,7 +284,7 @@ class LatestDealsPage(BaseBookPage):
286
284
 
287
285
  if success:
288
286
  # Update the run time and load new deals
289
- self.last_run_time = self.app.config.run_time
287
+ self.app.update_last_run_time()
290
288
  self.load_items()
291
289
  self.show_success(f"Found {len(self.items)} new deals!")
292
290
  else:
@@ -300,7 +298,7 @@ class LatestDealsPage(BaseBookPage):
300
298
  self.is_loading = False
301
299
  self.progress_container.visible = False
302
300
  self.run_button.disabled = False
303
- self.check_last_run() # Refresh the status
301
+ self.app.update_last_run_time() # Refresh the status
304
302
 
305
303
  # Re-enable navigation after the operation
306
304
  self.app.enable_navigation()
@@ -372,5 +370,5 @@ class LatestDealsPage(BaseBookPage):
372
370
  self.format_dropdown.value = "All"
373
371
 
374
372
  # Check last run and reload data
375
- self.check_last_run()
373
+ self.app.update_last_run_time()
376
374
  self.load_items()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tbr-deal-finder
3
- Version: 0.3.2
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,9 +1,9 @@
1
- tbr_deal_finder/__init__.py,sha256=3Zo2T2Th8AT8qexLKWZhNJIeyXPVf2N76kkDGJw5Wc0,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
9
  tbr_deal_finder/retailer_deal.py,sha256=l2n-79kSNZfPh73PZBbSE6tkNYYCaHPEehIPcelDPeY,7214
@@ -11,13 +11,13 @@ tbr_deal_finder/tracked_books.py,sha256=u3KfBNlwvsEwTfM5TAJVLbiTmm1lTe2k70JJOszQ
11
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=VvdCdSH3mbVhw_3PaDD78U8RQV-drTyXyCNSWFwmoe8,27539
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
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
18
  tbr_deal_finder/gui/pages/base_book_page.py,sha256=kmHjJE4eIGrMhXtBWePn_eXfO6qJ36kIoQ7hE1xB3ic,10465
19
- tbr_deal_finder/gui/pages/book_details.py,sha256=POknUa9yNjfqhC6eXuw7RtaRcFtQj_CdvJ8mK2r6DDo,22047
20
- tbr_deal_finder/gui/pages/latest_deals.py,sha256=E-DoINTb7hAdiope7y9kqCdWuViBq3zDmMmFEL-vzso,14019
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
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
@@ -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.2.dist-info/METADATA,sha256=W3GS1LYeE-KXn0aE9S8d4v7GtRGTAobTw4ZeUmXYtqw,3448
35
- tbr_deal_finder-0.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
- tbr_deal_finder-0.3.2.dist-info/entry_points.txt,sha256=xjeRw7aX_jbX1ERC--IgYIa2oLNgeRefsMbKeTAVb70,112
37
- tbr_deal_finder-0.3.2.dist-info/licenses/LICENSE,sha256=rNc0wNPn4d4HHu6ZheJzeUaz_FbJ4rj2Dr2FjAivkNg,1064
38
- tbr_deal_finder-0.3.2.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,,