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:
Eric Jungbauer
2026-04-20 22:23:26 +00:00
parent eac7b64a90
commit 20a8987dd9
5 changed files with 318 additions and 4 deletions
+95
View File
@@ -0,0 +1,95 @@
"""Whisparr tools — adult library queries (read-only).
Whisparr V2 uses a Sonarr-style series/episode model with `/api/v3/` paths.
"""
from ._common import arr_get
def _compact_series(s):
st = s.get("statistics", {}) or {}
return {
"id": s.get("id"),
"title": s.get("title"),
"year": s.get("year"),
"status": s.get("status"),
"monitored": s.get("monitored"),
"episode_count": st.get("episodeCount"),
"episode_file_count": st.get("episodeFileCount"),
"size_on_disk_gb": round((st.get("sizeOnDisk", 0) or 0) / 1e9, 1),
}
def whisparr_queue(limit=20):
"""Adult content currently downloading or pending import."""
data = arr_get("whisparr", "/api/v3/queue", {"pageSize": limit, "includeSeries": "true"})
items = []
for r in data.get("records", []):
items.append({
"title": r.get("title"),
"series": r.get("series", {}).get("title") if r.get("series") else None,
"status": r.get("status"),
"timeleft": r.get("timeleft"),
"size_gb": round((r.get("size", 0) or 0) / 1e9, 2),
"size_left_gb": round((r.get("sizeleft", 0) or 0) / 1e9, 2),
"download_client": r.get("downloadClient"),
})
return {"total_records": data.get("totalRecords", 0), "items": items}
def whisparr_series_search(query):
"""Look up a title in the user's Whisparr library by partial match.
Does NOT search indexers — only what's already tracked."""
all_series = arr_get("whisparr", "/api/v3/series")
q = query.lower().strip()
hits = [s for s in all_series if q in (s.get("title") or "").lower()]
return {"query": query, "count": len(hits), "series": [_compact_series(s) for s in hits[:20]]}
def whisparr_library_stats():
"""Summary stats of the Whisparr adult library."""
all_series = arr_get("whisparr", "/api/v3/series")
total_eps = total_on_disk = total_size = 0
for s in all_series:
st = s.get("statistics", {}) or {}
total_eps += st.get("episodeCount", 0) or 0
total_on_disk += st.get("episodeFileCount", 0) or 0
total_size += st.get("sizeOnDisk", 0) or 0
return {
"series_count": len(all_series),
"episode_total": total_eps,
"episodes_on_disk": total_on_disk,
"total_size_gb": round(total_size / 1e9, 1),
}
TOOLS = [
{
"name": "whisparr_queue",
"description": "List adult content Whisparr is currently downloading or importing.",
"input_schema": {
"type": "object",
"properties": {"limit": {"type": "integer", "default": 20}},
},
"read_only": True,
"fn": whisparr_queue,
},
{
"name": "whisparr_series_search",
"description": "Search the user's Whisparr library by partial title match. Does NOT search indexers.",
"input_schema": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
"read_only": True,
"fn": whisparr_series_search,
},
{
"name": "whisparr_library_stats",
"description": "High-level stats about the Whisparr adult library: title count, episodes on disk, total size.",
"input_schema": {"type": "object", "properties": {}},
"read_only": True,
"fn": whisparr_library_stats,
},
]