b8cb29e330
Groundwork for Jellyseerr request stats in the Jellyfin service, behind a
small reusable abstraction so future stats services (Sonarr/Radarr) reuse it.
Backend:
- JellyseerrClient.request_count() -> /api/v1/request/count (normalized
total/pending/approved/declined/processing/available) and recent_requests()
-> /api/v1/request mapped to {name,type,status,media_status,created_at}
with numeric status enums labelled.
- widgets/stats_provider.py: StatsProvider protocol + registry keyed by
service_type (StatValue/StatsResult). A thin generic interface.
- widgets/jellyseerr_stats.py: JellyseerrStatsProvider registered for the
Jellyfin service; reuses one authenticated client per service (lru_cache) and
caches the StatsResult for ~10s under a lock, so multiple widgets + the tab
collapse onto one Jellyseerr fetch (same lesson as the qBittorrent client).
Accepts jellyseerr_api_key from secrets OR config during the upcoming
config->secret migration.
- Jellyfin service gains two widget kinds: `stat` (a Literal selector over the
six stats — the "extract one value into a widget" affordance) and
`stats_overview` (all stats + recent list).
- widgets router routes widget_kind in {stat, stats_overview} to a generic
StatsWidgetSource (dispatches to the service type's provider), independent of
service type.
- new /api/jellyseerr/stats router endpoint for the Requests tab (resolves the
Jellyfin service by id or first-enabled; shares the provider cache).
Tests: provider normalization, not-configured, TTL caching; stat selector +
overview + unknown-stat widget dispatch; 7 new tests. 400/400 backend pass;
ruff clean.
63 lines
1.9 KiB
Python
63 lines
1.9 KiB
Python
"""Generic stats-provider abstraction.
|
|
|
|
A service type that exposes a set of named numeric metrics (Jellyseerr request
|
|
stats today; Sonarr/Radarr later) registers a :class:`StatsProvider`. The
|
|
widget layer renders the provider's output as either a single-stat widget (a
|
|
selector picks one metric) or a stats-overview grid. Keeping this behind a
|
|
small interface means future stats services reuse the same widgets + tab
|
|
without per-service widget kinds.
|
|
|
|
Providers are synchronous (they do blocking HTTP) and are run in a thread by
|
|
the widget source / router. A provider should cache/de-duplicate fetches so
|
|
that several widgets + the tab don't each hit the upstream service.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Protocol, runtime_checkable
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StatValue:
|
|
"""One named metric."""
|
|
|
|
key: str
|
|
label: str
|
|
value: int | float
|
|
|
|
|
|
@dataclass
|
|
class StatsResult:
|
|
"""Normalized output of a stats provider."""
|
|
|
|
stats: list[StatValue]
|
|
recent: list[dict[str, Any]] = field(default_factory=list)
|
|
# Optional human note (e.g. "not configured"); surfaced as an error when
|
|
# there are no stats.
|
|
detail: str | None = None
|
|
|
|
|
|
@runtime_checkable
|
|
class StatsProvider(Protocol):
|
|
"""Return the current stats for a service instance.
|
|
|
|
``service`` is a duck-typed record with ``id``, ``service_type``,
|
|
``config`` and ``secrets`` (see widgets.sources.ServiceRecord).
|
|
"""
|
|
|
|
def fetch_stats(self, service: Any) -> StatsResult: ...
|
|
|
|
|
|
# Registry keyed by service_type. A service type with no provider simply has no
|
|
# stat widgets available.
|
|
STATS_PROVIDERS: dict[str, StatsProvider] = {}
|
|
|
|
|
|
def register_stats_provider(service_type: str, provider: StatsProvider) -> None:
|
|
STATS_PROVIDERS[service_type] = provider
|
|
|
|
|
|
def get_stats_provider(service_type: str) -> StatsProvider | None:
|
|
return STATS_PROVIDERS.get(service_type)
|