Addition Of Files
This commit is contained in:
8
updater/__init__.py
Normal file
8
updater/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Updater module for UmamusumeCardManager
|
||||
Handles checking for updates and downloading new versions from GitHub Releases.
|
||||
"""
|
||||
|
||||
from updater.update_checker import check_for_updates, download_update, apply_update
|
||||
|
||||
__all__ = ['check_for_updates', 'download_update', 'apply_update']
|
||||
264
updater/update_checker.py
Normal file
264
updater/update_checker.py
Normal file
@@ -0,0 +1,264 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user