From 20e439b38e52e9df4044f9a2d1f03d10be2b9ae6 Mon Sep 17 00:00:00 2001 From: Maddox Date: Sun, 3 May 2026 14:40:11 -0400 Subject: [PATCH] v0.5.3: !media health covers all 7 backing services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Was only Sonarr+Radarr. Now also probes Lidarr (existing /health), Seerr (/api/v1/status), Emby (/System/Info/Public), NZBGet (status RPC) and qBittorrent (transfer/info). Each line shows ✅ healthy / ⚠️ warning (with detail) / ⏸️ paused / ❌ unreachable. NZBGet surfaces DownloadPaused; qBt surfaces connection_status (firewalled vs disconnected helps spot VPN issues quickly). --- maubot.yaml | 2 +- media_bot/bot.py | 63 +++++++++++++++++++++++++++++++++----- media_bot/clients/emby.py | 5 +++ media_bot/clients/seerr.py | 5 +++ 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/maubot.yaml b/maubot.yaml index 8423e20..426aa60 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -1,6 +1,6 @@ maubot: 0.3.1 id: com.3ddbrewery.media -version: 0.5.2 +version: 0.5.3 license: MIT modules: - media_bot diff --git a/media_bot/bot.py b/media_bot/bot.py index fce9c45..0ffbdc5 100644 --- a/media_bot/bot.py +++ b/media_bot/bot.py @@ -273,7 +273,7 @@ class MediaBot(Plugin): "- `!media queue` — combined queue\n" "- `!media upcoming` — Sonarr calendar (next 7 days)\n" "- `!media missing` — Sonarr wanted/missing\n" - "- `!media health` — active warnings on either arr\n\n" + "- `!media health` — Sonarr/Radarr/Lidarr/Seerr/Emby/NZBGet/qBt status\n\n" "*Lidarr (music)*\n" "- `!media music ` — search MusicBrainz (numbered)\n" "- `!media music add ` — add the artist to Lidarr\n\n" @@ -761,28 +761,75 @@ class MediaBot(Plugin): lines.append(f"- *{series}* {tag} — {t} (aired {air})") await self._say(evt, "\n".join(lines)) - @media.subcommand("health", help="Sonarr + Radarr active health warnings") + @media.subcommand("health", help="Status of all backing services") async def cmd_health(self, evt: MessageEvent) -> None: await evt.mark_read() if await self._reject_unmapped(evt): return - sonarr_h, radarr_h = await asyncio.gather( - self.sonarr.health(), self.radarr.health(), + (sonarr_h, radarr_h, lidarr_h, + seerr_s, emby_s, nzb_s, qbt_s) = await asyncio.gather( + self.sonarr.health(), + self.radarr.health(), + self.lidarr.health(), + self.seerr.status(), + self.emby.system_info(), + self.nzbget.status(), + self.qbt.transfer_info(), return_exceptions=True, ) + lines: list[str] = [] - for label, data in (("Sonarr", sonarr_h), ("Radarr", radarr_h)): + + # /health-style: lists of {source, message} warnings. + for label, data in (("Sonarr", sonarr_h), ("Radarr", radarr_h), ("Lidarr", lidarr_h)): if isinstance(data, Exception): - lines.append(f"**{label}:** unreachable ({data})") + lines.append(f"**{label}:** ❌ unreachable ({data})") continue if not data: lines.append(f"**{label}:** ✅ healthy") continue - lines.append(f"**{label} ({len(data)} warnings):**") + lines.append(f"**{label}:** ⚠️ {len(data)} warning(s)") for h in data: src = h.get("source") or "?" msg = h.get("message") or "?" - lines.append(f"- [{src}] {msg}") + lines.append(f" - [{src}] {msg}") + + # Reachability + version probes. + if isinstance(seerr_s, Exception): + lines.append(f"**Seerr:** ❌ unreachable ({seerr_s})") + else: + ver = seerr_s.get("version") or "?" + update = seerr_s.get("commitsBehind") + tag = f" (update available, {update} behind)" if update else "" + lines.append(f"**Seerr:** ✅ v{ver}{tag}") + + if isinstance(emby_s, Exception): + lines.append(f"**Emby:** ❌ unreachable ({emby_s})") + else: + ver = emby_s.get("Version") or "?" + name = emby_s.get("ServerName") or "Emby" + lines.append(f"**Emby:** ✅ {name} v{ver}") + + # Download clients — also surface paused state as a warning. + if isinstance(nzb_s, Exception): + lines.append(f"**NZBGet:** ❌ unreachable ({nzb_s})") + else: + paused = nzb_s.get("DownloadPaused") or nzb_s.get("ServerStandBy") + ver = nzb_s.get("Version") or "?" + if paused: + lines.append(f"**NZBGet:** ⏸️ paused (v{ver})") + else: + lines.append(f"**NZBGet:** ✅ v{ver}") + + if isinstance(qbt_s, Exception): + lines.append(f"**qBittorrent:** ❌ unreachable ({qbt_s})") + else: + conn = qbt_s.get("connection_status") or "?" + # 'connected' = healthy; 'firewalled' = reachable but inbound blocked; + # 'disconnected' = no peers reachable (often VPN issue). + icon = {"connected": "✅", "firewalled": "⚠️"}.get(conn, "❌") + lines.append(f"**qBittorrent:** {icon} {conn}") + await self._say(evt, "\n".join(lines)) def _arr_pct(self, q: dict) -> str: diff --git a/media_bot/clients/emby.py b/media_bot/clients/emby.py index 2666bc8..0e0ff47 100644 --- a/media_bot/clients/emby.py +++ b/media_bot/clients/emby.py @@ -26,6 +26,11 @@ class EmbyClient: raise EmbyError(f"GET {path} → {r.status}: {(await r.text())[:200]}") return await r.json() + async def system_info(self) -> dict: + """Reachability + version probe via /System/Info/Public (no auth needed).""" + data = await self._get("/System/Info/Public") + return data if isinstance(data, dict) else {} + async def sessions(self) -> list[dict]: data = await self._get("/Sessions") return data if isinstance(data, list) else (data.get("Items") or []) diff --git a/media_bot/clients/seerr.py b/media_bot/clients/seerr.py index ce4848a..aab75de 100644 --- a/media_bot/clients/seerr.py +++ b/media_bot/clients/seerr.py @@ -45,6 +45,11 @@ class SeerrClient: raise SeerrError(f"POST {path} → {r.status}: {(await r.text())[:200]}") return await r.json() + async def status(self) -> dict: + """Return /api/v1/status — used as a reachability + version probe.""" + data = await self._get("/api/v1/status") + return data if isinstance(data, dict) else {} + async def search(self, query: str) -> list[dict]: data = await self._get("/api/v1/search", params={"query": query}) return (data or {}).get("results", []) if isinstance(data, dict) else (data or [])