"""Hydrus API health check and initialization. Provides startup health checks for Hydrus API availability and gracefully disables Hydrus features if the API is unavailable. """ import logging import sys from helper.logger import log from typing import Tuple, Optional, Dict, Any from pathlib import Path logger = logging.getLogger(__name__) # Global state for Hydrus availability _HYDRUS_AVAILABLE: Optional[bool] = None _HYDRUS_UNAVAILABLE_REASON: Optional[str] = None _HYDRUS_CHECK_COMPLETE = False # Global state for Debrid availability _DEBRID_AVAILABLE: Optional[bool] = None _DEBRID_UNAVAILABLE_REASON: Optional[str] = None _DEBRID_CHECK_COMPLETE = False # Global state for MPV availability _MPV_AVAILABLE: Optional[bool] = None _MPV_UNAVAILABLE_REASON: Optional[str] = None _MPV_CHECK_COMPLETE = False def check_hydrus_availability(config: Dict[str, Any]) -> Tuple[bool, Optional[str]]: """Check if Hydrus API is available by pinging it. Args: config: Application configuration dictionary Returns: Tuple of (is_available: bool, reason: Optional[str]) - (True, None) if Hydrus is available - (False, reason) if Hydrus is unavailable with reason """ try: from helper.hydrus import is_available as _is_hydrus_available logger.info("[Hydrus Health Check] Pinging Hydrus API...") is_available, reason = _is_hydrus_available(config, use_cache=False) if is_available: logger.info("[Hydrus Health Check] ✅ Hydrus API is AVAILABLE") return True, None else: reason_str = f": {reason}" if reason else "" logger.warning(f"[Hydrus Health Check] ❌ Hydrus API is UNAVAILABLE{reason_str}") return False, reason except Exception as e: error_msg = str(e) logger.error(f"[Hydrus Health Check] ❌ Error checking Hydrus availability: {error_msg}") return False, error_msg def initialize_hydrus_health_check(config: Dict[str, Any]) -> None: """Initialize Hydrus health check at startup. This should be called once at application startup to determine if Hydrus features should be enabled or disabled. Args: config: Application configuration dictionary """ global _HYDRUS_AVAILABLE, _HYDRUS_UNAVAILABLE_REASON, _HYDRUS_CHECK_COMPLETE logger.info("[Startup] Starting Hydrus health check...") try: is_available, reason = check_hydrus_availability(config) _HYDRUS_AVAILABLE = is_available _HYDRUS_UNAVAILABLE_REASON = reason _HYDRUS_CHECK_COMPLETE = True if is_available: log("✅ Hydrus: ENABLED - All Hydrus features available", file=sys.stderr) else: log(f"⚠️ Hydrus: DISABLED - {reason or 'Connection failed'}", file=sys.stderr) log("- Export functionality disabled", file=sys.stderr) log("- Hydrus library features disabled", file=sys.stderr) log("- Hydrus tag operations disabled", file=sys.stderr) log("→ Local storage and All-Debrid features still available", file=sys.stderr) except Exception as e: logger.error(f"[Startup] Failed to initialize Hydrus health check: {e}", exc_info=True) _HYDRUS_AVAILABLE = False _HYDRUS_UNAVAILABLE_REASON = str(e) _HYDRUS_CHECK_COMPLETE = True log(f"⚠️ Hydrus: DISABLED - Error during health check: {e}", file=sys.stderr) def check_debrid_availability(config: Dict[str, Any]) -> Tuple[bool, Optional[str]]: """Check if Debrid API is available. Args: config: Application configuration dictionary Returns: Tuple of (is_available: bool, reason: Optional[str]) - (True, None) if Debrid API is available - (False, reason) if Debrid API is unavailable with reason """ try: from helper.http_client import HTTPClient logger.info("[Debrid Health Check] Pinging Debrid API at https://api.alldebrid.com/v4/ping...") try: # Use the public ping endpoint to check API availability # This endpoint doesn't require authentication with HTTPClient(timeout=10.0, verify_ssl=True) as client: response = client.get('https://api.alldebrid.com/v4/ping') logger.debug(f"[Debrid Health Check] Response status: {response.status_code}") # Read response text first (handles gzip decompression) try: response_text = response.text logger.debug(f"[Debrid Health Check] Response text: {response_text}") except Exception as e: logger.error(f"[Debrid Health Check] ❌ Failed to read response text: {e}") return False, f"Failed to read response: {e}" # Parse JSON try: result = response.json() logger.debug(f"[Debrid Health Check] Response JSON: {result}") except Exception as e: logger.error(f"[Debrid Health Check] ❌ Failed to parse JSON: {e}") logger.error(f"[Debrid Health Check] Response was: {response_text}") return False, f"Failed to parse response: {e}" # Validate response format if result.get('status') == 'success' and result.get('data', {}).get('ping') == 'pong': logger.info("[Debrid Health Check] ✅ Debrid API is AVAILABLE") return True, None else: logger.warning(f"[Debrid Health Check] ❌ Debrid API returned unexpected response: {result}") return False, "Invalid API response" except Exception as e: error_msg = str(e) logger.warning(f"[Debrid Health Check] ❌ Debrid API error: {error_msg}") import traceback logger.debug(f"[Debrid Health Check] Traceback: {traceback.format_exc()}") return False, error_msg except Exception as e: error_msg = str(e) logger.error(f"[Debrid Health Check] ❌ Error checking Debrid availability: {error_msg}") return False, error_msg def initialize_debrid_health_check(config: Dict[str, Any]) -> None: """Initialize Debrid health check at startup. This should be called once at application startup to determine if Debrid features should be enabled or disabled. Args: config: Application configuration dictionary """ global _DEBRID_AVAILABLE, _DEBRID_UNAVAILABLE_REASON, _DEBRID_CHECK_COMPLETE logger.info("[Startup] Starting Debrid health check...") try: is_available, reason = check_debrid_availability(config) _DEBRID_AVAILABLE = is_available _DEBRID_UNAVAILABLE_REASON = reason _DEBRID_CHECK_COMPLETE = True if is_available: log("✅ Debrid: ENABLED - All Debrid features available", file=sys.stderr) logger.info("[Startup] Debrid health check PASSED") else: log(f"⚠️ Debrid: DISABLED - {reason or 'Connection failed'}", file=sys.stderr) log("- Debrid export disabled", file=sys.stderr) log("- Debrid library features disabled", file=sys.stderr) log("→ Local storage and Hydrus features still available", file=sys.stderr) logger.warning(f"[Startup] Debrid health check FAILED: {reason}") except Exception as e: logger.error(f"[Startup] Failed to initialize Debrid health check: {e}", exc_info=True) _DEBRID_AVAILABLE = False _DEBRID_UNAVAILABLE_REASON = str(e) _DEBRID_CHECK_COMPLETE = True log(f"⚠️ Debrid: DISABLED - Error during health check: {e}", file=sys.stderr) def check_mpv_availability() -> Tuple[bool, Optional[str]]: """Check if MPV is available (installed and runnable). Returns: Tuple of (is_available: bool, reason: Optional[str]) """ global _MPV_AVAILABLE, _MPV_UNAVAILABLE_REASON, _MPV_CHECK_COMPLETE if _MPV_CHECK_COMPLETE and _MPV_AVAILABLE is not None: return _MPV_AVAILABLE, _MPV_UNAVAILABLE_REASON import shutil import subprocess logger.info("[MPV Health Check] Checking for MPV executable...") mpv_path = shutil.which("mpv") if not mpv_path: _MPV_AVAILABLE = False _MPV_UNAVAILABLE_REASON = "Executable 'mpv' not found in PATH" _MPV_CHECK_COMPLETE = True logger.warning(f"[MPV Health Check] ❌ MPV is UNAVAILABLE: {_MPV_UNAVAILABLE_REASON}") return False, _MPV_UNAVAILABLE_REASON # Try to get version to confirm it works try: result = subprocess.run( [mpv_path, "--version"], capture_output=True, text=True, timeout=2 ) if result.returncode == 0: version_line = result.stdout.split('\n')[0] _MPV_AVAILABLE = True _MPV_UNAVAILABLE_REASON = None _MPV_CHECK_COMPLETE = True logger.info(f"[MPV Health Check] ✅ MPV is AVAILABLE ({version_line})") return True, None else: _MPV_AVAILABLE = False _MPV_UNAVAILABLE_REASON = f"MPV returned non-zero exit code: {result.returncode}" _MPV_CHECK_COMPLETE = True logger.warning(f"[MPV Health Check] ❌ MPV is UNAVAILABLE: {_MPV_UNAVAILABLE_REASON}") return False, _MPV_UNAVAILABLE_REASON except Exception as e: _MPV_AVAILABLE = False _MPV_UNAVAILABLE_REASON = f"Error running MPV: {e}" _MPV_CHECK_COMPLETE = True logger.warning(f"[MPV Health Check] ❌ MPV is UNAVAILABLE: {_MPV_UNAVAILABLE_REASON}") return False, _MPV_UNAVAILABLE_REASON def initialize_mpv_health_check() -> None: """Initialize MPV health check at startup. This should be called once at application startup to determine if MPV features should be enabled or disabled. """ global _MPV_AVAILABLE, _MPV_UNAVAILABLE_REASON, _MPV_CHECK_COMPLETE logger.info("[Startup] Starting MPV health check...") try: is_available, reason = check_mpv_availability() _MPV_AVAILABLE = is_available _MPV_UNAVAILABLE_REASON = reason _MPV_CHECK_COMPLETE = True if is_available: log("✅ MPV: ENABLED - All MPV features available", file=sys.stderr) logger.info("[Startup] MPV health check PASSED") else: log(f"⚠️ MPV: DISABLED - {reason or 'Connection failed'}", file=sys.stderr) log("→ Hydrus features still available", file=sys.stderr) logger.warning(f"[Startup] MPV health check FAILED: {reason}") except Exception as e: logger.error(f"[Startup] Failed to initialize MPV health check: {e}", exc_info=True) _MPV_AVAILABLE = False _MPV_UNAVAILABLE_REASON = str(e) _MPV_CHECK_COMPLETE = True log(f"⚠️ MPV: DISABLED - Error during health check: {e}", file=sys.stderr) def is_hydrus_available() -> bool: """Check if Hydrus is available (from cached health check). Returns: True if Hydrus API is available, False otherwise """ return _HYDRUS_AVAILABLE is True def get_hydrus_unavailable_reason() -> Optional[str]: """Get the reason why Hydrus is unavailable. Returns: String explaining why Hydrus is unavailable, or None if available """ return _HYDRUS_UNAVAILABLE_REASON if not is_hydrus_available() else None def is_hydrus_check_complete() -> bool: """Check if the Hydrus health check has been completed. Returns: True if health check has run, False if still pending """ return _HYDRUS_CHECK_COMPLETE def disable_hydrus_features() -> None: """Manually disable all Hydrus features (for testing/fallback). This can be called if Hydrus connectivity is lost after startup. """ global _HYDRUS_AVAILABLE, _HYDRUS_UNAVAILABLE_REASON _HYDRUS_AVAILABLE = False _HYDRUS_UNAVAILABLE_REASON = "Manually disabled or lost connection" logger.warning("[Hydrus] Features manually disabled") def enable_hydrus_features() -> None: """Manually enable Hydrus features (for testing/fallback). This can be called if Hydrus connectivity is restored after startup. """ global _HYDRUS_AVAILABLE, _HYDRUS_UNAVAILABLE_REASON _HYDRUS_AVAILABLE = True _HYDRUS_UNAVAILABLE_REASON = None logger.info("[Hydrus] Features manually enabled") def is_debrid_available() -> bool: """Check if Debrid is available (from cached health check). Returns: True if Debrid API is available, False otherwise """ return _DEBRID_AVAILABLE is True def get_debrid_unavailable_reason() -> Optional[str]: """Get the reason why Debrid is unavailable. Returns: String explaining why Debrid is unavailable, or None if available """ return _DEBRID_UNAVAILABLE_REASON if not is_debrid_available() else None def is_debrid_check_complete() -> bool: """Check if the Debrid health check has been completed. Returns: True if health check has run, False if still pending """ return _DEBRID_CHECK_COMPLETE def disable_debrid_features() -> None: """Manually disable all Debrid features (for testing/fallback). This can be called if Debrid connectivity is lost after startup. """ global _DEBRID_AVAILABLE, _DEBRID_UNAVAILABLE_REASON _DEBRID_AVAILABLE = False _DEBRID_UNAVAILABLE_REASON = "Manually disabled or lost connection" logger.warning("[Debrid] Features manually disabled") def enable_debrid_features() -> None: """Manually enable Debrid features (for testing/fallback). This can be called if Debrid connectivity is restored after startup. """ global _DEBRID_AVAILABLE, _DEBRID_UNAVAILABLE_REASON _DEBRID_AVAILABLE = True _DEBRID_UNAVAILABLE_REASON = None logger.info("[Debrid] Features manually enabled") def is_mpv_available() -> bool: """Check if MPV is available (from cached health check). Returns: True if MPV is available, False otherwise """ return _MPV_AVAILABLE is True def get_mpv_unavailable_reason() -> Optional[str]: """Get the reason why MPV is unavailable. Returns: String explaining why MPV is unavailable, or None if available """ return _MPV_UNAVAILABLE_REASON if not is_mpv_available() else None def is_mpv_check_complete() -> bool: """Check if the MPV health check has been completed. Returns: True if health check has run, False if still pending """ return _MPV_CHECK_COMPLETE def disable_mpv_features() -> None: """Manually disable all MPV features (for testing/fallback). This can be called if MPV connectivity is lost after startup. """ global _MPV_AVAILABLE, _MPV_UNAVAILABLE_REASON _MPV_AVAILABLE = False _MPV_UNAVAILABLE_REASON = "Manually disabled or lost connection" logger.warning("[MPV] Features manually disabled") def enable_mpv_features() -> None: """Manually enable MPV features (for testing/fallback). This can be called if MPV connectivity is restored after startup. """ global _MPV_AVAILABLE, _MPV_UNAVAILABLE_REASON _MPV_AVAILABLE = True _MPV_UNAVAILABLE_REASON = None logger.info("[MPV] Features manually enabled")