3391fbc85d
TestResult dataclass + translate_connection_error shared helper in base.py. test_callable field on ServiceDefinition (default None). 7 per-type test_connection routines (qbittorrent, prometheus via Grafana gateway, alertmanager, jellyfin, authentik, ssh_tasks via build_ssh_client, nextcloud). POST /api/services/test endpoint: validation-first (422 on malformed config), dispatch, no-persistence, no-secret-logs. backups has test_callable=None. qBit 'Fails.' → specific auth message (resolves #3 at API layer). Backend: 362 pytest pass (+31 new), ruff clean. Frontend: build green.
98 lines
2.9 KiB
Python
98 lines
2.9 KiB
Python
"""Jellyfin service definition."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from media_library_viewer_api.clients.jellyfin import JellyfinClient
|
|
from media_library_viewer_api.integrations.base import (
|
|
SecretField,
|
|
ServiceBaseUrl,
|
|
ServiceConfigBase,
|
|
ServiceDefinition,
|
|
TestResult,
|
|
WidgetConfigBase,
|
|
translate_connection_error,
|
|
widget_kind,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from media_library_viewer_api.services.settings_store import SettingsStore
|
|
|
|
|
|
def test_connection(
|
|
config: dict[str, Any],
|
|
secrets: dict[str, str],
|
|
store: SettingsStore,
|
|
) -> TestResult:
|
|
"""Call JellyfinClient.users() — the lightest authenticated probe."""
|
|
try:
|
|
base_url = str(config.get("base_url") or "")
|
|
api_key = str(secrets.get("api_key") or "")
|
|
timeout = int(config.get("timeout_seconds") or 10)
|
|
client = JellyfinClient(base_url, api_key, timeout=timeout)
|
|
users = client.users()
|
|
return TestResult(ok=True, detail="Connected to Jellyfin.", evidence=f"{len(users)} users")
|
|
except Exception as exc:
|
|
return translate_connection_error(exc, context="Jellyfin")
|
|
|
|
|
|
class JellyfinConfig(ServiceConfigBase):
|
|
"""Non-secret Jellyfin connection config.
|
|
|
|
The optional ``jellyseerr_url`` / ``jellyseerr_api_key`` fields carry the
|
|
paired Jellyseerr companion config, absorbed from the former standalone
|
|
``jellyseerr`` service type (see OpenSpec change ``services-as-hub-ia``).
|
|
When both are set, the Jellyfin service page renders a Requests tab backed
|
|
by Jellyseerr.
|
|
"""
|
|
|
|
base_url: ServiceBaseUrl
|
|
user_id: str = ""
|
|
timeout_seconds: int = 10
|
|
jellyseerr_url: str = ""
|
|
jellyseerr_api_key: str = ""
|
|
|
|
|
|
class JellyfinActivityWidgetConfig(WidgetConfigBase):
|
|
"""Live Jellyfin session activity."""
|
|
|
|
# No user-overridable fields; the service record carries user_id.
|
|
pass
|
|
|
|
|
|
class JellyfinNowPlayingWidgetConfig(WidgetConfigBase):
|
|
"""Only show sessions with active playback (not idle/paused)."""
|
|
|
|
pass
|
|
|
|
|
|
DEFINITION = ServiceDefinition(
|
|
service_type="jellyfin",
|
|
name="Jellyfin",
|
|
description="Media server with live session activity.",
|
|
config_model=JellyfinConfig,
|
|
secret_fields=[
|
|
SecretField(key="api_key", label="API key", required=True),
|
|
],
|
|
widget_kinds=[
|
|
widget_kind(
|
|
kind="activity",
|
|
name="Activity",
|
|
description="Live sessions and idle users.",
|
|
model_cls=JellyfinActivityWidgetConfig,
|
|
default_config={},
|
|
refresh_interval_ms=30_000,
|
|
),
|
|
widget_kind(
|
|
kind="now_playing",
|
|
name="Now Playing",
|
|
description="Only sessions actively playing media.",
|
|
model_cls=JellyfinNowPlayingWidgetConfig,
|
|
default_config={},
|
|
refresh_interval_ms=30_000,
|
|
),
|
|
],
|
|
test_callable=test_connection,
|
|
)
|