tbr-deal-finder 0.3.2__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/.github/workflows/build-release-dmg.yml +51 -20
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/CHANGELOG.md +11 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/Makefile +1 -1
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/PKG-INFO +1 -1
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/pyproject.toml +10 -1
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/__init__.py +1 -1
- tbr_deal_finder-0.3.3/tbr_deal_finder/desktop_updater.py +71 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/gui/main.py +51 -27
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/gui/pages/book_details.py +2 -3
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/gui/pages/latest_deals.py +18 -20
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/uv.lock +1 -1
- tbr_deal_finder-0.3.2/tbr_deal_finder/desktop_updater.py +0 -147
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/.github/workflows/build-release-exe.yml +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/.github/workflows/publish-to-pypi.yaml +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/.gitignore +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/.python-version +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/DESIGN.md +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/LICENSE +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/README.md +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/assets/icon.ico +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/assets/icon.png +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/docs/desktop-app.md +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/docs/development.md +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/docs/python-cli.md +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/scripts/packaging/create_dmg.sh +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/__main__.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/book.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/cli.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/config.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/gui/__init__.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/gui/pages/__init__.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/gui/pages/all_books.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/gui/pages/all_deals.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/gui/pages/base_book_page.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/gui/pages/settings.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/migrations.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/owned_books.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/queries/get_active_deals.sql +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/queries/get_deals_found_at.sql +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/queries/latest_deal_last_ran_most_recent_success.sql +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/queries/latest_unknown_book_sync.sql +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/retailer/__init__.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/retailer/amazon.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/retailer/amazon_custom_auth.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/retailer/audible.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/retailer/chirp.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/retailer/kindle.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/retailer/librofm.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/retailer/models.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/retailer_deal.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/tracked_books.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/utils.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/version_check.py +0 -0
- {tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder.py +0 -0
@@ -14,11 +14,11 @@ jobs:
|
|
14
14
|
HAS_MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE != '' }}
|
15
15
|
HAS_MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD != '' }}
|
16
16
|
VERSION: ${{ github.event.release.tag_name }}
|
17
|
-
|
17
|
+
|
18
18
|
steps:
|
19
19
|
- name: Checkout code
|
20
20
|
uses: actions/checkout@v4
|
21
|
-
|
21
|
+
|
22
22
|
- name: Set up Python
|
23
23
|
uses: actions/setup-python@v5
|
24
24
|
with:
|
@@ -26,7 +26,7 @@ jobs:
|
|
26
26
|
|
27
27
|
- name: Install uv
|
28
28
|
uses: astral-sh/setup-uv@v5
|
29
|
-
|
29
|
+
|
30
30
|
- name: Install project dependencies
|
31
31
|
run: |
|
32
32
|
uv sync
|
@@ -35,7 +35,7 @@ jobs:
|
|
35
35
|
uses: maxim-lobanov/setup-xcode@v1
|
36
36
|
with:
|
37
37
|
xcode-version: latest-stable
|
38
|
-
|
38
|
+
|
39
39
|
- name: Set up CocoaPods
|
40
40
|
uses: maxim-lobanov/setup-cocoapods@v1
|
41
41
|
with:
|
@@ -44,44 +44,76 @@ jobs:
|
|
44
44
|
- name: Import Code Signing Certificate (if available)
|
45
45
|
if: ${{ env.HAS_MACOS_CERTIFICATE == 'true' && env.HAS_MACOS_CERTIFICATE_PASSWORD == 'true' }}
|
46
46
|
run: |
|
47
|
+
set -e # Exit on any error
|
48
|
+
|
47
49
|
# Create temporary keychain
|
48
|
-
|
49
|
-
|
50
|
-
|
50
|
+
KEYCHAIN_PASSWORD="temp_keychain_password"
|
51
|
+
KEYCHAIN_NAME="temp.keychain-db"
|
52
|
+
|
53
|
+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
|
54
|
+
security default-keychain -s "$KEYCHAIN_NAME"
|
55
|
+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
|
56
|
+
security set-keychain-settings -t 3600 -u "$KEYCHAIN_NAME"
|
51
57
|
|
52
|
-
#
|
58
|
+
# Decode PKCS12 certificate
|
53
59
|
echo "${{ secrets.MACOS_CERTIFICATE }}" | base64 --decode > certificate.p12
|
54
|
-
|
60
|
+
|
61
|
+
# Verify file exists and has content
|
62
|
+
if [ ! -s certificate.p12 ]; then
|
63
|
+
echo "❌ Certificate file is empty or missing"
|
64
|
+
exit 1
|
65
|
+
fi
|
66
|
+
|
67
|
+
# Import PKCS12 to keychain (specifying format explicitly is required)
|
68
|
+
echo "Importing certificate..."
|
69
|
+
security import certificate.p12 \
|
70
|
+
-k "$KEYCHAIN_NAME" \
|
71
|
+
-P "${{ secrets.MACOS_CERTIFICATE_PASSWORD }}" \
|
72
|
+
-f pkcs12 \
|
73
|
+
-T /usr/bin/codesign
|
55
74
|
|
56
75
|
# Allow codesign to access the certificate
|
57
|
-
security set-key-partition-list
|
76
|
+
security set-key-partition-list \
|
77
|
+
-S apple-tool:,apple:,codesign: \
|
78
|
+
-s \
|
79
|
+
-k "$KEYCHAIN_PASSWORD" \
|
80
|
+
"$KEYCHAIN_NAME" 2>/dev/null || true
|
58
81
|
|
59
82
|
# Set code signing identity
|
60
83
|
if [ -n "${{ secrets.CODESIGN_IDENTITY }}" ]; then
|
61
|
-
# Use provided identity name
|
62
84
|
echo "CODESIGN_IDENTITY=${{ secrets.CODESIGN_IDENTITY }}" >> $GITHUB_ENV
|
63
85
|
echo "✅ Code signing certificate imported: ${{ secrets.CODESIGN_IDENTITY }}"
|
64
86
|
else
|
65
87
|
# Auto-detect identity from certificate
|
66
|
-
CERT_IDENTITY=$(security find-identity -v -p codesigning
|
88
|
+
CERT_IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_NAME" | grep -v "0 valid identities" | head -1 | awk -F'"' '{print $2}')
|
89
|
+
if [ -z "$CERT_IDENTITY" ]; then
|
90
|
+
# Fallback to any identity if no codesigning specific one found
|
91
|
+
CERT_IDENTITY=$(security find-identity -v "$KEYCHAIN_NAME" | grep -v "0 valid identities" | head -1 | awk -F'"' '{print $2}')
|
92
|
+
fi
|
93
|
+
|
94
|
+
if [ -z "$CERT_IDENTITY" ]; then
|
95
|
+
echo "❌ No signing identity found"
|
96
|
+
exit 1
|
97
|
+
fi
|
98
|
+
|
67
99
|
echo "CODESIGN_IDENTITY=$CERT_IDENTITY" >> $GITHUB_ENV
|
68
100
|
echo "✅ Code signing certificate imported (auto-detected): $CERT_IDENTITY"
|
69
101
|
fi
|
70
102
|
|
71
|
-
# Clean up
|
103
|
+
# Clean up
|
72
104
|
rm -f certificate.p12
|
73
|
-
|
105
|
+
|
74
106
|
- name: Build DMG
|
75
107
|
run: |
|
76
108
|
make build-mac
|
77
|
-
|
109
|
+
|
78
110
|
- name: Cleanup Code Signing
|
79
111
|
if: ${{ env.HAS_MACOS_CERTIFICATE == 'true' && env.HAS_MACOS_CERTIFICATE_PASSWORD == 'true' }}
|
80
112
|
run: |
|
81
113
|
# Remove temporary keychain
|
82
114
|
security delete-keychain temp.keychain || true
|
83
115
|
echo "🧹 Cleaned up code signing keychain"
|
84
|
-
|
116
|
+
|
85
117
|
- name: Find and verify DMG was created
|
86
118
|
id: find-dmg
|
87
119
|
run: |
|
@@ -96,7 +128,7 @@ jobs:
|
|
96
128
|
echo "✅ DMG file created successfully: $DMG_FILE"
|
97
129
|
echo "dmg_path=$DMG_FILE" >> $GITHUB_OUTPUT
|
98
130
|
echo "dmg_name=$(basename $DMG_FILE)" >> $GITHUB_OUTPUT
|
99
|
-
|
131
|
+
|
100
132
|
- name: Get DMG info
|
101
133
|
id: dmg-info
|
102
134
|
run: |
|
@@ -104,7 +136,7 @@ jobs:
|
|
104
136
|
echo "size=$DMG_SIZE" >> $GITHUB_OUTPUT
|
105
137
|
echo "📦 DMG Size: $DMG_SIZE"
|
106
138
|
echo "📦 DMG Name: ${{ steps.find-dmg.outputs.dmg_name }}"
|
107
|
-
|
139
|
+
|
108
140
|
- name: Upload DMG to Release
|
109
141
|
env:
|
110
142
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
@@ -112,5 +144,4 @@ jobs:
|
|
112
144
|
gh release upload ${{ github.event.release.tag_name }} \
|
113
145
|
"${{ steps.find-dmg.outputs.dmg_path }}" \
|
114
146
|
--clobber \
|
115
|
-
--repo ${{ github.repository }}
|
116
|
-
|
147
|
+
--repo ${{ github.repository }}
|
@@ -3,6 +3,17 @@
|
|
3
3
|
|
4
4
|
---
|
5
5
|
|
6
|
+
## 0.3.3 (September 10, 2025)
|
7
|
+
|
8
|
+
Notes:
|
9
|
+
* Improved updater for Mac app
|
10
|
+
|
11
|
+
BUG FIXES:
|
12
|
+
* Fixed Mac app cert
|
13
|
+
* Fixed issue where pricing graph points were running off graph
|
14
|
+
|
15
|
+
---
|
16
|
+
|
6
17
|
## 0.3.2 (September 8, 2025)
|
7
18
|
|
8
19
|
Notes:
|
@@ -31,7 +31,7 @@ BUILD_SCRIPT := scripts/packaging/build_cross_platform.py
|
|
31
31
|
# Build self-signed macOS DMG (recommended)
|
32
32
|
build-mac:
|
33
33
|
@echo "🍎 Building app"
|
34
|
-
uv run flet build macos --output ${DIST_DIR}/app/
|
34
|
+
NONINTERACTIVE=1 NO_COLOR=1 TERM=dumb CI=true uv run flet build macos --output ${DIST_DIR}/app/
|
35
35
|
@echo ""
|
36
36
|
@echo "📦 Creating self-signed macOS DMG for app"
|
37
37
|
bash scripts/packaging/create_dmg.sh
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "tbr-deal-finder"
|
3
|
-
version = "0.3.
|
3
|
+
version = "0.3.3"
|
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"
|
@@ -30,6 +30,15 @@ product = "TBR Deal Finder"
|
|
30
30
|
app.module = "tbr_deal_finder"
|
31
31
|
app.path = "."
|
32
32
|
|
33
|
+
# Dependency locks
|
34
|
+
# Fix for https://github.com/flet-dev/flet/issues/5630
|
35
|
+
android.min_sdk_version = 24
|
36
|
+
|
37
|
+
[tool.flet.flutter.pubspec.dependency_overrides]
|
38
|
+
# Dependency locks
|
39
|
+
# Fix for https://github.com/flet-dev/flet/issues/5630
|
40
|
+
webview_flutter_android = "4.10.1"
|
41
|
+
|
33
42
|
[tool.setuptools.package-data]
|
34
43
|
"tbr-deal-finder" = [
|
35
44
|
"queries/**/*.sql",
|
@@ -0,0 +1,71 @@
|
|
1
|
+
"""
|
2
|
+
Desktop application update checker and handler.
|
3
|
+
For packaged desktop applications (.dmg/.exe).
|
4
|
+
"""
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
import os
|
8
|
+
import platform
|
9
|
+
import subprocess
|
10
|
+
import tempfile
|
11
|
+
import webbrowser
|
12
|
+
from pathlib import Path
|
13
|
+
from typing import Optional, Dict, Any
|
14
|
+
|
15
|
+
import requests
|
16
|
+
from packaging import version
|
17
|
+
|
18
|
+
from tbr_deal_finder import __VERSION__
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
class DesktopUpdater:
|
23
|
+
"""Handle updates for packaged desktop applications."""
|
24
|
+
|
25
|
+
def __init__(self, github_repo: str = "WillNye/tbr-deal-finder"):
|
26
|
+
self.github_repo = github_repo
|
27
|
+
self.current_version = __VERSION__
|
28
|
+
self.platform = platform.system().lower()
|
29
|
+
|
30
|
+
def check_for_updates(self) -> Optional[Dict[str, Any]]:
|
31
|
+
"""
|
32
|
+
Check GitHub releases for newer versions.
|
33
|
+
Returns dict with update info or None if no update available.
|
34
|
+
"""
|
35
|
+
try:
|
36
|
+
# Check GitHub releases API
|
37
|
+
response = requests.get(
|
38
|
+
f"https://api.github.com/repos/{self.github_repo}/releases/latest",
|
39
|
+
timeout=5
|
40
|
+
)
|
41
|
+
response.raise_for_status()
|
42
|
+
|
43
|
+
release_data = response.json()
|
44
|
+
latest_version = release_data["tag_name"].lstrip("v")
|
45
|
+
if version.parse(latest_version) > version.parse(self.current_version):
|
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
|
+
|
52
|
+
return {
|
53
|
+
"version": latest_version,
|
54
|
+
"download_url": download_url,
|
55
|
+
"release_notes": release_data.get("body", ""),
|
56
|
+
}
|
57
|
+
|
58
|
+
return None
|
59
|
+
|
60
|
+
except Exception as e:
|
61
|
+
logger.error(f"Failed to check updates for {self.github_repo}: {e}")
|
62
|
+
return None
|
63
|
+
|
64
|
+
|
65
|
+
# Global instance
|
66
|
+
desktop_updater = DesktopUpdater()
|
67
|
+
|
68
|
+
|
69
|
+
def check_for_desktop_updates() -> Optional[Dict[str, Any]]:
|
70
|
+
"""Convenience function to check for updates."""
|
71
|
+
return desktop_updater.check_for_updates()
|
@@ -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.
|
642
|
-
|
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
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
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.
|
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.
|
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
|
-
|
78
|
-
|
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
|
84
|
-
status_text = f"Last run: {
|
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
|
-
|
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(
|
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
|
-
|
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
|
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.
|
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.
|
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.
|
373
|
+
self.app.update_last_run_time()
|
376
374
|
self.load_items()
|
@@ -1,147 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Desktop application update checker and handler.
|
3
|
-
For packaged desktop applications (.dmg/.exe).
|
4
|
-
"""
|
5
|
-
import json
|
6
|
-
import logging
|
7
|
-
import os
|
8
|
-
import platform
|
9
|
-
import subprocess
|
10
|
-
import tempfile
|
11
|
-
import webbrowser
|
12
|
-
from pathlib import Path
|
13
|
-
from typing import Optional, Dict, Any
|
14
|
-
|
15
|
-
import requests
|
16
|
-
from packaging import version
|
17
|
-
|
18
|
-
from tbr_deal_finder import __VERSION__
|
19
|
-
|
20
|
-
logger = logging.getLogger(__name__)
|
21
|
-
|
22
|
-
class DesktopUpdater:
|
23
|
-
"""Handle updates for packaged desktop applications."""
|
24
|
-
|
25
|
-
def __init__(self, github_repo: str = "WillNye/tbr-deal-finder"):
|
26
|
-
self.github_repo = github_repo
|
27
|
-
self.current_version = __VERSION__
|
28
|
-
self.platform = platform.system().lower()
|
29
|
-
|
30
|
-
def check_for_updates(self) -> Optional[Dict[str, Any]]:
|
31
|
-
"""
|
32
|
-
Check GitHub releases for newer versions.
|
33
|
-
Returns dict with update info or None if no update available.
|
34
|
-
"""
|
35
|
-
try:
|
36
|
-
# Check GitHub releases API
|
37
|
-
response = requests.get(
|
38
|
-
f"https://api.github.com/repos/{self.github_repo}/releases/latest",
|
39
|
-
timeout=5
|
40
|
-
)
|
41
|
-
response.raise_for_status()
|
42
|
-
|
43
|
-
release_data = response.json()
|
44
|
-
latest_version = release_data["tag_name"].lstrip("v")
|
45
|
-
if version.parse(latest_version) > version.parse(self.current_version):
|
46
|
-
release_url = release_data["html_url"]
|
47
|
-
return {
|
48
|
-
"version": latest_version,
|
49
|
-
"download_url": release_url,
|
50
|
-
"release_notes": release_data.get("body", ""),
|
51
|
-
"release_url": release_url
|
52
|
-
}
|
53
|
-
|
54
|
-
return None
|
55
|
-
|
56
|
-
except Exception as e:
|
57
|
-
logger.error(f"Failed to check updates for {self.github_repo}: {e}")
|
58
|
-
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
|
-
|
140
|
-
|
141
|
-
# Global instance
|
142
|
-
desktop_updater = DesktopUpdater()
|
143
|
-
|
144
|
-
|
145
|
-
def check_for_desktop_updates() -> Optional[Dict[str, Any]]:
|
146
|
-
"""Convenience function to check for updates."""
|
147
|
-
return desktop_updater.check_for_updates()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/queries/get_active_deals.sql
RENAMED
File without changes
|
{tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/queries/get_deals_found_at.sql
RENAMED
File without changes
|
File without changes
|
{tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/queries/latest_unknown_book_sync.sql
RENAMED
File without changes
|
File without changes
|
File without changes
|
{tbr_deal_finder-0.3.2 → tbr_deal_finder-0.3.3}/tbr_deal_finder/retailer/amazon_custom_auth.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|