v0.5.3: !media health covers all 7 backing services
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).
This commit is contained in:
parent
b23eb8b403
commit
20e439b38e
4 changed files with 66 additions and 9 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
maubot: 0.3.1
|
maubot: 0.3.1
|
||||||
id: com.3ddbrewery.media
|
id: com.3ddbrewery.media
|
||||||
version: 0.5.2
|
version: 0.5.3
|
||||||
license: MIT
|
license: MIT
|
||||||
modules:
|
modules:
|
||||||
- media_bot
|
- media_bot
|
||||||
|
|
|
||||||
|
|
@ -273,7 +273,7 @@ class MediaBot(Plugin):
|
||||||
"- `!media queue` — combined queue\n"
|
"- `!media queue` — combined queue\n"
|
||||||
"- `!media upcoming` — Sonarr calendar (next 7 days)\n"
|
"- `!media upcoming` — Sonarr calendar (next 7 days)\n"
|
||||||
"- `!media missing` — Sonarr wanted/missing\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"
|
"*Lidarr (music)*\n"
|
||||||
"- `!media music <q>` — search MusicBrainz (numbered)\n"
|
"- `!media music <q>` — search MusicBrainz (numbered)\n"
|
||||||
"- `!media music add <q|N>` — add the artist to Lidarr\n\n"
|
"- `!media music add <q|N>` — add the artist to Lidarr\n\n"
|
||||||
|
|
@ -761,28 +761,75 @@ class MediaBot(Plugin):
|
||||||
lines.append(f"- *{series}* {tag} — {t} (aired {air})")
|
lines.append(f"- *{series}* {tag} — {t} (aired {air})")
|
||||||
await self._say(evt, "\n".join(lines))
|
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:
|
async def cmd_health(self, evt: MessageEvent) -> None:
|
||||||
await evt.mark_read()
|
await evt.mark_read()
|
||||||
if await self._reject_unmapped(evt):
|
if await self._reject_unmapped(evt):
|
||||||
return
|
return
|
||||||
sonarr_h, radarr_h = await asyncio.gather(
|
(sonarr_h, radarr_h, lidarr_h,
|
||||||
self.sonarr.health(), self.radarr.health(),
|
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,
|
return_exceptions=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
lines: list[str] = []
|
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):
|
if isinstance(data, Exception):
|
||||||
lines.append(f"**{label}:** unreachable ({data})")
|
lines.append(f"**{label}:** ❌ unreachable ({data})")
|
||||||
continue
|
continue
|
||||||
if not data:
|
if not data:
|
||||||
lines.append(f"**{label}:** ✅ healthy")
|
lines.append(f"**{label}:** ✅ healthy")
|
||||||
continue
|
continue
|
||||||
lines.append(f"**{label} ({len(data)} warnings):**")
|
lines.append(f"**{label}:** ⚠️ {len(data)} warning(s)")
|
||||||
for h in data:
|
for h in data:
|
||||||
src = h.get("source") or "?"
|
src = h.get("source") or "?"
|
||||||
msg = h.get("message") 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))
|
await self._say(evt, "\n".join(lines))
|
||||||
|
|
||||||
def _arr_pct(self, q: dict) -> str:
|
def _arr_pct(self, q: dict) -> str:
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@ class EmbyClient:
|
||||||
raise EmbyError(f"GET {path} → {r.status}: {(await r.text())[:200]}")
|
raise EmbyError(f"GET {path} → {r.status}: {(await r.text())[:200]}")
|
||||||
return await r.json()
|
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]:
|
async def sessions(self) -> list[dict]:
|
||||||
data = await self._get("/Sessions")
|
data = await self._get("/Sessions")
|
||||||
return data if isinstance(data, list) else (data.get("Items") or [])
|
return data if isinstance(data, list) else (data.get("Items") or [])
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,11 @@ class SeerrClient:
|
||||||
raise SeerrError(f"POST {path} → {r.status}: {(await r.text())[:200]}")
|
raise SeerrError(f"POST {path} → {r.status}: {(await r.text())[:200]}")
|
||||||
return await r.json()
|
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]:
|
async def search(self, query: str) -> list[dict]:
|
||||||
data = await self._get("/api/v1/search", params={"query": query})
|
data = await self._get("/api/v1/search", params={"query": query})
|
||||||
return (data or {}).get("results", []) if isinstance(data, dict) else (data or [])
|
return (data or {}).get("results", []) if isinstance(data, dict) else (data or [])
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue