The Pirate Phase 1.b: Lidarr + Whisparr + Overseerr tools
Adds 9 read-only tools across three new services: - Lidarr: queue, artist_search, library_stats (music library awareness) - Whisparr: queue, series_search, library_stats (adult content, v3 API) - Overseerr: search, requests, request_counts (cross-library availability, request tracking via X-Api-Key header reusing arr_get helper) Fix _common.py urlencode to use %20 instead of + for query values — Overseerr rejects + as reserved. Safe for all arr services. Pirate catalog: 11 → 20 tools.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
"""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,
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user