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:
|
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.
|
- **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.
|
- **Sonarr / Radarr / Lidarr** queue, calendar, missing, music search/add.
|
||||||
- **Downloads** — NZBGet + qBittorrent activity, completed, speed, pause/resume.
|
- **Downloads** — NZBGet + qBittorrent activity, completed, speed, pause/resume.
|
||||||
- **Subscriptions** — `!media subscribe <show>`, with a Sonarr webhook pinging the room when new episodes import.
|
- **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>
|
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)
|
### Webhooks (optional)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,10 @@ lidarr:
|
||||||
search_on_add: true
|
search_on_add: true
|
||||||
|
|
||||||
emby:
|
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
|
url: http://emby:8096/emby
|
||||||
api_key: CHANGEME
|
api_key: CHANGEME
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
maubot: 0.3.1
|
maubot: 0.3.1
|
||||||
id: com.3ddbrewery.media
|
id: com.3ddbrewery.media
|
||||||
version: 0.6.0
|
version: 0.6.1
|
||||||
license: MIT
|
license: MIT
|
||||||
modules:
|
modules:
|
||||||
- media_bot
|
- media_bot
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,11 @@ class MediaBot(Plugin):
|
||||||
def _resolve_user(self, sender: str) -> Optional[dict]:
|
def _resolve_user(self, sender: str) -> Optional[dict]:
|
||||||
return (self.config["user_map"] or {}).get(sender)
|
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:
|
async def _say(self, evt: MessageEvent, message: str) -> None:
|
||||||
await evt.respond(f"{evt.sender}\n\n{message}")
|
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; "
|
"_(Seerr not configured — search/request go directly to Sonarr+Radarr; "
|
||||||
"no approval workflow.)_\n\n")
|
"no approval workflow.)_\n\n")
|
||||||
+
|
+
|
||||||
"*Library (Emby)*\n"
|
f"*Library ({self._media_label()})*\n"
|
||||||
"- `!media nowplaying` — active sessions\n"
|
"- `!media nowplaying` — active sessions\n"
|
||||||
"- `!media recent [movies|tv]` — recently added\n"
|
"- `!media recent [movies|tv]` — recently added\n"
|
||||||
"- `!media watched` — what you recently finished\n"
|
"- `!media watched` — what you recently finished\n"
|
||||||
|
|
@ -291,7 +296,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` — 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"
|
"*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"
|
||||||
|
|
@ -660,13 +665,13 @@ class MediaBot(Plugin):
|
||||||
try:
|
try:
|
||||||
sessions = await self.emby.sessions()
|
sessions = await self.emby.sessions()
|
||||||
except EmbyError as ex:
|
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
|
return
|
||||||
active = [s for s in sessions if s.get("NowPlayingItem")]
|
active = [s for s in sessions if s.get("NowPlayingItem")]
|
||||||
if not active:
|
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
|
return
|
||||||
lines = [f"**Now playing on Emby ({len(active)}):**"]
|
lines = [f"**Now playing on {self._media_label()} ({len(active)}):**"]
|
||||||
for s in active:
|
for s in active:
|
||||||
item = s.get("NowPlayingItem") or {}
|
item = s.get("NowPlayingItem") or {}
|
||||||
user = s.get("UserName") or "?"
|
user = s.get("UserName") or "?"
|
||||||
|
|
@ -705,7 +710,7 @@ class MediaBot(Plugin):
|
||||||
try:
|
try:
|
||||||
items = await self.emby.recently_added(emby_uid, limit=10, item_types=item_types)
|
items = await self.emby.recently_added(emby_uid, limit=10, item_types=item_types)
|
||||||
except EmbyError as ex:
|
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
|
return
|
||||||
if not items:
|
if not items:
|
||||||
await self._say(evt, "Nothing recently added.")
|
await self._say(evt, "Nothing recently added.")
|
||||||
|
|
@ -729,7 +734,7 @@ class MediaBot(Plugin):
|
||||||
try:
|
try:
|
||||||
items = await self.emby.user_played(emby_uid, limit=10)
|
items = await self.emby.user_played(emby_uid, limit=10)
|
||||||
except EmbyError as ex:
|
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
|
return
|
||||||
if not items:
|
if not items:
|
||||||
await self._say(evt, "No recently watched items.")
|
await self._say(evt, "No recently watched items.")
|
||||||
|
|
@ -752,7 +757,7 @@ class MediaBot(Plugin):
|
||||||
try:
|
try:
|
||||||
items = await self.emby.find(emby_uid, query, limit=10)
|
items = await self.emby.find(emby_uid, query, limit=10)
|
||||||
except EmbyError as ex:
|
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
|
return
|
||||||
if not items:
|
if not items:
|
||||||
await self._say(evt, f"Nothing in the library matching **{query}**.")
|
await self._say(evt, f"Nothing in the library matching **{query}**.")
|
||||||
|
|
@ -775,7 +780,7 @@ class MediaBot(Plugin):
|
||||||
try:
|
try:
|
||||||
items = await self.emby.resume(emby_uid, limit=10)
|
items = await self.emby.resume(emby_uid, limit=10)
|
||||||
except EmbyError as ex:
|
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
|
return
|
||||||
if not items:
|
if not items:
|
||||||
await self._say(evt, "Nothing to resume — you've finished everything you started.")
|
await self._say(evt, "Nothing to resume — you've finished everything you started.")
|
||||||
|
|
@ -809,7 +814,7 @@ class MediaBot(Plugin):
|
||||||
try:
|
try:
|
||||||
item = await self.emby.random_unplayed(emby_uid, item_type=item_type)
|
item = await self.emby.random_unplayed(emby_uid, item_type=item_type)
|
||||||
except EmbyError as ex:
|
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
|
return
|
||||||
if not item:
|
if not item:
|
||||||
await self._say(evt, f"No unwatched {item_type.lower()}s found — go you.")
|
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 ""
|
tag = f" (update available, {update} behind)" if update else ""
|
||||||
blocks.append([f"**Seerr:** ✅ v{ver}{tag}"])
|
blocks.append([f"**Seerr:** ✅ v{ver}{tag}"])
|
||||||
|
|
||||||
|
media_label = self._media_label()
|
||||||
if isinstance(emby_s, Exception):
|
if isinstance(emby_s, Exception):
|
||||||
blocks.append([f"**Emby:** ❌ unreachable ({emby_s})"])
|
blocks.append([f"**{media_label}:** ❌ unreachable ({emby_s})"])
|
||||||
else:
|
else:
|
||||||
ver = emby_s.get("Version") or "?"
|
ver = emby_s.get("Version") or "?"
|
||||||
name = emby_s.get("ServerName") or "Emby"
|
name = emby_s.get("ServerName") or media_label
|
||||||
blocks.append([f"**Emby:** ✅ {name} v{ver}"])
|
blocks.append([f"**{media_label}:** ✅ {name} v{ver}"])
|
||||||
|
|
||||||
if isinstance(nzb_s, Exception):
|
if isinstance(nzb_s, Exception):
|
||||||
blocks.append([f"**NZBGet:** ❌ unreachable ({nzb_s})"])
|
blocks.append([f"**NZBGet:** ❌ unreachable ({nzb_s})"])
|
||||||
|
|
@ -1708,7 +1714,7 @@ class MediaBot(Plugin):
|
||||||
if nzb_unique or qbt_unique:
|
if nzb_unique or qbt_unique:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
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"{len(nzb_unique) + len(qbt_unique)}** "
|
||||||
f"(NZBGet: {len(nzb_unique)} · qBt: {len(qbt_unique)})"
|
f"(NZBGet: {len(nzb_unique)} · qBt: {len(qbt_unique)})"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue