Maubot plugin for the homelab media stack (Seerr/Emby/Sonarr/Radarr/NZBGet/qBt)
Find a file
Maddox ce07eab868 v0.6.5: database_type: asyncpg — the plugin was on maubot's legacy sync interface
Every DB call in this plugin is mautrix async_db style (awaited, $1 params),
but the manifest only said `database: true`. maubot defaults database_type to
`sqlalchemy`, which hands the plugin a SYNCHRONOUS SQLAlchemy Engine, so:

  TypeError: object ResultProxy can't be used in 'await' expression

…on every awaited execute, and `self.database.fetch()` doesn't exist at all.
That is the real reason !media subscribe never worked and digest_state was
missing — not the INSERT OR REPLACE spelling fixed in v0.6.4 (still correct,
just not sufficient).

Nastiest part: the sync engine EXECUTES the statement before the await raises,
so _ensure_db_schema's first CREATE TABLE landed and the rest didn't — leaving
a half-built media.db with subscriptions but no digest_state and no version
table, which reads like a working schema.

The old file is parked at media.db.legacy-sqlalchemy-2026-09-26 (zero rows);
mautrix rebuilt media.db from the UpgradeTable: subscriptions + digest_state
+ version=1, and the digest_state row that the 8 AM UPDATE needs.

Verified: no bootstrap error in the journal, and the Sonarr webhook's
SELECT now returns {"ok": true, "subscribers": 0} instead of a 500.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLykfosgWWUL8MtwmWkDD3
2026-09-26 16:27:42 -04:00
media_bot v0.6.4: portable upsert for !media subscribe (Postgres, not SQLite) 2026-09-26 16:20:24 -04:00
.gitignore Initial commit: media bot v0.1.0 2026-04-28 08:22:38 -04:00
base-config.yaml v0.6.1: Jellyfin support via emby.type config flag 2026-05-03 15:26:25 -04:00
LICENSE Initial commit: media bot v0.1.0 2026-04-28 08:22:38 -04:00
maubot.yaml v0.6.5: database_type: asyncpg — the plugin was on maubot's legacy sync interface 2026-09-26 16:27:42 -04:00
README.md v0.6.1: Jellyfin support via emby.type config flag 2026-05-03 15:26:25 -04:00

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 an access_token + device_id for 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 in maubot.yaml before 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's UpgradeTable; nothing manual needed.

License

MIT — see LICENSE.