"""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 JELLYFIN_USER_ID = "308e0dab19ce4a2180a2933d73694514" 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/latest") def jellyfin_latest(): """Get recently added items from Jellyfin.""" try: items = _jellyfin(f"/Users/{JELLYFIN_USER_ID}/Items/Latest?Limit=10&Fields=Overview,DateCreated") return [{"name": i.get("Name", "?"), "type": i.get("Type", "?"), "series": i.get("SeriesName"), "date": i.get("DateCreated", "?")[:10], "year": i.get("ProductionYear")} for i in (items if isinstance(items, list) else [])] except Exception as e: return {"error": str(e)} @router.get("/sonarr/history") def sonarr_history(): """Recent Sonarr grabs/imports.""" try: with httpx.Client(timeout=10) as client: r = client.get(f"{SONARR_URL}/api/v3/history", headers={"X-Api-Key": SONARR_KEY}, params={"pageSize": 10, "sortKey": "date", "sortDirection": "descending"}) r.raise_for_status() records = r.json().get("records", []) return [{"title": rec.get("sourceTitle", "?"), "event": rec.get("eventType", "?"), "date": rec.get("date", "?")[:10], "quality": rec.get("quality", {}).get("quality", {}).get("name", "?")} for rec in records] except Exception as e: return {"error": str(e)} @router.get("/radarr/history") def radarr_history(): """Recent Radarr grabs/imports.""" try: with httpx.Client(timeout=10) as client: r = client.get(f"{RADARR_URL}/api/v3/history", headers={"X-Api-Key": RADARR_KEY}, params={"pageSize": 10, "sortKey": "date", "sortDirection": "descending"}) r.raise_for_status() records = r.json().get("records", []) return [{"title": rec.get("sourceTitle", "?"), "event": rec.get("eventType", "?"), "date": rec.get("date", "?")[:10], "quality": rec.get("quality", {}).get("quality", {}).get("name", "?")} for rec in records] except Exception as e: return {"error": str(e)} @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)} # --------------------------------------------------------------------------- # Plex # --------------------------------------------------------------------------- PLEX_TOKEN = "REDACTED_TOKEN" # pragma: allowlist secret PLEX_SERVERS = { "Calypso": "http://192.168.0.250:32400", "Atlantis": "http://192.168.0.200:32400", } @router.get("/plex/status") def plex_status(): """Get Plex server status and active sessions.""" import xml.etree.ElementTree as ET results = [] for name, url in PLEX_SERVERS.items(): try: with httpx.Client(timeout=5) as client: # Get sessions r = client.get(f"{url}/status/sessions", headers={"X-Plex-Token": PLEX_TOKEN}) r.raise_for_status() root = ET.fromstring(r.text) sessions = [] for v in root.iter("Video"): # Build a rich title for TV episodes title = v.get("title", "?") if v.get("REDACTED_APP_PASSWORD"): season = v.get("parentTitle", "") title = f"{v.get('REDACTED_APP_PASSWORD')} — {season + ' · ' if season else ''}{title}" session = { "title": title, "type": v.get("type", "?"), "year": v.get("year"), } for p in v.iter("Player"): session["player"] = p.get("title") or p.get("product", "?") session["platform"] = p.get("platform", "?") session["device"] = p.get("device") or p.get("platform", "?") session["state"] = p.get("state", "?") session["local"] = p.get("local") == "1" for u in v.iter("User"): session["user"] = u.get("title") for s in v.iter("Session"): session["bandwidth"] = s.get("bandwidth") session["location"] = s.get("location") for m in v.iter("Media"): session["video_resolution"] = m.get("videoResolution") session["video_codec"] = m.get("videoCodec") session["media_bitrate"] = m.get("bitrate") for t in v.iter("REDACTED_APP_PASSWORD"): session["transcode"] = True session["video_decision"] = t.get("videoDecision") session["transcode_speed"] = t.get("speed") sessions.append(session) # Get library counts lr = client.get(f"{url}/library/sections", headers={"X-Plex-Token": PLEX_TOKEN}) libraries = [] if lr.status_code == 200: lroot = ET.fromstring(lr.text) for d in lroot.iter("Directory"): libraries.append({ "title": d.get("title", "?"), "type": d.get("type", "?"), }) results.append({ "name": name, "url": url, "online": True, "sessions": sessions, "libraries": libraries, }) except Exception as e: results.append({"name": name, "url": url, "online": False, "error": str(e), "sessions": [], "libraries": []}) return {"servers": results} # --------------------------------------------------------------------------- # Deluge (torrent client) # --------------------------------------------------------------------------- TDARR_URL = "http://192.168.0.200:8265" 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)} # --------------------------------------------------------------------------- # Tdarr (media transcoding cluster) # --------------------------------------------------------------------------- @router.get("/tdarr/cluster") def tdarr_cluster(): """Get Tdarr cluster status — nodes, workers, stats.""" try: # Get nodes with active workers with httpx.Client(timeout=10) as client: nodes_r = client.get(f"{TDARR_URL}/api/v2/get-nodes") nodes_r.raise_for_status() raw_nodes = nodes_r.json() # Get statistics stats_r = client.post( f"{TDARR_URL}/api/v2/cruddb", json={"data": {"collection": "REDACTED_APP_PASSWORD", "mode": "getAll"}}, ) stats = ( stats_r.json()[0] if stats_r.status_code == 200 and stats_r.json() else {} ) nodes = [] total_workers = 0 total_active = 0 for nid, node in raw_nodes.items(): name = node.get("nodeName", "?") paused = node.get("nodePaused", False) workers_data = node.get("workers", {}) workers = [] if isinstance(workers_data, dict): for wid, w in workers_data.items(): if isinstance(w, dict) and w.get("file"): file_path = str(w.get("file", "")) filename = ( file_path.split("/")[-1] if "/" in file_path else file_path ) workers.append( { "id": wid, "type": w.get("workerType", "?"), "file": filename[:80], "percentage": round(w.get("percentage", 0), 1), "fps": w.get("fps", 0), "eta": w.get("ETA", "?"), } ) total_workers_count = ( len(workers_data) if isinstance(workers_data, dict) else 0 ) active_count = len(workers) total_workers += total_workers_count total_active += active_count # Determine hardware type based on node name hw_map = { "Olares": "NVENC (RTX 5090)", "Guava": "VAAPI (Radeon 760M)", "NUC": "QSV (Intel)", "Atlantis": "CPU", "Calypso": "CPU", } nodes.append( { "id": nid, "name": name, "paused": paused, "hardware": hw_map.get(name, "CPU"), "workers": workers, "active": active_count, } ) # Sort: active nodes first, then by name nodes.sort(key=lambda n: (-n["active"], n["name"])) return { "server_version": "2.67.01", "nodes": nodes, "total_active": total_active, "stats": { "total_files": stats.get("totalFileCount", 0), "transcoded": stats.get("totalTranscodeCount", 0), "health_checked": stats.get("totalHealthCheckCount", 0), "size_saved_gb": round(stats.get("sizeDiff", 0), 1), "queue_transcode": stats.get("table0Count", 0), "queue_health": stats.get("table4Count", 0), "error_transcode": stats.get("table3Count", 0), "error_health": stats.get("table6Count", 0), "tdarr_score": stats.get("tdarrScore", "?"), }, } except Exception as e: return {"error": str(e), "nodes": [], "stats": {}}