"""Overseerr tools — cross-library media search + request tracking (read-only). Overseerr is the *request portal* sitting in front of Radarr/Sonarr, so it knows: - Whether a title is already in the library (via Plex sync) - Whether someone has requested it and what state the request is in Reuses the `arr_get` helper because Overseerr accepts the same `X-Api-Key` header. """ from ._common import arr_get # Overseerr status enums (from their API docs) _REQUEST_STATUS = {1: "pending", 2: "approved", 3: "declined"} _MEDIA_STATUS = {1: "unknown", 2: "pending", 3: "processing", 4: "partially_available", 5: "available"} def overseerr_search(query, limit=10): """Search TMDB via Overseerr for movies or TV shows. Returns whether each result is already in the library, in progress, or not yet requested.""" data = arr_get("overseerr", "/api/v1/search", {"query": query}) out = [] for r in (data.get("results") or [])[:limit]: info = r.get("mediaInfo") or {} media_status_code = info.get("status") out.append({ "tmdb_id": r.get("id"), "type": r.get("mediaType"), "title": r.get("title") or r.get("name"), "year": (r.get("releaseDate") or r.get("firstAirDate") or "")[:4], "overview": (r.get("overview") or "")[:200], "media_status": _MEDIA_STATUS.get(media_status_code, "not_in_library") if media_status_code else "not_in_library", }) return {"query": query, "total_results": data.get("totalResults", 0), "results": out} def _resolve_request_title(media): """Given a request's media object, fetch its title via /movie/{tmdbId} or /tv/{tmdbId}.""" mtype = media.get("mediaType") tid = media.get("tmdbId") if not mtype or not tid: return None try: data = arr_get("overseerr", f"/api/v1/{mtype}/{tid}") return data.get("title") or data.get("name") except Exception: return None def overseerr_requests(status="all", limit=20): """List recent Overseerr media requests, optionally filtered by status. status: all | pending | approved | processing | available | unavailable.""" valid = {"all", "pending", "approved", "processing", "available", "unavailable"} if status not in valid: status = "all" params = {"take": limit, "sort": "added"} if status != "all": params["filter"] = status data = arr_get("overseerr", "/api/v1/request", params) out = [] for r in (data.get("results") or [])[:limit]: media = r.get("media") or {} out.append({ "id": r.get("id"), "type": r.get("type"), "request_status": _REQUEST_STATUS.get(r.get("status"), "unknown"), "created_at": r.get("createdAt"), "title": _resolve_request_title(media), "media_status": _MEDIA_STATUS.get(media.get("status"), "unknown"), "requested_by": (r.get("requestedBy") or {}).get("displayName"), }) return { "status_filter": status, "count": len(out), "page_info": data.get("pageInfo"), "requests": out, } def overseerr_request_counts(): """High-level counts of Overseerr requests by status and type.""" return arr_get("overseerr", "/api/v1/request/count") TOOLS = [ { "name": "overseerr_search", "description": "Search TMDB via Overseerr for movies or TV shows. Returns whether each result is already in the library (available), in progress (processing/pending), or not yet requested. Use when the user asks 'do we have X?', 'is Y available?', or 'has anyone requested Z?' — this is the best cross-library check because Overseerr knows Radarr, Sonarr, and Plex state.", "input_schema": { "type": "object", "properties": { "query": {"type": "string"}, "limit": {"type": "integer", "description": "Max results to return (default 10)", "default": 10}, }, "required": ["query"], }, "read_only": True, "fn": overseerr_search, }, { "name": "overseerr_requests", "description": "List recent Overseerr media requests, optionally filtered by status. Use when the user asks 'what's pending', 'what did Angela request', 'what's processing'. Status values: all, pending, approved, processing, available, unavailable.", "input_schema": { "type": "object", "properties": { "status": {"type": "string", "description": "Filter: all, pending, approved, processing, available, unavailable", "default": "all"}, "limit": {"type": "integer", "default": 20}, }, }, "read_only": True, "fn": overseerr_requests, }, { "name": "overseerr_request_counts", "description": "High-level counts of Overseerr requests broken down by status (pending/approved/declined/processing/available) and type (movie/tv).", "input_schema": {"type": "object", "properties": {}}, "read_only": True, "fn": overseerr_request_counts, }, ]