221 lines
7.7 KiB
Python
221 lines
7.7 KiB
Python
"""
|
|
EU-Utility - Auto Updater (Core Framework Component)
|
|
|
|
Built-in update checker - not a plugin. Disabled by default, notifies only.
|
|
"""
|
|
|
|
import json
|
|
import urllib.request
|
|
from pathlib import Path
|
|
from typing import Optional, Dict, List
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass
|
|
class UpdateInfo:
|
|
"""Update information."""
|
|
version: str
|
|
download_url: str
|
|
changelog: str
|
|
is_required: bool = False
|
|
|
|
|
|
class AutoUpdater:
|
|
"""Auto updater - built into the framework.
|
|
|
|
By default, only checks and notifies. Auto-install is opt-in.
|
|
"""
|
|
|
|
UPDATE_CHECK_URL = "https://git.lemonlink.eu/impulsivefps/EU-Utility/raw/branch/main/version.json"
|
|
|
|
def __init__(self, settings):
|
|
self.settings = settings
|
|
self.current_version = "2.1.0" # Should be loaded from package
|
|
self.available_update: Optional[UpdateInfo] = None
|
|
self.plugin_updates: List[Dict] = []
|
|
|
|
def check_for_updates(self, silent: bool = False) -> bool:
|
|
"""Check for available updates.
|
|
|
|
Args:
|
|
silent: If True, don't show UI notifications
|
|
|
|
Returns:
|
|
True if updates are available
|
|
"""
|
|
try:
|
|
# Check core updates
|
|
with urllib.request.urlopen(self.UPDATE_CHECK_URL, timeout=10) as response:
|
|
data = json.loads(response.read().decode('utf-8'))
|
|
|
|
latest_version = data.get('version', self.current_version)
|
|
|
|
if self._version_compare(latest_version, self.current_version) > 0:
|
|
self.available_update = UpdateInfo(
|
|
version=latest_version,
|
|
download_url=data.get('download_url', ''),
|
|
changelog=data.get('changelog', ''),
|
|
is_required=data.get('required', False)
|
|
)
|
|
|
|
if not silent:
|
|
self._notify_update_available()
|
|
|
|
return True
|
|
|
|
# Check plugin updates
|
|
self._check_plugin_updates(silent)
|
|
|
|
if not silent and not self.available_update:
|
|
self._notify_up_to_date()
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
print(f"[AutoUpdater] Check failed: {e}")
|
|
if not silent:
|
|
self._notify_error(str(e))
|
|
return False
|
|
|
|
def _check_plugin_updates(self, silent: bool = False):
|
|
"""Check for plugin updates from the plugin repository."""
|
|
try:
|
|
# Fetch plugin manifest
|
|
manifest_url = "https://git.lemonlink.eu/impulsivefps/EU-Utility-Plugins-Repo/raw/branch/main/manifest.json"
|
|
|
|
with urllib.request.urlopen(manifest_url, timeout=10) as response:
|
|
manifest = json.loads(response.read().decode('utf-8'))
|
|
|
|
plugins_dir = Path("plugins")
|
|
updates = []
|
|
|
|
for plugin_info in manifest.get('plugins', []):
|
|
plugin_id = plugin_info['id']
|
|
plugin_path = plugins_dir / plugin_id / "plugin.py"
|
|
|
|
if plugin_path.exists():
|
|
# Check local version vs remote
|
|
local_version = self._get_local_plugin_version(plugin_path)
|
|
remote_version = plugin_info['version']
|
|
|
|
if self._version_compare(remote_version, local_version) > 0:
|
|
updates.append({
|
|
'id': plugin_id,
|
|
'name': plugin_info['name'],
|
|
'local_version': local_version,
|
|
'remote_version': remote_version
|
|
})
|
|
|
|
self.plugin_updates = updates
|
|
|
|
if updates and not silent:
|
|
self._notify_plugin_updates(updates)
|
|
|
|
except Exception as e:
|
|
print(f"[AutoUpdater] Plugin check failed: {e}")
|
|
|
|
def _get_local_plugin_version(self, plugin_path: Path) -> str:
|
|
"""Extract version from plugin file."""
|
|
try:
|
|
with open(plugin_path, 'r') as f:
|
|
content = f.read()
|
|
|
|
# Look for version = "x.x.x"
|
|
import re
|
|
match = re.search(r'version\s*=\s*["\']([\d.]+)["\']', content)
|
|
if match:
|
|
return match.group(1)
|
|
except:
|
|
pass
|
|
|
|
return "0.0.0"
|
|
|
|
def _version_compare(self, v1: str, v2: str) -> int:
|
|
"""Compare two version strings.
|
|
|
|
Returns:
|
|
>0 if v1 > v2, 0 if equal, <0 if v1 < v2
|
|
"""
|
|
parts1 = [int(x) for x in v1.split('.')]
|
|
parts2 = [int(x) for x in v2.split('.')]
|
|
|
|
for p1, p2 in zip(parts1, parts2):
|
|
if p1 != p2:
|
|
return p1 - p2
|
|
|
|
return len(parts1) - len(parts2)
|
|
|
|
def install_update(self) -> bool:
|
|
"""Install available update if auto-update is enabled."""
|
|
if not self.available_update:
|
|
return False
|
|
|
|
if not self.settings.get('auto_update_enabled', False):
|
|
print("[AutoUpdater] Auto-update disabled, skipping installation")
|
|
return False
|
|
|
|
# TODO: Implement update installation
|
|
print(f"[AutoUpdater] Installing update to {self.available_update.version}")
|
|
return True
|
|
|
|
def _notify_update_available(self):
|
|
"""Notify user of available update."""
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
|
|
if self.available_update:
|
|
msg = QMessageBox()
|
|
msg.setWindowTitle("Update Available")
|
|
msg.setText(f"EU-Utility {self.available_update.version} is available!")
|
|
msg.setInformativeText(
|
|
f"Current: {self.current_version}\n"
|
|
f"Latest: {self.available_update.version}\n\n"
|
|
f"Changelog:\n{self.available_update.changelog}\n\n"
|
|
"Go to Settings → Updates to install."
|
|
)
|
|
msg.setIcon(QMessageBox.Icon.Information)
|
|
msg.exec()
|
|
|
|
def _notify_plugin_updates(self, updates: List[Dict]):
|
|
"""Notify user of plugin updates."""
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
|
|
update_list = "\n".join([
|
|
f"• {u['name']}: {u['local_version']} → {u['remote_version']}"
|
|
for u in updates[:5] # Show max 5
|
|
])
|
|
|
|
if len(updates) > 5:
|
|
update_list += f"\n... and {len(updates) - 5} more"
|
|
|
|
msg = QMessageBox()
|
|
msg.setWindowTitle("Plugin Updates Available")
|
|
msg.setText(f"{len(updates)} plugin(s) have updates available!")
|
|
msg.setInformativeText(
|
|
f"{update_list}\n\n"
|
|
"Go to Settings → Plugin Store to update."
|
|
)
|
|
msg.setIcon(QMessageBox.Icon.Information)
|
|
msg.exec()
|
|
|
|
def _notify_up_to_date(self):
|
|
"""Notify user they are up to date."""
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
|
|
msg = QMessageBox()
|
|
msg.setWindowTitle("No Updates")
|
|
msg.setText("You are running the latest version!")
|
|
msg.setInformativeText(f"EU-Utility {self.current_version}")
|
|
msg.setIcon(QMessageBox.Icon.Information)
|
|
msg.exec()
|
|
|
|
def _notify_error(self, error: str):
|
|
"""Notify user of error."""
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
|
|
msg = QMessageBox()
|
|
msg.setWindowTitle("Update Check Failed")
|
|
msg.setText("Could not check for updates.")
|
|
msg.setInformativeText(f"Error: {error}")
|
|
msg.setIcon(QMessageBox.Icon.Warning)
|
|
msg.exec()
|