167 lines
6.8 KiB
Python
167 lines
6.8 KiB
Python
"""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 "<default>", url.rstrip("/") or "<unset>"
|
|
)
|
|
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)
|