from typing import Any, Dict import httpx from fastapi import APIRouter, Depends, HTTPException from ..auth import require_admin from ..runtime import get_runtime_settings from ..clients.jellyseerr import JellyseerrClient from ..clients.sonarr import SonarrClient from ..clients.radarr import RadarrClient from ..clients.bazarr import BazarrClient from ..clients.prowlarr import ProwlarrClient from ..clients.qbittorrent import QBittorrentClient from ..clients.jellyfin import JellyfinClient from ..clients.jellystat import JellystatClient router = APIRouter(prefix="/status", tags=["status"], dependencies=[Depends(require_admin)]) async def _check(name: str, configured: bool, func) -> Dict[str, Any]: if not configured: return {"name": name, "status": "not_configured"} try: result = await func() return {"name": name, "status": "up", "detail": result} except httpx.HTTPError as exc: return {"name": name, "status": "down", "message": str(exc)} except Exception as exc: return {"name": name, "status": "down", "message": str(exc)} async def _check_qbittorrent(qbittorrent: QBittorrentClient) -> Dict[str, Any]: if not qbittorrent.base_url: return {"name": "qBittorrent", "status": "not_configured"} if not qbittorrent.username or not qbittorrent.password: reachable = await qbittorrent.is_webui_reachable() return { "name": "qBittorrent", "status": "degraded" if reachable else "not_configured", "message": "qBittorrent credentials are incomplete" if reachable else "qBittorrent is not fully configured", } try: result = await qbittorrent.get_app_version() return {"name": "qBittorrent", "status": "up", "detail": result} except RuntimeError as exc: if "login failed" in str(exc).lower(): reachable = await qbittorrent.is_webui_reachable() if reachable: return { "name": "qBittorrent", "status": "degraded", "message": "qBittorrent is reachable but the saved credentials were rejected", } return {"name": "qBittorrent", "status": "down", "message": str(exc)} except httpx.HTTPError as exc: return {"name": "qBittorrent", "status": "down", "message": str(exc)} except Exception as exc: return {"name": "qBittorrent", "status": "down", "message": str(exc)} @router.get("/services") async def services_status() -> Dict[str, Any]: runtime = get_runtime_settings() jellyseerr = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) sonarr = SonarrClient(runtime.sonarr_base_url, runtime.sonarr_api_key) radarr = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key) bazarr = BazarrClient(runtime.bazarr_base_url, runtime.bazarr_api_key) prowlarr = ProwlarrClient(runtime.prowlarr_base_url, runtime.prowlarr_api_key) qbittorrent = QBittorrentClient( runtime.qbittorrent_base_url, runtime.qbittorrent_username, runtime.qbittorrent_password ) jellyfin = JellyfinClient(runtime.jellyfin_base_url, runtime.jellyfin_api_key) services = [] services.append( await _check( "Seerr", jellyseerr.configured(), lambda: jellyseerr.get_recent_requests(take=1, skip=0), ) ) services.append( await _check( "Sonarr", sonarr.configured(), sonarr.get_system_status, ) ) services.append( await _check( "Radarr", radarr.configured(), radarr.get_system_status, ) ) services.append( await _check( "Bazarr", bazarr.configured() and bool(runtime.bazarr_api_key), bazarr.get_system_status, ) ) prowlarr_status = await _check( "Prowlarr", prowlarr.configured(), prowlarr.get_health, ) if prowlarr_status.get("status") == "up": health = prowlarr_status.get("detail") if isinstance(health, list) and health: prowlarr_status["status"] = "degraded" prowlarr_status["message"] = "Health warnings" services.append(prowlarr_status) services.append(await _check_qbittorrent(qbittorrent)) services.append( await _check( "Jellyfin", jellyfin.configured(), jellyfin.get_system_info, ) ) jellystat = JellystatClient(runtime.jellystat_base_url, runtime.jellystat_api_key) # Optional analytics must not degrade the media pipeline when not configured. if jellystat.configured(): services.append(await _check("Jellystat", True, jellystat.test_connection)) overall = "up" if any(s.get("status") == "down" for s in services): overall = "down" elif any(s.get("status") in {"degraded", "not_configured"} for s in services): overall = "degraded" return {"overall": overall, "services": services} @router.post("/services/{service}/test") async def test_service(service: str) -> Dict[str, Any]: runtime = get_runtime_settings() jellyseerr = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) sonarr = SonarrClient(runtime.sonarr_base_url, runtime.sonarr_api_key) radarr = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key) bazarr = BazarrClient(runtime.bazarr_base_url, runtime.bazarr_api_key) prowlarr = ProwlarrClient(runtime.prowlarr_base_url, runtime.prowlarr_api_key) qbittorrent = QBittorrentClient( runtime.qbittorrent_base_url, runtime.qbittorrent_username, runtime.qbittorrent_password ) jellyfin = JellyfinClient(runtime.jellyfin_base_url, runtime.jellyfin_api_key) service_key = service.strip().lower() if service_key == "jellystat": jellystat = JellystatClient(runtime.jellystat_base_url, runtime.jellystat_api_key) return await _check("Jellystat", jellystat.configured(), jellystat.test_connection) checks = { "seerr": ( "Seerr", jellyseerr.configured(), lambda: jellyseerr.get_recent_requests(take=1, skip=0), ), "jellyseerr": ( "Seerr", jellyseerr.configured(), lambda: jellyseerr.get_recent_requests(take=1, skip=0), ), "sonarr": ("Sonarr", sonarr.configured(), sonarr.get_system_status), "radarr": ("Radarr", radarr.configured(), radarr.get_system_status), "bazarr": ( "Bazarr", bazarr.configured() and bool(runtime.bazarr_api_key), bazarr.get_system_status, ), "prowlarr": ("Prowlarr", prowlarr.configured(), prowlarr.get_health), "jellyfin": ("Jellyfin", jellyfin.configured(), jellyfin.get_system_info), } if service_key == "qbittorrent": return await _check_qbittorrent(qbittorrent) if service_key not in checks: raise HTTPException(status_code=404, detail="Unknown service") name, configured, func = checks[service_key] result = await _check(name, configured, func) if name == "Prowlarr" and result.get("status") == "up": health = result.get("detail") if isinstance(health, list) and health: result["status"] = "degraded" result["message"] = "Health warnings" return result