Queries containing apostrophes/parens/etc were 400ing on Seerr's strict ajv validator. The SeerrClient _qs() helper percent-encodes the value correctly, but aiohttp parses the URL string through yarl.URL(), which normalizes %27 (and other sub-delims) back to the literal character in the query component. The literal ' then trips Seerr's "must be url encoded" check. Fix: pass yarl.URL(url, encoded=True) to bypass yarl's re-normalization so the RFC 3986 form built by _qs reaches Seerr unchanged. Reproduced + verified against deployed Seerr v3.2.0 with queries including "rocky's revenge", "what's up, doc?", "8 1/2". Closes #4 |
||
|---|---|---|
| media_bot | ||
| .gitignore | ||
| base-config.yaml | ||
| LICENSE | ||
| maubot.yaml | ||
| README.md | ||
maubot-media
A maubot plugin that turns a Matrix room into a control surface for the *arr / Plex-style media stack:
- Search & request via Jellyseerr / Overseerr or directly against Sonarr/Radarr.
- 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. - Daily digest posted to a notifications room (recently added, completed, queued, plus an optional Sunday recap pulled from an ntfy topic).
- Health check across all seven backing services in one command.
Each Matrix user is mapped to per-service IDs in plugin config; unmapped senders get a refusal.
Commands
!media help # full command list
# Search & request
!media search <query> # numbered results
!media request <query> [--tv|--movie]
!media request <N> # pick N from the last search/trending
!media requests # (Seerr only) your pending/processing
!media trending # (Seerr only) what's trending
# Library & playback (Emby)
!media nowplaying
!media recent [movies|tv]
!media watched
!media find <query>
!media resume
!media random [movie|tv]
# Sonarr / Radarr / Lidarr
!media queue
!media upcoming
!media missing
!media music <q>
!media music add <q|N>
!media health
# Downloads (NZBGet + qBittorrent)
!media activity
!media completed
!media speed
!media pause / unpause
# Subscriptions / digest
!media subscribe <show>
!media unsubscribe <show>
!media subscriptions
!media digest # fire today's digest now
Requirements
- maubot 0.3.1+ running somewhere reachable from your Matrix server.
- A Matrix user for the bot. Provision via
/_synapse/admin/v1/register(Synapse) or your homeserver's equivalent, then log in once to capture anaccess_token+device_idfor the maubot client config. - At minimum: Sonarr and Radarr (or just one, with the other left as the placeholder URL — the missing one will fail searches for that media type).
- Optional: Seerr/Jellyseerr/Overseerr, Emby, NZBGet, qBittorrent, Lidarr.
Build & install
git clone https://git.3ddbrewery.com/maddox/maubot-media.git
cd maubot-media
# Bump the plugin id in maubot.yaml if you fork this — it must be unique per
# maubot instance. The current id (com.3ddbrewery.media) is reserved for the
# upstream deployment.
PLUGIN_VERSION=$(grep '^version:' maubot.yaml | awk '{print $2}')
zip -rq "com.3ddbrewery.media-v${PLUGIN_VERSION}.mbp" \
maubot.yaml base-config.yaml media_bot/ README.md LICENSE \
-x '*/__pycache__/*'
Upload *.mbp via the maubot UI's Plugins → upload, then create an instance pointing at your bot user.
Configuration
All config lives in the maubot UI's instance config tab; the defaults shipped in base-config.yaml use placeholder hostnames (http://sonarr:8989, etc.) and CHANGEME API keys.
Required
sonarr:
url: http://<sonarr-host>:8989
api_key: <sonarr-api-key>
radarr:
url: http://<radarr-host>:7878
api_key: <radarr-api-key>
user_map:
"@alice:example.com":
seerr_user_id: 1 # only required if Seerr is configured
emby_user_id: "abc123def" # only required for Emby commands
Seerr (optional)
If seerr.url and seerr.api_key are set, search/request route through Seerr (with its approval workflow + 👍/👎 admin reactions). If left empty, the bot falls back to direct Sonarr/Radarr lookup + add — no approval, no per-user request quotas, but no Seerr install needed either.
seerr:
url: http://<seerr-host>:5055
api_key: <seerr-api-key>
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/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:
emby:
type: jellyfin # or "emby" (default)
url: http://<host>:8096 # no /emby suffix on Jellyfin
api_key: <api-key>
Webhooks (optional)
Two endpoints are exposed when the plugin's webapp is enabled (default):
| URL | Purpose |
|---|---|
/_matrix/maubot/plugin/<instance>/seerr-webhook |
Seerr → Matrix notifications (request created/approved/available/etc) |
/_matrix/maubot/plugin/<instance>/sonarr-webhook |
Sonarr Download events → ping subscribers |
Configure them in Seerr/Sonarr's Settings → Notifications/Connect → Webhook and set Authorization: Bearer <secret> headers. The shared secret is seerr_webhook_secret / sonarr_webhook_secret in the instance config; leave empty to disable auth (not recommended).
Notifications room + digest
notifications_room: "!roomid:example.com" # bot must be joined
digest_enabled: true
digest_hour: 8 # local hour, 24h
The Sunday digest can also pull a recap from an ntfy topic (e.g. an external library-cleaner job that posts its summary). Set ntfy_url to enable.
Notes for forkers
- The plugin id (
com.3ddbrewery.media) is unique per maubot deployment — change it inmaubot.yamlbefore building if you're running this alongside the upstream instance. - No personal data is stored in the plugin: every URL, key, and Matrix-ID lives in the instance config you set in the maubot UI.
- The plugin's SQLite tables (
subscriptions,digest_state) are managed via maubot'sUpgradeTable; nothing manual needed.
License
MIT — see LICENSE.