"""Jellyfin + Arr suite media endpoints.""" import json import subprocess from fastapi import APIRouter import httpx router = APIRouter(tags=["media"]) JELLYFIN_API_KEY = "REDACTED_API_KEY" # pragma: allowlist secret SONARR_URL = "http://192.168.0.200:8989" SONARR_KEY = "REDACTED_SONARR_API_KEY" # pragma: allowlist secret RADARR_URL = "http://192.168.0.200:7878" RADARR_KEY = "REDACTED_RADARR_API_KEY" # pragma: allowlist secret SABNZBD_URL = "http://192.168.0.200:8080" SABNZBD_KEY = "6ae289de5a4f45f7a0124b43ba9c3dea" # pragma: allowlist secret def _jellyfin(path: str) -> dict: """Call Jellyfin API via SSH+kubectl to bypass Olares auth sidecar.""" sep = "&" if "?" in path else "?" url = f"http://localhost:8096{path}{sep}api_key={JELLYFIN_API_KEY}" try: result = subprocess.run( ["ssh", "-o", "ConnectTimeout=3", "olares", f"kubectl exec -n jellyfin-vishinator deploy/jellyfin -c jellyfin -- curl -s '{url}'"], capture_output=True, text=True, timeout=15, ) return json.loads(result.stdout) if result.returncode == 0 else {} except Exception: return {} @router.get("/jellyfin/status") def jellyfin_status(): """Jellyfin server status: version, libraries, sessions.""" info = _jellyfin("/System/Info") libraries = _jellyfin("/Library/VirtualFolders") sessions = _jellyfin("/Sessions") active = [] idle_count = 0 if isinstance(sessions, list): for s in sessions: if s.get("NowPlayingItem"): active.append({ "user": s.get("UserName", ""), "client": s.get("Client", ""), "device": s.get("DeviceName", ""), "now_playing": s["NowPlayingItem"].get("Name", ""), "type": s["NowPlayingItem"].get("Type", ""), }) else: idle_count += 1 return { "version": info.get("Version", "unknown"), "server_name": info.get("ServerName", "unknown"), "libraries": [{"name": lib.get("Name"), "type": lib.get("CollectionType", "")} for lib in libraries] if isinstance(libraries, list) else [], "active_sessions": active, "idle_sessions": idle_count, } @router.get("/sonarr/queue") async def sonarr_queue(): """Sonarr download queue.""" try: async with httpx.AsyncClient(timeout=10) as client: resp = await client.get( f"{SONARR_URL}/api/v3/queue", headers={"X-Api-Key": SONARR_KEY}, ) return resp.json() except Exception as e: return {"error": str(e)} @router.get("/radarr/queue") async def radarr_queue(): """Radarr download queue.""" try: async with httpx.AsyncClient(timeout=10) as client: resp = await client.get( f"{RADARR_URL}/api/v3/queue", headers={"X-Api-Key": RADARR_KEY}, ) return resp.json() except Exception as e: return {"error": str(e)} @router.get("/sabnzbd/queue") async def sabnzbd_queue(): """SABnzbd download queue.""" try: async with httpx.AsyncClient(timeout=10) as client: resp = await client.get( f"{SABNZBD_URL}/api", params={"apikey": SABNZBD_KEY, "output": "json", "mode": "queue"}, ) return resp.json() except Exception as e: return {"error": str(e)} # --------------------------------------------------------------------------- # Prowlarr (indexer manager) # --------------------------------------------------------------------------- PROWLARR_URL = "http://192.168.0.200:9696" PROWLARR_KEY = "58b5963e008243cf8cc4fae5276e68af" # pragma: allowlist secret @router.get("/prowlarr/stats") async def prowlarr_stats(): """Prowlarr indexer status.""" try: async with httpx.AsyncClient(timeout=10) as client: r = await client.get( f"{PROWLARR_URL}/api/v1/indexer", headers={"X-Api-Key": PROWLARR_KEY}, ) r.raise_for_status() indexers = r.json() enabled = [i for i in indexers if i.get("enable")] return { "total": len(indexers), "enabled": len(enabled), "indexers": [ {"name": i["name"], "protocol": i.get("protocol", "?")} for i in enabled[:10] ], } except Exception as e: return {"total": 0, "enabled": 0, "error": str(e)} # --------------------------------------------------------------------------- # Bazarr (subtitles) # --------------------------------------------------------------------------- BAZARR_URL = "http://192.168.0.200:6767" BAZARR_KEY = "REDACTED_BAZARR_API_KEY" # pragma: allowlist secret @router.get("/bazarr/status") async def bazarr_status(): """Bazarr subtitle status.""" try: async with httpx.AsyncClient(timeout=10) as client: r = await client.get( f"{BAZARR_URL}/api/system/status", headers={"X-Api-Key": BAZARR_KEY}, ) r.raise_for_status() status = r.json().get("data", r.json()) w = await client.get( f"{BAZARR_URL}/api/badges", headers={"X-Api-Key": BAZARR_KEY}, ) badges = w.json() if w.status_code == 200 else {} return { "version": status.get("bazarr_version", "?"), "sonarr_signalr": badges.get("sonarr_signalr", "?"), "radarr_signalr": badges.get("radarr_signalr", "?"), "wanted_episodes": badges.get("episodes", 0), "wanted_movies": badges.get("movies", 0), } except Exception as e: return {"error": str(e)} # --------------------------------------------------------------------------- # Audiobookshelf # --------------------------------------------------------------------------- ABS_URL = "http://192.168.0.200:13378" ABS_TOKEN = "REDACTED_TOKEN" # pragma: allowlist secret @router.get("/audiobookshelf/stats") async def audiobookshelf_stats(): """Audiobookshelf library stats.""" try: async with httpx.AsyncClient(timeout=10) as client: libs = await client.get( f"{ABS_URL}/api/libraries", headers={"Authorization": f"Bearer {ABS_TOKEN}"}, ) libs.raise_for_status() libraries = libs.json().get("libraries", []) result = [] for lib in libraries: result.append({ "name": lib.get("name", "?"), "type": lib.get("mediaType", "?"), "items": lib.get("stats", {}).get("totalItems", 0), }) return {"libraries": result, "total": sum(l["items"] for l in result)} except Exception as e: return {"error": str(e)} # --------------------------------------------------------------------------- # Deluge (torrent client) # --------------------------------------------------------------------------- DELUGE_URL = "http://192.168.0.200:8112" @router.get("/deluge/status") async def deluge_status(): """Deluge torrent client status.""" try: async with httpx.AsyncClient(timeout=10) as client: login = await client.post( f"{DELUGE_URL}/json", json={"method": "auth.login", "params": ["deluge"], "id": 1}, ) if login.status_code != 200: return {"available": False} stats = await client.post( f"{DELUGE_URL}/json", json={ "method": "web.update_ui", "params": [ ["name", "state", "progress", "download_payload_rate", "upload_payload_rate"], {}, ], "id": 2, }, ) data = stats.json().get("result", {}) torrents = data.get("torrents", {}) active = [ t for t in torrents.values() if t.get("state") in ("Downloading", "Seeding") ] return { "available": True, "total": len(torrents), "active": len(active), "downloading": len( [t for t in torrents.values() if t.get("state") == "Downloading"] ), "seeding": len( [t for t in torrents.values() if t.get("state") == "Seeding"] ), } except Exception as e: return {"available": False, "error": str(e)}