v0.6.1: Jellyfin support via emby.type config flag
Issue #3. Audit confirmed every Emby endpoint we use (/System/Info/Public, /Sessions, /Users/{id}/Items[/Latest|/Resume], /Items/{id}/Images/Primary) is API-compatible with Jellyfin, and the ?api_key= auth scheme works on both. So 'add Jellyfin support' is really 'document it and fix display labels' — no client branching needed. Added a 'type: emby|jellyfin' field inside the emby config block (defaults to emby for backward compat). New _media_label() helper sources the label from config and is threaded through: - !media health server name - !media nowplaying header + empty-state - !media help section header + health command summary - daily digest 'not yet in <server>' line README updated with a Jellyfin example block and notes that the URL should NOT include the /emby suffix when pointing at Jellyfin.
This commit is contained in:
parent
d90d58755f
commit
f8f89b794e
4 changed files with 37 additions and 18 deletions
15
README.md
15
README.md
|
|
@ -3,7 +3,7 @@
|
|||
A [maubot](https://github.com/maubot/maubot) plugin that turns a Matrix room into a control surface for the *arr / Plex-style media stack:
|
||||
|
||||
- **Search & request** via [Jellyseerr / Overseerr](https://docs.jellyseerr.dev/) **or** directly against Sonarr/Radarr.
|
||||
- **Library + playback** via Emby (`recent`, `nowplaying`, `watched`, `find`, `resume`, `random`).
|
||||
- **Library + playback** via Emby **or Jellyfin** (`recent`, `nowplaying`, `watched`, `find`, `resume`, `random`).
|
||||
- **Sonarr / Radarr / Lidarr** queue, calendar, missing, music search/add.
|
||||
- **Downloads** — NZBGet + qBittorrent activity, completed, speed, pause/resume.
|
||||
- **Subscriptions** — `!media subscribe <show>`, with a Sonarr webhook pinging the room when new episodes import.
|
||||
|
|
@ -109,9 +109,18 @@ seerr:
|
|||
api_key: <seerr-api-key>
|
||||
```
|
||||
|
||||
### Lidarr / Emby / Downloads
|
||||
### Lidarr / Emby (or Jellyfin) / Downloads
|
||||
|
||||
All optional — the corresponding commands fail gracefully when the service isn't reachable. Commands that need them: Lidarr (`!media music`), Emby (`!media nowplaying|recent|watched|find|resume|random`), NZBGet/qBt (`!media activity|completed|speed|pause|unpause` and the daily digest's "completed" section).
|
||||
All optional — the corresponding commands fail gracefully when the service isn't reachable. Commands that need them: Lidarr (`!media music`), Emby/Jellyfin (`!media nowplaying|recent|watched|find|resume|random`), NZBGet/qBt (`!media activity|completed|speed|pause|unpause` and the daily digest's "completed" section).
|
||||
|
||||
The bot uses the same config block (`emby:`) for both servers — they expose the same API surface. Set `emby.type: jellyfin` so the display labels (`!media health`, `!media nowplaying`, etc.) read correctly:
|
||||
|
||||
```yaml
|
||||
emby:
|
||||
type: jellyfin # or "emby" (default)
|
||||
url: http://<host>:8096 # no /emby suffix on Jellyfin
|
||||
api_key: <api-key>
|
||||
```
|
||||
|
||||
### Webhooks (optional)
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,10 @@ lidarr:
|
|||
search_on_add: true
|
||||
|
||||
emby:
|
||||
# Set to "jellyfin" for a Jellyfin server. Affects display labels only —
|
||||
# the API surface used (/Users/{id}/Items, /Sessions, /System/Info/Public,
|
||||
# etc.) is identical between the two.
|
||||
type: emby
|
||||
url: http://emby:8096/emby
|
||||
api_key: CHANGEME
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
maubot: 0.3.1
|
||||
id: com.3ddbrewery.media
|
||||
version: 0.6.0
|
||||
version: 0.6.1
|
||||
license: MIT
|
||||
modules:
|
||||
- media_bot
|
||||
|
|
|
|||
|
|
@ -206,6 +206,11 @@ class MediaBot(Plugin):
|
|||
def _resolve_user(self, sender: str) -> Optional[dict]:
|
||||
return (self.config["user_map"] or {}).get(sender)
|
||||
|
||||
def _media_label(self) -> str:
|
||||
"""Display label for the configured Emby/Jellyfin server."""
|
||||
kind = ((self.config["emby"] or {}).get("type") or "emby").lower()
|
||||
return "Jellyfin" if kind == "jellyfin" else "Emby"
|
||||
|
||||
async def _say(self, evt: MessageEvent, message: str) -> None:
|
||||
await evt.respond(f"{evt.sender}\n\n{message}")
|
||||
|
||||
|
|
@ -280,7 +285,7 @@ class MediaBot(Plugin):
|
|||
"_(Seerr not configured — search/request go directly to Sonarr+Radarr; "
|
||||
"no approval workflow.)_\n\n")
|
||||
+
|
||||
"*Library (Emby)*\n"
|
||||
f"*Library ({self._media_label()})*\n"
|
||||
"- `!media nowplaying` — active sessions\n"
|
||||
"- `!media recent [movies|tv]` — recently added\n"
|
||||
"- `!media watched` — what you recently finished\n"
|
||||
|
|
@ -291,7 +296,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` — Sonarr/Radarr/Lidarr/Seerr/Emby/NZBGet/qBt status\n\n"
|
||||
f"- `!media health` — Sonarr/Radarr/Lidarr/Seerr/{self._media_label()}/NZBGet/qBt status\n\n"
|
||||
"*Lidarr (music)*\n"
|
||||
"- `!media music <q>` — search MusicBrainz (numbered)\n"
|
||||
"- `!media music add <q|N>` — add the artist to Lidarr\n\n"
|
||||
|
|
@ -660,13 +665,13 @@ class MediaBot(Plugin):
|
|||
try:
|
||||
sessions = await self.emby.sessions()
|
||||
except EmbyError as ex:
|
||||
await self._say(evt, f"Emby fetch failed: {ex}")
|
||||
await self._say(evt, f"{self._media_label()} fetch failed: {ex}")
|
||||
return
|
||||
active = [s for s in sessions if s.get("NowPlayingItem")]
|
||||
if not active:
|
||||
await self._say(evt, "Nothing playing on Emby right now.")
|
||||
await self._say(evt, f"Nothing playing on {self._media_label()} right now.")
|
||||
return
|
||||
lines = [f"**Now playing on Emby ({len(active)}):**"]
|
||||
lines = [f"**Now playing on {self._media_label()} ({len(active)}):**"]
|
||||
for s in active:
|
||||
item = s.get("NowPlayingItem") or {}
|
||||
user = s.get("UserName") or "?"
|
||||
|
|
@ -705,7 +710,7 @@ class MediaBot(Plugin):
|
|||
try:
|
||||
items = await self.emby.recently_added(emby_uid, limit=10, item_types=item_types)
|
||||
except EmbyError as ex:
|
||||
await self._say(evt, f"Emby fetch failed: {ex}")
|
||||
await self._say(evt, f"{self._media_label()} fetch failed: {ex}")
|
||||
return
|
||||
if not items:
|
||||
await self._say(evt, "Nothing recently added.")
|
||||
|
|
@ -729,7 +734,7 @@ class MediaBot(Plugin):
|
|||
try:
|
||||
items = await self.emby.user_played(emby_uid, limit=10)
|
||||
except EmbyError as ex:
|
||||
await self._say(evt, f"Emby fetch failed: {ex}")
|
||||
await self._say(evt, f"{self._media_label()} fetch failed: {ex}")
|
||||
return
|
||||
if not items:
|
||||
await self._say(evt, "No recently watched items.")
|
||||
|
|
@ -752,7 +757,7 @@ class MediaBot(Plugin):
|
|||
try:
|
||||
items = await self.emby.find(emby_uid, query, limit=10)
|
||||
except EmbyError as ex:
|
||||
await self._say(evt, f"Emby fetch failed: {ex}")
|
||||
await self._say(evt, f"{self._media_label()} fetch failed: {ex}")
|
||||
return
|
||||
if not items:
|
||||
await self._say(evt, f"Nothing in the library matching **{query}**.")
|
||||
|
|
@ -775,7 +780,7 @@ class MediaBot(Plugin):
|
|||
try:
|
||||
items = await self.emby.resume(emby_uid, limit=10)
|
||||
except EmbyError as ex:
|
||||
await self._say(evt, f"Emby fetch failed: {ex}")
|
||||
await self._say(evt, f"{self._media_label()} fetch failed: {ex}")
|
||||
return
|
||||
if not items:
|
||||
await self._say(evt, "Nothing to resume — you've finished everything you started.")
|
||||
|
|
@ -809,7 +814,7 @@ class MediaBot(Plugin):
|
|||
try:
|
||||
item = await self.emby.random_unplayed(emby_uid, item_type=item_type)
|
||||
except EmbyError as ex:
|
||||
await self._say(evt, f"Emby fetch failed: {ex}")
|
||||
await self._say(evt, f"{self._media_label()} fetch failed: {ex}")
|
||||
return
|
||||
if not item:
|
||||
await self._say(evt, f"No unwatched {item_type.lower()}s found — go you.")
|
||||
|
|
@ -985,12 +990,13 @@ class MediaBot(Plugin):
|
|||
tag = f" (update available, {update} behind)" if update else ""
|
||||
blocks.append([f"**Seerr:** ✅ v{ver}{tag}"])
|
||||
|
||||
media_label = self._media_label()
|
||||
if isinstance(emby_s, Exception):
|
||||
blocks.append([f"**Emby:** ❌ unreachable ({emby_s})"])
|
||||
blocks.append([f"**{media_label}:** ❌ unreachable ({emby_s})"])
|
||||
else:
|
||||
ver = emby_s.get("Version") or "?"
|
||||
name = emby_s.get("ServerName") or "Emby"
|
||||
blocks.append([f"**Emby:** ✅ {name} v{ver}"])
|
||||
name = emby_s.get("ServerName") or media_label
|
||||
blocks.append([f"**{media_label}:** ✅ {name} v{ver}"])
|
||||
|
||||
if isinstance(nzb_s, Exception):
|
||||
blocks.append([f"**NZBGet:** ❌ unreachable ({nzb_s})"])
|
||||
|
|
@ -1708,7 +1714,7 @@ class MediaBot(Plugin):
|
|||
if nzb_unique or qbt_unique:
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f"**✅ Also completed (not yet in Emby): "
|
||||
f"**✅ Also completed (not yet in {self._media_label()}): "
|
||||
f"{len(nzb_unique) + len(qbt_unique)}** "
|
||||
f"(NZBGet: {len(nzb_unique)} · qBt: {len(qbt_unique)})"
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue