Files
manage/backend/src/media_library_viewer_api/integrations/qbittorrent.py
T
Developer 05a9faca3e feat(widgets): add unit/scale config to chart widget kinds
Graph widgets need consistent value scaling (kB/MB/GB, kbps/Mbps, …). Add
shared `unit` (none/bytes/bytes_per_sec/bits_per_sec/bits/percent/seconds) and
`scale` (auto/k/m/g/t) enum fields to:

- PrometheusChartWidgetConfig (alongside promql/window)
- new QbittorrentSpeedWidgetConfig — the speed widget previously had NO config
  options at all; totals/active keep their empty config.

Declared as Pydantic Literal enums so the widget-kind JSON schema exposes
`enum`, which the frontend config dialog renders as a dropdown. The data
sources are unchanged (raw values); scaling is a display concern handled
client-side. qBittorrent speed defaults to bytes/sec.

Test: chart widget kinds expose the shared unit/scale enums; speed defaults to
bytes_per_sec; totals/active stay option-less. 389/389 backend tests pass.
2026-07-12 11:45:25 +00:00

119 lines
4.0 KiB
Python

"""qBittorrent service definition.
Declares the config model (base URL + timeout), secret fields (username +
password), and three widget kinds (totals, active, speed). Models on
:mod:`media_library_viewer_api.integrations.prometheus`.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Literal
from media_library_viewer_api.clients.qbittorrent import QbittorrentClient
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:
"""Login + probe maindata; surface auth failures specifically."""
try:
base_url = str(config.get("base_url") or "")
username = str(secrets.get("username") or "")
password = str(secrets.get("password") or "")
timeout = int(config.get("timeout_seconds") or 60)
client = QbittorrentClient(base_url, username, password, timeout=timeout)
data = client.maindata()
version = str(data.get("server_state", {}).get("qbittorrent_version", "") or "connected")
return TestResult(ok=True, detail="Connected to qBittorrent.", evidence=version)
except RuntimeError as exc:
lowered = str(exc).lower()
if "invalid username or password" in lowered:
return TestResult(ok=False, detail="Authentication failed — qBittorrent rejected the credentials.")
# Gateway timeout, wrong URL/path, empty body, etc. — surface the real
# reason instead of masking every login error as an auth failure.
return translate_connection_error(exc, context="qBittorrent")
except Exception as exc:
return translate_connection_error(exc, context="qBittorrent")
class QbittorrentConfig(ServiceConfigBase):
"""Non-secret qBittorrent connection config."""
base_url: ServiceBaseUrl
timeout_seconds: int = 60
class QbittorrentWidgetConfig(WidgetConfigBase):
"""Per-widget config for totals/active (empty — derived from the service connection)."""
pass
class QbittorrentSpeedWidgetConfig(WidgetConfigBase):
"""Speed chart config. The source returns raw bytes/sec; the frontend scales."""
unit: Literal[
"none",
"bytes",
"bytes_per_sec",
"bits_per_sec",
"bits",
"percent",
"seconds",
] = "bytes_per_sec"
scale: Literal["auto", "k", "m", "g", "t"] = "auto"
DEFINITION = ServiceDefinition(
service_type="qbittorrent",
name="qBittorrent",
description="Torrent client activity, speeds, and item counts.",
config_model=QbittorrentConfig,
secret_fields=[
SecretField(key="username", label="Username", required=True),
SecretField(key="password", label="Password", required=True, helper="Stored encrypted"),
],
widget_kinds=[
widget_kind(
kind="totals",
name="Totals",
description="Count of all listed torrents, broken down by state.",
model_cls=QbittorrentWidgetConfig,
default_config={},
refresh_interval_ms=30_000,
),
widget_kind(
kind="active",
name="Active torrents",
description="Torrents currently downloading or uploading.",
model_cls=QbittorrentWidgetConfig,
default_config={},
refresh_interval_ms=15_000,
),
widget_kind(
kind="speed",
name="Speed chart",
description="Live download/upload speed over a short window.",
model_cls=QbittorrentSpeedWidgetConfig,
default_config={"unit": "bytes_per_sec", "scale": "auto"},
refresh_interval_ms=5_000,
),
],
test_callable=test_connection,
)