05a9faca3e
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.
119 lines
4.0 KiB
Python
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,
|
|
)
|