"""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, )