Files
Medios-Macina/hydrus_health_check.py
2025-11-25 20:09:33 -08:00

426 lines
15 KiB
Python

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