feat(service-storage-harness): slice 2 — qbit widgets + LineSeriesChart extract

This commit is contained in:
Developer
2026-07-09 08:46:20 +00:00
parent e7bd0afdd1
commit 1fb12b8a0a
14 changed files with 831 additions and 81 deletions
@@ -19,11 +19,13 @@ from typing import Any, Protocol
import requests
from media_library_viewer_api.clients.jellyfin import JellyfinClient
from media_library_viewer_api.clients.qbittorrent import QbittorrentClient
from media_library_viewer_api.domain.dashboard import (
_map_sessions_to_activity_rows,
build_backup_dashboard_summary,
)
from media_library_viewer_api.integrations.alertmanager import summarize_alerts
from media_library_viewer_api.services.qbittorrent_store import QbittorrentSampleStore
from media_library_viewer_api.services.settings_store import SettingsStore, get_settings_store
from media_library_viewer_api.services.task_runner import run_saved_task
from media_library_viewer_api.widgets.prometheus_range import (
@@ -368,12 +370,75 @@ def _record_timeout(service: ServiceRecord | None, config: dict[str, Any], timeo
logger.exception("failed to record ssh task timeout")
class QbittorrentWidgetSource:
"""Fetch qBittorrent data for totals, active, and speed widgets."""
async def fetch(self, service: ServiceRecord | None, widget_kind: str, config: dict[str, Any]) -> dict[str, Any]:
try:
if service is None:
return {"error": "qBittorrent widget is missing its service"}
base_url = str(service.config.get("base_url") or "")
username = str(service.secrets.get("username") or "")
password = str(service.secrets.get("password") or "")
timeout = int(service.config.get("timeout_seconds") or 10)
if not base_url or not username or not password:
return {"error": "qBittorrent service is missing base_url, username, or password"}
client = QbittorrentClient(base_url, username, password, timeout)
data = await asyncio.wait_for(asyncio.to_thread(client.maindata), timeout=timeout)
server_state = data.get("server_state", {})
torrents = data.get("torrents", {})
if widget_kind == "totals":
by_state: dict[str, int] = {}
for t in torrents.values():
state = str(t.get("state", "unknown"))
by_state[state] = by_state.get(state, 0) + 1
return {"total": len(torrents), "by_state": by_state}
if widget_kind == "active":
active = [
{
"name": t.get("name"),
"state": t.get("state"),
"size": t.get("size"),
"progress": t.get("progress"),
"dl_speed": t.get("dlspeed"),
"up_speed": t.get("upspeed"),
}
for t in torrents.values()
if str(t.get("state", "")) in {"downloading", "uploading"}
]
return {"torrents": active}
if widget_kind == "speed":
dl = int(server_state.get("dl_info_speed", 0))
up = int(server_state.get("up_info_speed", 0))
ts = int(time.time())
store = QbittorrentSampleStore()
store.append(service.id, ts, dl, up)
samples = store.window(service.id)
series = [
{"label": "download", "points": [{"t": s["ts"] * 1000, "v": s["dl_speed"]} for s in samples]},
{"label": "upload", "points": [{"t": s["ts"] * 1000, "v": s["up_speed"]} for s in samples]},
]
return {"series": series}
return {"error": f"Unknown qBittorrent widget kind: {widget_kind}"}
except asyncio.TimeoutError:
return {"error": "qBittorrent data fetch timed out"}
except Exception as exc:
logger.exception("qbittorrent adapter failed")
return {"error": f"qBittorrent fetch failed: {exc}"}
# ---------------------------------------------------------------------------
# Registries
# ---------------------------------------------------------------------------
SERVICE_ADAPTERS: dict[str, WidgetSource] = {
"prometheus": PrometheusWidgetSource(),
"qbittorrent": QbittorrentWidgetSource(),
"alertmanager": AlertmanagerWidgetSource(),
"jellyfin": JellyfinWidgetSource(),
"ssh_tasks": SshTaskWidgetSource(),