265 lines
7.8 KiB
Python
265 lines
7.8 KiB
Python
"""
|
|
Update checker for UmamusumeCardManager
|
|
Checks GitHub Releases for new versions and handles downloading updates.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import subprocess
|
|
import requests
|
|
from typing import Optional, Callable, Tuple
|
|
|
|
# Import version info
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
from version import VERSION, GITHUB_API_URL, APP_NAME
|
|
|
|
|
|
def parse_version(version_str: str) -> Tuple[int, int, int]:
|
|
"""
|
|
Parse a version string into a tuple of integers.
|
|
Handles formats like "1.0.0", "v1.0.0", "1.2.3-beta", etc.
|
|
"""
|
|
# Remove 'v' prefix if present
|
|
version_str = version_str.lstrip('v').lstrip('V')
|
|
|
|
# Remove any suffix like -beta, -rc1, etc.
|
|
if '-' in version_str:
|
|
version_str = version_str.split('-')[0]
|
|
|
|
parts = version_str.split('.')
|
|
|
|
# Ensure we have at least 3 parts
|
|
while len(parts) < 3:
|
|
parts.append('0')
|
|
|
|
try:
|
|
return (int(parts[0]), int(parts[1]), int(parts[2]))
|
|
except ValueError:
|
|
return (0, 0, 0)
|
|
|
|
|
|
def compare_versions(local: str, remote: str) -> int:
|
|
"""
|
|
Compare two version strings.
|
|
Returns:
|
|
-1 if local < remote (update available)
|
|
0 if local == remote (up to date)
|
|
1 if local > remote (local is newer)
|
|
"""
|
|
local_tuple = parse_version(local)
|
|
remote_tuple = parse_version(remote)
|
|
|
|
if local_tuple < remote_tuple:
|
|
return -1
|
|
elif local_tuple > remote_tuple:
|
|
return 1
|
|
else:
|
|
return 0
|
|
|
|
|
|
def check_for_updates() -> Optional[dict]:
|
|
"""
|
|
Check GitHub Releases for a new version.
|
|
|
|
Returns:
|
|
dict with update info if available, None if up to date or error.
|
|
{
|
|
'current_version': str,
|
|
'new_version': str,
|
|
'download_url': str,
|
|
'release_notes': str,
|
|
'html_url': str # Link to the release page
|
|
}
|
|
"""
|
|
try:
|
|
# Add a user-agent header (GitHub API requires this)
|
|
headers = {
|
|
'User-Agent': f'{APP_NAME}-Updater',
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
}
|
|
|
|
response = requests.get(GITHUB_API_URL, headers=headers, timeout=10)
|
|
|
|
if response.status_code == 404:
|
|
# No releases found
|
|
print("No releases found on GitHub.")
|
|
return None
|
|
|
|
response.raise_for_status()
|
|
release_data = response.json()
|
|
|
|
remote_version = release_data.get('tag_name', '')
|
|
|
|
# Compare versions
|
|
if compare_versions(VERSION, remote_version) < 0:
|
|
# Find the Windows exe asset
|
|
download_url = None
|
|
for asset in release_data.get('assets', []):
|
|
asset_name = asset.get('name', '').lower()
|
|
if asset_name.endswith('.exe'):
|
|
download_url = asset.get('browser_download_url')
|
|
break
|
|
|
|
if not download_url:
|
|
print("No .exe asset found in the latest release.")
|
|
return None
|
|
|
|
return {
|
|
'current_version': VERSION,
|
|
'new_version': remote_version,
|
|
'download_url': download_url,
|
|
'release_notes': release_data.get('body', 'No release notes provided.'),
|
|
'html_url': release_data.get('html_url', '')
|
|
}
|
|
else:
|
|
# Already up to date
|
|
return None
|
|
|
|
except requests.exceptions.Timeout:
|
|
print("Update check timed out.")
|
|
return None
|
|
except requests.exceptions.RequestException as e:
|
|
print(f"Error checking for updates: {e}")
|
|
return None
|
|
except Exception as e:
|
|
print(f"Unexpected error during update check: {e}")
|
|
return None
|
|
|
|
|
|
def download_update(download_url: str, progress_callback: Optional[Callable[[int, int], None]] = None) -> Optional[str]:
|
|
"""
|
|
Download the update file.
|
|
|
|
Args:
|
|
download_url: URL to download the new exe from
|
|
progress_callback: Optional callback function(downloaded_bytes, total_bytes)
|
|
|
|
Returns:
|
|
Path to the downloaded file, or None if failed.
|
|
"""
|
|
try:
|
|
headers = {
|
|
'User-Agent': f'{APP_NAME}-Updater'
|
|
}
|
|
|
|
response = requests.get(download_url, headers=headers, stream=True, timeout=60)
|
|
response.raise_for_status()
|
|
|
|
# Get total file size
|
|
total_size = int(response.headers.get('content-length', 0))
|
|
|
|
# Create temp file for download
|
|
temp_dir = tempfile.gettempdir()
|
|
temp_path = os.path.join(temp_dir, f'{APP_NAME}_update.exe')
|
|
|
|
downloaded = 0
|
|
chunk_size = 8192
|
|
|
|
with open(temp_path, 'wb') as f:
|
|
for chunk in response.iter_content(chunk_size=chunk_size):
|
|
if chunk:
|
|
f.write(chunk)
|
|
downloaded += len(chunk)
|
|
if progress_callback and total_size > 0:
|
|
progress_callback(downloaded, total_size)
|
|
|
|
return temp_path
|
|
|
|
except Exception as e:
|
|
print(f"Error downloading update: {e}")
|
|
return None
|
|
|
|
|
|
def get_current_exe_path() -> str:
|
|
"""Get the path to the current running executable."""
|
|
if getattr(sys, 'frozen', False):
|
|
# Running as compiled exe
|
|
return sys.executable
|
|
else:
|
|
# Running as script
|
|
return os.path.abspath(sys.argv[0])
|
|
|
|
|
|
def apply_update(new_exe_path: str) -> bool:
|
|
"""
|
|
Apply the update by replacing the current exe with the new one.
|
|
|
|
This creates a batch script that:
|
|
1. Waits for the current process to exit
|
|
2. Replaces the old exe with the new one
|
|
3. Starts the new exe
|
|
4. Cleans up the batch script
|
|
|
|
Args:
|
|
new_exe_path: Path to the downloaded new exe
|
|
|
|
Returns:
|
|
True if the update process was started successfully.
|
|
"""
|
|
try:
|
|
current_exe = get_current_exe_path()
|
|
|
|
# If running as a script, we can't self-update
|
|
if not getattr(sys, 'frozen', False):
|
|
print("Cannot apply update when running as a script.")
|
|
print(f"New version downloaded to: {new_exe_path}")
|
|
return False
|
|
|
|
# Create a batch script to perform the update
|
|
batch_script = os.path.join(tempfile.gettempdir(), f'{APP_NAME}_updater.bat')
|
|
|
|
# Simple batch script that just waits and applies the update
|
|
# We don't auto-restart because PyInstaller temp cleanup causes DLL errors
|
|
script_content = f'''@echo off
|
|
title {APP_NAME} Updater
|
|
echo ========================================
|
|
echo {APP_NAME} Updater
|
|
echo ========================================
|
|
echo.
|
|
echo Waiting for application to close...
|
|
timeout /t 3 >nul
|
|
|
|
echo.
|
|
echo Applying update...
|
|
move /Y "{new_exe_path}" "{current_exe}"
|
|
if errorlevel 1 (
|
|
echo.
|
|
echo ERROR: Update failed!
|
|
echo Please close the application completely and try again.
|
|
pause
|
|
exit /b 1
|
|
)
|
|
|
|
echo.
|
|
echo ========================================
|
|
echo Update applied successfully!
|
|
echo ========================================
|
|
echo.
|
|
echo Please start the application manually.
|
|
echo This window will close in 5 seconds...
|
|
timeout /t 5 >nul
|
|
exit
|
|
'''
|
|
|
|
with open(batch_script, 'w') as f:
|
|
f.write(script_content)
|
|
|
|
# Start the batch script with a visible window so user can see progress
|
|
CREATE_NEW_CONSOLE = 0x00000010
|
|
subprocess.Popen(
|
|
['cmd', '/c', batch_script],
|
|
creationflags=CREATE_NEW_CONSOLE
|
|
)
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"Error applying update: {e}")
|
|
return False
|
|
|
|
|
|
def get_current_version() -> str:
|
|
"""Get the current application version."""
|
|
return VERSION
|