This commit is contained in:
nose
2025-12-11 12:47:30 -08:00
parent 6b05dc5552
commit 65d12411a2
92 changed files with 17447 additions and 14308 deletions

View File

@@ -12,26 +12,14 @@ 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
# Global state for Matrix availability
_MATRIX_AVAILABLE: Optional[bool] = None
_MATRIX_UNAVAILABLE_REASON: Optional[str] = None
_MATRIX_CHECK_COMPLETE = False
# Global state for all service availability checks - consolidated from 12 separate globals
_SERVICE_STATE = {
"hydrus": {"available": None, "reason": None, "complete": False},
"hydrusnetwork_stores": {}, # Track individual Hydrus instances
"debrid": {"available": None, "reason": None, "complete": False},
"mpv": {"available": None, "reason": None, "complete": False},
"matrix": {"available": None, "reason": None, "complete": False},
}
# Global state for Cookies availability
_COOKIES_FILE_PATH: Optional[str] = None
@@ -68,130 +56,73 @@ def check_hydrus_availability(config: Dict[str, Any]) -> Tuple[bool, Optional[st
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
def initialize_hydrus_health_check(config: Dict[str, Any], emit_debug: bool = True) -> Tuple[bool, Optional[str]]:
"""Initialize Hydrus health check at startup."""
global _SERVICE_STATE
logger.info("[Startup] Starting Hydrus health check...")
is_available, reason = check_hydrus_availability(config)
_SERVICE_STATE["hydrus"]["available"] = is_available
_SERVICE_STATE["hydrus"]["reason"] = reason
_SERVICE_STATE["hydrus"]["complete"] = True
# Track individual Hydrus instances
try:
is_available, reason = check_hydrus_availability(config)
_HYDRUS_AVAILABLE = is_available
_HYDRUS_UNAVAILABLE_REASON = reason
_HYDRUS_CHECK_COMPLETE = True
if is_available:
debug("Hydrus: ENABLED - All Hydrus features available", file=sys.stderr)
else:
debug(f"Hydrus: DISABLED - {reason or 'Connection failed'}", file=sys.stderr)
store_config = config.get("store", {})
hydrusnetwork = store_config.get("hydrusnetwork", {})
for instance_name, instance_config in hydrusnetwork.items():
if isinstance(instance_config, dict):
url = instance_config.get("url")
access_key = instance_config.get("Hydrus-Client-API-Access-Key")
if url and access_key:
_SERVICE_STATE["hydrusnetwork_stores"][instance_name] = {
"ok": is_available,
"url": url,
"detail": reason if not is_available else "Connected"
}
else:
_SERVICE_STATE["hydrusnetwork_stores"][instance_name] = {
"ok": False,
"url": url or "Not configured",
"detail": "Missing credentials"
}
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
debug(f"Hydrus: DISABLED - Error during health check: {e}", file=sys.stderr)
logger.debug(f"Could not enumerate Hydrus instances: {e}")
if emit_debug:
status = 'ENABLED' if is_available else f'DISABLED - {reason or "Connection failed"}'
debug(f"Hydrus: {status}", file=sys.stderr)
return is_available, reason
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
"""
"""Check if Debrid API is available."""
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
logger.info("[Debrid Health Check] Pinging Debrid API...")
with HTTPClient(timeout=10.0, verify_ssl=True) as client:
response = client.get('https://api.alldebrid.com/v4/ping')
result = response.json()
if result.get('status') == 'success' and result.get('data', {}).get('ping') == 'pong':
logger.info("[Debrid Health Check] Debrid API is AVAILABLE")
return True, None
return False, "Invalid API response"
except Exception as e:
error_msg = str(e)
logger.error(f"[Debrid Health Check] ❌ Error checking Debrid availability: {error_msg}")
return False, error_msg
logger.warning(f"[Debrid Health Check] Debrid API error: {e}")
return False, str(e)
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
def initialize_debrid_health_check(config: Dict[str, Any], emit_debug: bool = True) -> Tuple[bool, Optional[str]]:
"""Initialize Debrid health check at startup."""
global _SERVICE_STATE
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:
debug("✅ Debrid: ENABLED - All Debrid features available", file=sys.stderr)
logger.info("[Startup] Debrid health check PASSED")
else:
debug(f"⚠️ Debrid: DISABLED - {reason or 'Connection failed'}", 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
debug(f"⚠️ Debrid: DISABLED - Error during health check: {e}", file=sys.stderr)
is_available, reason = check_debrid_availability(config)
_SERVICE_STATE["debrid"]["available"] = is_available
_SERVICE_STATE["debrid"]["reason"] = reason
_SERVICE_STATE["debrid"]["complete"] = True
if emit_debug:
status = 'ENABLED' if is_available else f'DISABLED - {reason or "Connection failed"}'
debug(f"Debrid: {status}", file=sys.stderr)
return is_available, reason
def check_mpv_availability() -> Tuple[bool, Optional[str]]:
@@ -200,10 +131,10 @@ def check_mpv_availability() -> Tuple[bool, Optional[str]]:
Returns:
Tuple of (is_available: bool, reason: Optional[str])
"""
global _MPV_AVAILABLE, _MPV_UNAVAILABLE_REASON, _MPV_CHECK_COMPLETE
global _SERVICE_STATE
if _MPV_CHECK_COMPLETE and _MPV_AVAILABLE is not None:
return _MPV_AVAILABLE, _MPV_UNAVAILABLE_REASON
if _SERVICE_STATE["mpv"]["complete"] and _SERVICE_STATE["mpv"]["available"] is not None:
return _SERVICE_STATE["mpv"]["available"], _SERVICE_STATE["mpv"]["reason"]
import shutil
import subprocess
@@ -212,11 +143,8 @@ def check_mpv_availability() -> Tuple[bool, Optional[str]]:
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
logger.warning(f"[MPV Health Check] ❌ MPV is UNAVAILABLE: Executable 'mpv' not found in PATH")
return False, "Executable 'mpv' not found in PATH"
# Try to get version to confirm it works
try:
@@ -228,55 +156,35 @@ def check_mpv_availability() -> Tuple[bool, Optional[str]]:
)
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})")
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
reason = f"MPV returned non-zero exit code: {result.returncode}"
logger.warning(f"[MPV Health Check] ❌ MPV is UNAVAILABLE: {reason}")
return False, 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
reason = f"Error running MPV: {e}"
logger.warning(f"[MPV Health Check] ❌ MPV is UNAVAILABLE: {reason}")
return False, 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
def initialize_mpv_health_check(emit_debug: bool = True) -> Tuple[bool, Optional[str]]:
"""Initialize MPV health check at startup and return (is_available, reason)."""
global _SERVICE_STATE
logger.info("[Startup] Starting MPV health check...")
is_available, reason = check_mpv_availability()
_SERVICE_STATE["mpv"]["available"] = is_available
_SERVICE_STATE["mpv"]["reason"] = reason
_SERVICE_STATE["mpv"]["complete"] = True
try:
is_available, reason = check_mpv_availability()
_MPV_AVAILABLE = is_available
_MPV_UNAVAILABLE_REASON = reason
_MPV_CHECK_COMPLETE = True
if emit_debug:
if is_available:
debug("MPV: ENABLED - All MPV features available", file=sys.stderr)
logger.info("[Startup] MPV health check PASSED")
else:
debug(f"⚠️ MPV: DISABLED - {reason or 'Connection failed'}", file=sys.stderr)
debug("→ 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
debug(f"⚠️ MPV: DISABLED - Error during health check: {e}", file=sys.stderr)
debug("MPV: ENABLED - All MPV features available", file=sys.stderr)
elif reason != "Not configured":
debug(f"MPV: DISABLED - {reason or 'Connection failed'}", file=sys.stderr)
return is_available, reason
def check_matrix_availability(config: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
@@ -324,264 +232,262 @@ def check_matrix_availability(config: Dict[str, Any]) -> Tuple[bool, Optional[st
return False, str(e)
def initialize_matrix_health_check(config: Dict[str, Any]) -> None:
"""Initialize Matrix health check at startup."""
global _MATRIX_AVAILABLE, _MATRIX_UNAVAILABLE_REASON, _MATRIX_CHECK_COMPLETE
def initialize_matrix_health_check(config: Dict[str, Any], emit_debug: bool = True) -> Tuple[bool, Optional[str]]:
"""Initialize Matrix health check at startup and return (is_available, reason)."""
global _SERVICE_STATE
logger.info("[Startup] Starting Matrix health check...")
is_available, reason = check_matrix_availability(config)
_SERVICE_STATE["matrix"]["available"] = is_available
_SERVICE_STATE["matrix"]["reason"] = reason
_SERVICE_STATE["matrix"]["complete"] = True
try:
is_available, reason = check_matrix_availability(config)
_MATRIX_AVAILABLE = is_available
_MATRIX_UNAVAILABLE_REASON = reason
_MATRIX_CHECK_COMPLETE = True
if emit_debug:
if is_available:
debug("Matrix: ENABLED - Homeserver reachable", file=sys.stderr)
else:
if reason != "Not configured":
debug(f"Matrix: DISABLED - {reason}", file=sys.stderr)
except Exception as e:
logger.error(f"[Startup] Failed to initialize Matrix health check: {e}", exc_info=True)
_MATRIX_AVAILABLE = False
_MATRIX_UNAVAILABLE_REASON = str(e)
_MATRIX_CHECK_COMPLETE = True
def is_hydrus_available() -> bool:
"""Check if Hydrus is available (from cached health check).
elif reason != "Not configured":
debug(f"Matrix: DISABLED - {reason}", file=sys.stderr)
Returns:
True if Hydrus API is available, False otherwise
"""
return _HYDRUS_AVAILABLE is True
return is_available, reason
# Unified getter functions for service availability - all use _SERVICE_STATE
def is_hydrus_available() -> bool:
"""Check if Hydrus is available (from cached health check)."""
return _SERVICE_STATE["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
"""Get the reason why Hydrus is unavailable."""
return _SERVICE_STATE["hydrus"]["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
"""Check if the Hydrus health check has been completed."""
return _SERVICE_STATE["hydrus"]["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"
"""Manually disable all Hydrus features (for testing/fallback)."""
global _SERVICE_STATE
_SERVICE_STATE["hydrus"]["available"] = False
_SERVICE_STATE["hydrus"]["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
"""Manually enable Hydrus features (for testing/fallback)."""
global _SERVICE_STATE
_SERVICE_STATE["hydrus"]["available"] = True
_SERVICE_STATE["hydrus"]["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
"""Check if Debrid is available (from cached health check)."""
return _SERVICE_STATE["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
"""Get the reason why Debrid is unavailable."""
return _SERVICE_STATE["debrid"]["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
"""Check if the Debrid health check has been completed."""
return _SERVICE_STATE["debrid"]["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"
"""Manually disable all Debrid features (for testing/fallback)."""
global _SERVICE_STATE
_SERVICE_STATE["debrid"]["available"] = False
_SERVICE_STATE["debrid"]["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
"""Manually enable Debrid features (for testing/fallback)."""
global _SERVICE_STATE
_SERVICE_STATE["debrid"]["available"] = True
_SERVICE_STATE["debrid"]["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
"""Check if MPV is available (from cached health check)."""
return _SERVICE_STATE["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
"""Get the reason why MPV is unavailable."""
return _SERVICE_STATE["mpv"]["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
"""Check if the MPV health check has been completed."""
return _SERVICE_STATE["mpv"]["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"
"""Manually disable all MPV features (for testing/fallback)."""
global _SERVICE_STATE
_SERVICE_STATE["mpv"]["available"] = False
_SERVICE_STATE["mpv"]["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
"""Manually enable MPV features (for testing/fallback)."""
global _SERVICE_STATE
_SERVICE_STATE["mpv"]["available"] = True
_SERVICE_STATE["mpv"]["reason"] = None
logger.info("[MPV] Features manually enabled")
def is_matrix_available() -> bool:
"""Check if Matrix is available (from cached health check).
Returns:
True if Matrix is available, False otherwise
"""
return _MATRIX_AVAILABLE is True
"""Check if Matrix is available (from cached health check)."""
return _SERVICE_STATE["matrix"]["available"] is True
def get_matrix_unavailable_reason() -> Optional[str]:
"""Get the reason why Matrix is unavailable.
Returns:
String explaining why Matrix is unavailable, or None if available
"""
return _MATRIX_UNAVAILABLE_REASON if not is_matrix_available() else None
"""Get the reason why Matrix is unavailable."""
return _SERVICE_STATE["matrix"]["reason"] if not is_matrix_available() else None
def is_matrix_check_complete() -> bool:
"""Check if the Matrix health check has been completed.
Returns:
True if health check has run, False if still pending
"""
return _MATRIX_CHECK_COMPLETE
"""Check if the Matrix health check has been completed."""
return _SERVICE_STATE["matrix"]["complete"]
def disable_matrix_features() -> None:
"""Manually disable all Matrix features (for testing/fallback).
This can be called if Matrix connectivity is lost after startup.
"""
global _MATRIX_AVAILABLE, _MATRIX_UNAVAILABLE_REASON
_MATRIX_AVAILABLE = False
_MATRIX_UNAVAILABLE_REASON = "Manually disabled or lost connection"
"""Manually disable all Matrix features (for testing/fallback)."""
global _SERVICE_STATE
_SERVICE_STATE["matrix"]["available"] = False
_SERVICE_STATE["matrix"]["reason"] = "Manually disabled or lost connection"
logger.warning("[Matrix] Features manually disabled")
def enable_matrix_features() -> None:
"""Manually enable Matrix features (for testing/fallback).
This can be called if Matrix connectivity is restored after startup.
"""
global _MATRIX_AVAILABLE, _MATRIX_UNAVAILABLE_REASON
_MATRIX_AVAILABLE = True
_MATRIX_UNAVAILABLE_REASON = None
"""Manually enable Matrix features (for testing/fallback)."""
global _SERVICE_STATE
_SERVICE_STATE["matrix"]["available"] = True
_SERVICE_STATE["matrix"]["reason"] = None
logger.info("[Matrix] Features manually enabled")
def initialize_local_library_scan(config: Dict[str, Any]) -> None:
"""Initialize and scan local library at startup.
def initialize_local_library_scan(config: Dict[str, Any], emit_debug: bool = True) -> Tuple[bool, str]:
"""Initialize and scan all folder stores at startup.
Returns a tuple of (success, detail_message).
This ensures that any new files in the local library folder are indexed
Note: Individual store results are stored in _SERVICE_STATE["folder_stores"]
for the CLI to display as separate table rows.
This ensures that any new files in configured folder stores are indexed
and their sidecar files are imported and cleaned up.
"""
from config import get_local_storage_path
from helper.local_library import LocalLibraryInitializer
from helper.folder_store import LocalLibraryInitializer
from helper.store import Folder
logger.info("[Startup] Starting Local Library scan...")
logger.info("[Startup] Starting folder store scans...")
try:
storage_path = get_local_storage_path(config)
if not storage_path:
debug("⚠️ Local Library: SKIPPED - No storage path configured", file=sys.stderr)
return
# Get all configured folder stores from config
folder_sources = config.get("store", {}).get("folder", {})
if not isinstance(folder_sources, dict) or not folder_sources:
if emit_debug:
debug("⚠️ Folder stores: SKIPPED - No folder stores configured", file=sys.stderr)
return False, "No folder stores configured"
results = []
total_new_files = 0
total_sidecars = 0
failed_stores = []
store_results = {}
for store_name, store_config in folder_sources.items():
if not isinstance(store_config, dict):
continue
debug(f"Scanning local library at: {storage_path}", file=sys.stderr)
initializer = LocalLibraryInitializer(storage_path)
stats = initializer.scan_and_index()
store_path = store_config.get("path")
if not store_path:
continue
try:
from pathlib import Path
storage_path = Path(str(store_path)).expanduser()
if emit_debug:
debug(f"Scanning folder store '{store_name}' at: {storage_path}", file=sys.stderr)
# Migrate the folder store to hash-based naming (only runs once per location)
Folder.migrate_location(str(storage_path))
initializer = LocalLibraryInitializer(storage_path)
stats = initializer.scan_and_index()
# Accumulate stats
new_files = stats.get('files_new', 0)
sidecars = stats.get('sidecars_imported', 0)
total_new_files += new_files
total_sidecars += sidecars
# Record result for this store
if new_files > 0 or sidecars > 0:
result_detail = f"New: {new_files}, Sidecars: {sidecars}"
if emit_debug:
debug(f" {store_name}: {result_detail}", file=sys.stderr)
else:
result_detail = "Up to date"
if emit_debug:
debug(f" {store_name}: {result_detail}", file=sys.stderr)
results.append(f"{store_name}: {result_detail}")
store_results[store_name] = {
"path": str(storage_path),
"detail": result_detail,
"ok": True
}
except Exception as e:
logger.error(f"[Startup] Failed to scan folder store '{store_name}': {e}", exc_info=True)
if emit_debug:
debug(f" {store_name}: ERROR - {e}", file=sys.stderr)
failed_stores.append(store_name)
store_results[store_name] = {
"path": str(store_config.get("path", "?")),
"detail": f"ERROR - {e}",
"ok": False
}
# Log summary
new_files = stats.get('files_new', 0)
sidecars = stats.get('sidecars_imported', 0)
# Store individual results for CLI to display
_SERVICE_STATE["folder_stores"] = store_results
if new_files > 0 or sidecars > 0:
debug(f"✅ Local Library: Scanned - New files: {new_files}, Sidecars imported: {sidecars}", file=sys.stderr)
# Build detail message
if failed_stores:
detail = f"Scanned {len(results)} stores ({len(failed_stores)} failed); Total new: {total_new_files}, Sidecars: {total_sidecars}"
if emit_debug:
debug(f"Folder stores scan complete: {detail}", file=sys.stderr)
return len(failed_stores) < len(results), detail
else:
debug("✅ Local Library: Up to date", file=sys.stderr)
detail = f"Scanned {len(results)} stores; Total new: {total_new_files}, Sidecars: {total_sidecars}"
if emit_debug:
debug(f"Folder stores scan complete: {detail}", file=sys.stderr)
return True, detail
except Exception as e:
logger.error(f"[Startup] Failed to scan local library: {e}", exc_info=True)
debug(f"⚠️ Local Library: ERROR - Scan failed: {e}", file=sys.stderr)
logger.error(f"[Startup] Failed to scan folder stores: {e}", exc_info=True)
if emit_debug:
debug(f"⚠️ Folder stores: ERROR - Scan failed: {e}", file=sys.stderr)
return False, f"Scan failed: {e}"
def initialize_cookies_check() -> None:
"""Check for cookies.txt in the application root directory."""
def initialize_cookies_check(emit_debug: bool = True) -> Tuple[bool, str]:
"""Check for cookies.txt in the application root directory.
Returns a tuple of (found, detail_message).
"""
global _COOKIES_FILE_PATH
# Assume CLI.py is in the root
@@ -590,10 +496,12 @@ def initialize_cookies_check() -> None:
if cookies_path.exists():
_COOKIES_FILE_PATH = str(cookies_path)
debug(f"✅ Cookies: ENABLED - Found cookies.txt", file=sys.stderr)
if emit_debug:
debug(f"Cookies: ENABLED - Found cookies.txt", file=sys.stderr)
return True, str(cookies_path)
else:
_COOKIES_FILE_PATH = None
# debug(" Cookies: Using browser cookies (fallback)", file=sys.stderr)
return False, "Not found"
def get_cookies_file_path() -> Optional[str]: