""" 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()