"""Sonarr / Radarr v3 HTTP clients (shared base).""" from __future__ import annotations import aiohttp class ArrError(RuntimeError): pass class ArrClient: def __init__(self, session: aiohttp.ClientSession, base_url: str, api_key: str) -> None: self.session = session self.base = base_url.rstrip("/") self.headers = {"X-Api-Key": api_key, "Accept": "application/json"} async def _get(self, path: str, params: dict | None = None) -> dict | list: async with self.session.get(f"{self.base}{path}", headers=self.headers, params=params) as r: if r.status >= 400: raise ArrError(f"GET {path} → {r.status}: {(await r.text())[:200]}") return await r.json() async def _post(self, path: str, body: dict) -> dict | list: async with self.session.post(f"{self.base}{path}", headers=self.headers, json=body) as r: if r.status >= 400: raise ArrError(f"POST {path} → {r.status}: {(await r.text())[:200]}") return await r.json() async def health(self) -> list[dict]: """Returns active health-check warnings (empty list = healthy).""" data = await self._get("/api/v3/health") return data if isinstance(data, list) else (data or []) async def queue(self, page_size: int = 50) -> list[dict]: params = { "pageSize": page_size, "includeUnknownSeriesItems": "false", "includeUnknownMovieItems": "false", "includeSeries": "true", "includeMovie": "true", "includeEpisode": "true", } data = await self._get("/api/v3/queue", params=params) return (data or {}).get("records", []) if isinstance(data, dict) else (data or []) class SonarrClient(ArrClient): async def list_series(self) -> list[dict]: """Full list of series Sonarr knows about (used for subscription lookup).""" data = await self._get("/api/v3/series") return data if isinstance(data, list) else (data or []) async def calendar(self, start: str, end: str) -> list[dict]: data = await self._get( "/api/v3/calendar", params={"start": start, "end": end, "includeSeries": "true"}, ) return data if isinstance(data, list) else (data or []) async def missing(self, page_size: int = 10) -> list[dict]: params = { "pageSize": page_size, "sortKey": "airDateUtc", "sortDirection": "descending", "monitored": "true", "includeSeries": "true", } data = await self._get("/api/v3/wanted/missing", params=params) return (data or {}).get("records", []) if isinstance(data, dict) else (data or []) async def lookup(self, term: str) -> list[dict]: data = await self._get("/api/v3/series/lookup", params={"term": term}) return data if isinstance(data, list) else (data or []) async def quality_profiles(self) -> list[dict]: data = await self._get("/api/v3/qualityprofile") return data if isinstance(data, list) else [] async def language_profiles(self) -> list[dict]: # Sonarr v4 dropped these; v3 still has them. Empty list = skip the field. try: data = await self._get("/api/v3/languageprofile") return data if isinstance(data, list) else [] except ArrError: return [] async def root_folders(self) -> list[dict]: data = await self._get("/api/v3/rootfolder") return data if isinstance(data, list) else [] async def add_series(self, series: dict) -> dict: return await self._post("/api/v3/series", series) # type: ignore[return-value] class RadarrClient(ArrClient): async def lookup(self, term: str) -> list[dict]: data = await self._get("/api/v3/movie/lookup", params={"term": term}) return data if isinstance(data, list) else (data or []) async def quality_profiles(self) -> list[dict]: data = await self._get("/api/v3/qualityprofile") return data if isinstance(data, list) else [] async def root_folders(self) -> list[dict]: data = await self._get("/api/v3/rootfolder") return data if isinstance(data, list) else [] async def add_movie(self, movie: dict) -> dict: return await self._post("/api/v3/movie", movie) # type: ignore[return-value] class LidarrClient(ArrClient): """Lidarr API v1 — note the version difference from Sonarr/Radarr.""" async def health(self) -> list[dict]: data = await self._get("/api/v1/health") return data if isinstance(data, list) else (data or []) async def lookup(self, term: str) -> list[dict]: data = await self._get("/api/v1/artist/lookup", params={"term": term}) return data if isinstance(data, list) else (data or []) async def quality_profiles(self) -> list[dict]: data = await self._get("/api/v1/qualityprofile") return data if isinstance(data, list) else [] async def metadata_profiles(self) -> list[dict]: data = await self._get("/api/v1/metadataprofile") return data if isinstance(data, list) else [] async def root_folders(self) -> list[dict]: data = await self._get("/api/v1/rootfolder") return data if isinstance(data, list) else [] async def add_artist(self, payload: dict) -> dict: return await self._post("/api/v1/artist", payload) # type: ignore[return-value]