diff --git a/unshackle/core/config.py b/unshackle/core/config.py index 64cc788..4d51ba5 100644 --- a/unshackle/core/config.py +++ b/unshackle/core/config.py @@ -79,6 +79,7 @@ class Config: self.tag: str = kwargs.get("tag") or "" self.tmdb_api_key: str = kwargs.get("tmdb_api_key") or "" self.update_checks: bool = kwargs.get("update_checks", True) + self.update_check_interval: int = kwargs.get("update_check_interval", 24) @classmethod def from_yaml(cls, path: Path) -> Config: diff --git a/unshackle/core/update_checker.py b/unshackle/core/update_checker.py index 671d8d6..15c2c2e 100644 --- a/unshackle/core/update_checker.py +++ b/unshackle/core/update_checker.py @@ -1,6 +1,9 @@ from __future__ import annotations import asyncio +import json +import time +from pathlib import Path from typing import Optional import requests @@ -11,6 +14,66 @@ class UpdateChecker: REPO_URL = "https://api.github.com/repos/unshackle-dl/unshackle/releases/latest" TIMEOUT = 5 + DEFAULT_CHECK_INTERVAL = 24 * 60 * 60 # 24 hours in seconds + + @classmethod + def _get_cache_file(cls) -> Path: + """Get the path to the update check cache file.""" + from unshackle.core.config import config + + return config.directories.cache / "update_check.json" + + @classmethod + def _should_check_for_updates(cls, check_interval: int = DEFAULT_CHECK_INTERVAL) -> bool: + """ + Check if enough time has passed since the last update check. + + Args: + check_interval: Time in seconds between checks (default: 24 hours) + + Returns: + True if we should check for updates, False otherwise + """ + cache_file = cls._get_cache_file() + + if not cache_file.exists(): + return True + + try: + with open(cache_file, "r") as f: + cache_data = json.load(f) + + last_check = cache_data.get("last_check", 0) + current_time = time.time() + + return (current_time - last_check) >= check_interval + + except (json.JSONDecodeError, KeyError, OSError): + # If cache is corrupted or unreadable, allow check + return True + + @classmethod + def _update_cache(cls, latest_version: Optional[str] = None) -> None: + """ + Update the cache file with the current timestamp and latest version. + + Args: + latest_version: The latest version found, if any + """ + cache_file = cls._get_cache_file() + + try: + # Ensure cache directory exists + cache_file.parent.mkdir(parents=True, exist_ok=True) + + cache_data = {"last_check": time.time(), "latest_version": latest_version} + + with open(cache_file, "w") as f: + json.dump(cache_data, f) + + except (OSError, json.JSONEncodeError): + # Silently fail if we can't write cache + pass @staticmethod def _compare_versions(current: str, latest: str) -> bool: @@ -75,32 +138,51 @@ class UpdateChecker: return None @classmethod - def check_for_updates_sync(cls, current_version: str) -> Optional[str]: + def check_for_updates_sync(cls, current_version: str, check_interval: Optional[int] = None) -> Optional[str]: """ - Synchronous version of update check. + Synchronous version of update check with rate limiting. Args: current_version: The current version string (e.g., "1.1.0") + check_interval: Time in seconds between checks (default: from config) Returns: The latest version string if an update is available, None otherwise """ + # Use config value if not specified + if check_interval is None: + from unshackle.core.config import config + + check_interval = config.update_check_interval * 60 * 60 # Convert hours to seconds + + # Check if we should skip this check due to rate limiting + if not cls._should_check_for_updates(check_interval): + return None + try: response = requests.get(cls.REPO_URL, timeout=cls.TIMEOUT) if response.status_code != 200: + # Update cache even on failure to prevent rapid retries + cls._update_cache() return None data = response.json() latest_version = data.get("tag_name", "").lstrip("v") if not latest_version: + cls._update_cache() return None + # Update cache with the latest version info + cls._update_cache(latest_version) + if cls._compare_versions(current_version, latest_version): return latest_version except Exception: + # Update cache even on exception to prevent rapid retries + cls._update_cache() pass return None diff --git a/unshackle/unshackle.yaml b/unshackle/unshackle.yaml index c15c7c0..94621d5 100644 --- a/unshackle/unshackle.yaml +++ b/unshackle/unshackle.yaml @@ -7,6 +7,9 @@ set_terminal_bg: false # Check for updates from GitHub repository on startup (default: true) update_checks: true +# How often to check for updates, in hours (default: 24) +update_check_interval: 24 + # Muxing configuration muxing: set_title: false