maubot-media/media_bot/clients/downloads.py
Maddox 8c62c9fd31 Initial commit: media bot v0.1.0
Maubot plugin: Matrix companion for the homelab media stack.

Services wrapped:
- Seerr: search, request, requests, trending
- Emby: nowplaying, recent, watched
- Sonarr/Radarr: queue, upcoming, missing
- NZBGet/qBittorrent: activity

Each Matrix sender is mapped to per-service user IDs via plugin config;
unmapped senders are rejected. Replies are MXID-prefixed for shared rooms.
2026-04-28 08:22:38 -04:00

70 lines
2.8 KiB
Python

"""NZBGet (JSON-RPC over HTTP basic) and qBittorrent (cookie session) clients."""
from __future__ import annotations
import aiohttp
class DownloadError(RuntimeError):
pass
class NzbgetClient:
def __init__(self, session: aiohttp.ClientSession, base_url: str,
username: str, password: str) -> None:
self.session = session
self.base = base_url.rstrip("/")
self.auth = aiohttp.BasicAuth(username, password)
async def _rpc(self, method: str, params: list | None = None) -> dict | list:
body = {"method": method, "params": params or [], "id": 1}
async with self.session.post(f"{self.base}/jsonrpc", json=body, auth=self.auth) as r:
if r.status >= 400:
raise DownloadError(f"NZBGet {method}{r.status}: {(await r.text())[:200]}")
data = await r.json()
if "error" in data and data["error"]:
raise DownloadError(f"NZBGet {method}: {data['error']}")
return data.get("result", [])
async def listgroups(self) -> list[dict]:
return await self._rpc("listgroups") # type: ignore[return-value]
class QbtClient:
def __init__(self, session: aiohttp.ClientSession, base_url: str,
username: str, password: str) -> None:
self.session = session
self.base = base_url.rstrip("/")
self.username = username
self.password = password
self._logged_in = False
async def _login(self) -> None:
async with self.session.post(
f"{self.base}/api/v2/auth/login",
data={"username": self.username, "password": self.password},
headers={"Referer": self.base},
) as r:
text = (await r.text()).strip()
if r.status >= 400 or text != "Ok.":
raise DownloadError(f"qBt login → {r.status}: {text[:200]}")
self._logged_in = True
async def _get(self, path: str, params: dict | None = None) -> list | dict:
if not self._logged_in:
await self._login()
async with self.session.get(f"{self.base}{path}", params=params) as r:
if r.status == 403:
self._logged_in = False
await self._login()
async with self.session.get(f"{self.base}{path}", params=params) as r2:
if r2.status >= 400:
raise DownloadError(f"qBt GET {path}{r2.status}")
return await r2.json()
if r.status >= 400:
raise DownloadError(f"qBt GET {path}{r.status}: {(await r.text())[:200]}")
return await r.json()
async def downloading(self) -> list[dict]:
data = await self._get("/api/v2/torrents/info", params={"filter": "downloading"})
return data if isinstance(data, list) else []