"""Dependency injection for FastAPI. Provides access to service-specific Jellyfin/Jellyseerr clients and remote-machine SSH clients via FastAPI's request context. - Jellyfin/Jellyseerr are selected with a ``jellyfin_service_id`` query parameter (resolved against the service registry); the backend falls back to the first enabled ``jellyfin``/``jellyseerr`` service instance. - SSH/Files transport is selected with an enabled ``remote_machine`` ``service_id``. """ from __future__ import annotations import logging from functools import lru_cache from typing import Any from fastapi import HTTPException, Request from media_library_viewer_api.clients.jellyfin import JellyfinClient from media_library_viewer_api.clients.ssh import RemoteSSHClient from media_library_viewer_api.services.mail_queue import MailQueue from media_library_viewer_api.services.mail_queue import get_mail_queue as _get_mail_queue from media_library_viewer_api.services.settings_store import SettingsStore from media_library_viewer_api.services.settings_store import get_settings_store as _get_settings_store logger = logging.getLogger(__name__) def _request_remote_machine_service_id(request: Request | None) -> str | None: if request is None: return None service_id = request.query_params.get("service_id") return service_id or None def _request_jellyfin_service_id(request: Request | None) -> str | None: if request is None: return None service_id = request.query_params.get("jellyfin_service_id") return service_id or None def _service_record(store: SettingsStore, service_type: str, service_id: str | None) -> dict[str, Any] | None: """Return a service row for a type, preferring the requested id. The row carries an in-memory decrypted ``secrets`` dict. Returns None if no enabled instance of the type exists. """ from media_library_viewer_api.services.secrets import decrypt_secrets row = None if service_id: candidate = store.get_service(service_id) if candidate and candidate.get("service_type") == service_type and candidate.get("enabled", True): row = candidate if row is None: instances = [s for s in store.list_services(service_type) if s.get("enabled", True)] row = instances[0] if instances else None if row is None: return None decrypted = {} blob = row.get("secrets") or {} if blob: try: decrypted = decrypt_secrets(blob) except Exception: logger.exception("Failed to decrypt service secrets service_id=%s", row.get("id")) return {**row, "secrets": decrypted} @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) def get_jellyfin_client(request: Request) -> JellyfinClient: """Return a Jellyfin client for the selected enabled service instance.""" store = get_settings_store() service = _service_record(store, "jellyfin", _request_jellyfin_service_id(request)) if service is None: raise HTTPException( status_code=503, detail="No Jellyfin service is configured. Add a Jellyfin service on the Services page.", ) base_url = str(service.get("config", {}).get("base_url") or "") api_key = str(service.get("secrets", {}).get("api_key") or "") if not base_url or not api_key: raise HTTPException( status_code=503, detail="Jellyfin service is missing base_url or api_key. Edit it on the Services page.", ) return _jellyfin_client_for((service["id"], base_url, api_key)) def get_ssh_client(request: Request) -> RemoteSSHClient: """Return SSH transport for the requested enabled remote-machine service.""" from media_library_viewer_api.services.task_runner import build_ssh_client from media_library_viewer_api.widgets.sources import build_service_record store = get_settings_store() service_id = _request_remote_machine_service_id(request) if not service_id: raise HTTPException(status_code=400, detail="service_id is required for remote file and job operations") row = store.get_service(service_id) if not row or row.get("service_type") != "remote_machine" or not row.get("enabled", True): raise HTTPException(status_code=404, detail="Enabled remote machine service not found") try: return build_ssh_client(store, build_service_record(store, row)) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc def get_mail_queue() -> MailQueue: """Return the singleton background email queue.""" return _get_mail_queue() def get_settings_store() -> SettingsStore: """Return the singleton persistent settings store.""" return _get_settings_store() def get_user_id(request: Request) -> str: """Return the Jellyfin user Id, resolving a configured username if needed. The service ``user_id`` config field accepts either the internal Jellyfin Id or a username (e.g. ``'admin'``). Jellyfin's ``/Users/{id}/...`` endpoints reject usernames with HTTP 400 (``"The value 'admin' is not valid."``), so always resolve to the internal Id before use. Resolution is cached per (service, base_url, api_key, configured) so repeated dashboard/media requests don't re-list users on every call. """ store = get_settings_store() service_id = _request_jellyfin_service_id(request) service = _service_record(store, "jellyfin", service_id) if service is None: raise HTTPException( status_code=503, detail="No Jellyfin service is configured. Add a Jellyfin service on the Services page.", ) configured = str(service.get("config", {}).get("user_id") or "").strip() base_url = str(service.get("config", {}).get("base_url") or "") api_key = str(service.get("secrets", {}).get("api_key") or "") if not base_url or not api_key: raise HTTPException( status_code=503, detail="Jellyfin service is missing base_url or api_key. Edit it on the Services page.", ) return _resolved_user_id((service["id"], base_url, api_key, configured)) @lru_cache(maxsize=64) def _resolved_user_id(cache_key: tuple[str, str, str, str]) -> str: """Resolve a configured Jellyfin identifier (Id or username) to the internal Id. Keyed by (service_id, base_url, api_key, configured) so a credentials change or a different configured user busts the cache automatically. """ service_id, base_url, api_key, configured = cache_key client = _jellyfin_client_for((service_id, base_url, api_key)) return client.resolve_user_id(configured or None)