"""Dependency injection for FastAPI. Provides access to machine-specific Jellyfin/SSH clients via FastAPI's request context. The selected machine can be chosen with a ``machine_id`` query parameter; otherwise the backend falls back to the first enabled machine that matches the requested service. """ from __future__ import annotations import logging from functools import lru_cache from typing import Any from fastapi import Request from media_library_viewer_api.clients.jellyfin import JellyfinClient from media_library_viewer_api.clients.jellyseerr import JellyseerrClient from media_library_viewer_api.clients.local import LocalCommandClient from media_library_viewer_api.clients.ssh import RemoteSSHClient from media_library_viewer_api.config import get_settings from media_library_viewer_api.services.mail_queue import MailQueue, get_mail_queue as _get_mail_queue from media_library_viewer_api.services.monitoring_poller import ( MonitoringPoller, get_monitoring_poller as _get_monitoring_poller, ) from media_library_viewer_api.services.settings_store import SettingsStore, get_settings_store as _get_settings_store logger = logging.getLogger(__name__) def _request_machine_id(request: Request | None) -> str | None: if request is None: return None machine_id = request.query_params.get("machine_id") return machine_id or None @lru_cache(maxsize=32) def _jellyfin_client_for(cache_key: tuple[str, str, str]) -> JellyfinClient: machine_id, url, api_key = cache_key logger.info("Creating Jellyfin client machine_id=%s url=%s", machine_id or "", url.rstrip("/") or "") return JellyfinClient(url, api_key) @lru_cache(maxsize=32) def _jellyseerr_client_for(cache_key: tuple[str, str]) -> JellyseerrClient | None: machine_id, url = cache_key if not url: return None settings = get_settings_store().get_machine_config(machine_id) if machine_id else None api_key = (settings or {}).get("jellyseerr_api_key") if settings else "" if not api_key: return None logger.info("Creating Jellyseerr client machine_id=%s url=%s", machine_id or "", url.rstrip("/") or "") return JellyseerrClient(url, api_key) @lru_cache(maxsize=32) def _ssh_client_for(cache_key: tuple[str, str, str, int, str, str | None, str | None, str | None, str | None]) -> RemoteSSHClient: machine_id, host, username, port, key_filename, password, private_key, private_key_passphrase, known_hosts_path = cache_key logger.info( "Creating SSH client machine_id=%s host=%s user=%s port=%s key=%s password=%s private_key=%s passphrase=%s", machine_id or "", host or "", username or "", port, key_filename or "", "set" if password else "missing", "set" if private_key else "missing", "set" if private_key_passphrase else "missing", ) client = RemoteSSHClient( host=host, username=username, port=port, key_filename=key_filename or None, private_key=private_key or None, private_key_passphrase=private_key_passphrase or None, password=password or None, known_hosts_path=known_hosts_path or None, ) try: client.connect() except Exception: logger.exception("Failed to establish SSH connection to %s", host or "") raise return client def _resolve_machine(service: str, request: Request | None = None) -> dict[str, Any] | None: store = get_settings_store() machine_id = _request_machine_id(request) if machine_id: machine = store.get_machine(machine_id) if machine and (service in machine.get("services", []) or service == "ssh"): return machine return machine if service == "jellyfin": machines = store.list_machines_for_service("jellyfin") elif service == "jellyseerr": machines = [m for m in store.list_machines_for_service("jellyfin") if m.get("jellyseerr_url")] elif service == "ssh": machines = store.list_machines_for_service("files") or store.list_machines_for_service("monitoring") else: machines = store.list_machines_for_service(service) return machines[0] if machines else None def get_jellyfin_client(request: Request = None) -> JellyfinClient: """Return a Jellyfin client for the selected machine or legacy env fallback.""" machine = get_settings_store().get_machine_config(_request_machine_id(request)) or _resolve_machine("jellyfin", request) if machine and machine.get("jellyfin_url") and machine.get("jellyfin_api_key"): cache_key = (machine["id"], machine["jellyfin_url"], machine.get("jellyfin_api_key") or "") return _jellyfin_client_for(cache_key) settings = get_settings() if not settings.jellyfin_url or not settings.jellyfin_api_key: raise RuntimeError("No Jellyfin machine is configured and JELLYFIN_URL/API_KEY are not set") logger.info("Falling back to legacy Jellyfin env settings") return _jellyfin_client_for(("legacy", settings.jellyfin_url, settings.jellyfin_api_key)) def get_jellyseerr_client(request: Request = None) -> JellyseerrClient | None: """Return a cached Jellyseerr client when configured, otherwise None.""" machine = get_settings_store().get_machine_config(_request_machine_id(request)) or _resolve_machine("jellyseerr", request) if machine and machine.get("jellyseerr_url") and machine.get("jellyseerr_api_key"): return JellyseerrClient(machine["jellyseerr_url"], machine.get("jellyseerr_api_key") or "") settings = get_settings() if not settings.jellyseerr_url or not settings.jellyseerr_api_key: logger.info( "Jellyseerr client not configured (url=%s, api_key=%s)", "set" if settings.jellyseerr_url else "missing", "set" if settings.jellyseerr_api_key else "missing", ) return None logger.info("Falling back to legacy Jellyseerr env settings") return JellyseerrClient(settings.jellyseerr_url, settings.jellyseerr_api_key) def get_ssh_client(request: Request = None): """Return a command client for the selected machine or legacy env fallback.""" store = get_settings_store() machine_id = _request_machine_id(request) machine = store.get_machine_config(machine_id) if machine_id else None if machine is None: machine_ref = _resolve_machine("ssh", request) machine = store.get_machine_config(machine_ref["id"]) if machine_ref else None if machine and str(machine.get("mode") or "local").strip().lower() == "local": logger.info("Creating LocalCommandClient machine_id=%s", machine["id"]) return LocalCommandClient() if machine and machine.get("host") and machine.get("username"): known_hosts_path = get_settings().ssh_known_hosts_file key_data = None key_passphrase = None ssh_key_id = str(machine.get("ssh_key_id") or "").strip() if ssh_key_id: ssh_key = store.get_ssh_key(ssh_key_id) if ssh_key: key_data = ssh_key.get("private_key") or None key_passphrase = ssh_key.get("passphrase") or None if not key_data and machine.get("ssh_private_key"): key_data = machine.get("ssh_private_key") or None key_passphrase = machine.get("ssh_private_key_passphrase") or None cache_key = ( machine["id"], machine["host"], machine["username"], int(machine.get("port") or 22), f"{machine.get('key_directory')}/{machine.get('key_name')}" if machine.get("key_directory") and machine.get("key_name") else "", machine.get("password") or None, key_data, key_passphrase, str(known_hosts_path), ) return _ssh_client_for(cache_key) settings = get_settings() logger.info( "Creating SSH client from legacy env host=%s user=%s port=%s key_dir=%s key_name=%s password=%s", settings.ssh_host or "", settings.ssh_username or "", settings.ssh_port, settings.ssh_key_directory or "", settings.ssh_key_name or "", "set" if settings.ssh_password else "missing", ) if not settings.ssh_key_path: raise RuntimeError("No SSH machine is configured and SSH key settings must be configured") return _ssh_client_for(("legacy", settings.ssh_host, settings.ssh_username, settings.ssh_port, settings.ssh_key_path, settings.ssh_password or None, None, None, str(settings.ssh_known_hosts_file))) def get_mail_queue() -> MailQueue: """Return the singleton background email queue.""" return _get_mail_queue() def get_monitoring_poller() -> MonitoringPoller: """Return the singleton background monitoring poller.""" return _get_monitoring_poller() def get_settings_store() -> SettingsStore: """Return the singleton persistent settings store.""" return _get_settings_store() def get_user_id(request: Request = None) -> str: """Return the configured Jellyfin user ID or discover the first available one.""" machine = get_settings_store().get_machine_config(_request_machine_id(request)) or _resolve_machine("jellyfin", request) if machine and machine.get("jellyfin_user_id"): return str(machine["jellyfin_user_id"]) settings = get_settings() if settings.jellyfin_user_id: return settings.jellyfin_user_id client = get_jellyfin_client(request) users = client.users() if not users: raise RuntimeError("No Jellyfin users found and no machine/user id configured") return users[0]["Id"]