seerr-approver¶
seerr-approver is a Telegram bot that receives media request notifications from two request portals and lets the admin approve, decline, or manage them directly from Telegram — no browser required. It also monitors for requests where no indexer source was found and sends actionable alerts.
| Instance | Request portal | Media server |
|---|---|---|
jellyseerr |
Jellyseerr | Jellyfin |
seerr |
Seerr | Emby |
Both instances are managed by the same bot process. Each Telegram card identifies which instance the request came from.
How It Works¶
The bot runs two concurrent loops: a FastAPI webhook server (receives events from Jellyseerr and Radarr/Sonarr) and a Telegram long-polling loop (receives button taps from the admin).
graph LR
JFS[Jellyseerr/Seerr] -->|1. webhook| Bot[seerr-approver :8080]
Radarr -->|1. webhook| Bot
Bot -->|2. Telegram card + buttons| Admin[Admin Telegram]
Admin -->|3. tap button| Bot
Bot -->|4. approve / decline / re-request| JFS
Bot -->|5. library refresh on MEDIA_AVAILABLE| MS[Jellyfin / Emby]
Telegram Cards¶
Pending Approval¶
Sent when Jellyseerr fires MEDIA_PENDING. Shows media info and inline action buttons.
🎬 Dune: Part Two (2024)
Movie · via Jellyseerr
👤 alice
🎯 Any - HD-1080p
📁 /data/media/movie
[ ✅ Approve ] [ ❌ Decline ] [ ✏️ Edit ]
The Edit button opens a submenu to change quality profile, root folder, or reassign the request to a different user before approving.
No Source Found¶
Sent when a request was approved but no indexer source was found. Triggered by:
- Jellyseerr
MEDIA_FAILEDwebhook (Jellyseerr could not reach Radarr/Sonarr) - Radarr/Sonarr
DownloadFailureorManualInteractionRequiredwebhook - Background monitor: approved request with no grab after 5 minutes
⚠️ No source found
🎬 Dune: Part Two (2024)
Movie · via Jellyseerr
👤 alice
🎯 Any - HD-1080p
📁 /data/media/movie
[ 🗑️ Delete ] [ 🔄 Re-request ] [ ⏳ Leave It ]
No-Source Monitor¶
Radarr and Sonarr have no webhook for "search returned zero results". The bot works around this by combining two signals:
graph TD
A[MEDIA_APPROVED webhook] -->|store tmdbId + approved_at| P[pending_approvals dict]
B[Radarr Grab webhook] -->|source found → remove| P
C[Background task every 2 min] --> D{age > 5 min?}
D -->|yes| E[GET /api/v3/log on Radarr/Sonarr]
E --> F{0 reports found\nafter approved_at?}
F -->|yes| G[Send nosource card]
F -->|no| H[Keep watching up to 30 min]
D -->|no| I[Too soon, skip]
| Signal | Meaning |
|---|---|
Grab webhook received |
Source found — cancel nosource watch |
No Grab + log confirms 0 reports |
No source — send alert |
No Grab + no log match after 30 min |
Timed out — drop silently |
Restart recovery
On startup the bot queries both Seerr instances for all currently approved requests and re-populates the watch list. Only requests approved within the last 30 minutes are recovered — anything older has already exceeded the monitoring window. Duplicate nosource cards are prevented by a deduplication set that covers both the webhook and monitor paths.
Media Removal¶
Requesters can ask for media they requested to be deleted. Every removal is confirmed by the admin in Telegram before any file is touched — seerr has no user-level delete permission, so the bot brokers it with the admin API key.
graph TD
A[User: seerr web UI<br/>Report an issue] -->|ISSUE_CREATED webhook| Bot
B[User: /mine in Telegram] -->|tap Remove| Bot
Bot[seerr-approver] -->|removal card| Admin[Admin Telegram]
Admin -->|🗑️ Delete| Bot
Bot -->|DELETE /api/v1/media/id/file| Seerr[Seerr / Jellyseerr]
Seerr -->|deleteFiles=true| Arr[Radarr / Sonarr]
Bot -->|library refresh| MS[Jellyfin / Emby]
Bot -->|resolve issue| Seerr
Two channels¶
Open the title in seerr → Report an issue → Other, and write a message containing one of the removal keywords (default: remove, delete, watched, done with).
Requires the Issue Reported notification type on the webhook agent. Issues whose text does not match a keyword are forwarded to the admin as an ordinary issue notification rather than being treated as removal requests.
This channel works for every user with no setup — seerr already knows who filed the issue.
/link <seerr username> → the admin approves once → /mine lists the user's available titles with a Remove button on each.
Users who already set a Telegram chat ID in their seerr notification settings (Settings → Notifications → Telegram) are linked automatically on first use, with no approval step.
Removal card¶
🗑️ Removal requested
🎬 Dune: Part Two (2024)
Movie · via Jellyseerr
👤 alice
💬 done with this, please remove
💾 7.8 GB
[ 🗑️ Delete ] [ ❌ Keep ]
The size comes from Radarr's sizeOnDisk (Sonarr: statistics.sizeOnDisk), so the admin can see what a deletion actually frees.
Ownership is advisory
A user can file an issue on any available title, not only their own requests. When the reporter is not the original requester the card shows ⚠️ originally requested by <name> rather than dropping the request — shared households have legitimate cases. The /mine channel is strictly scoped: the list is re-derived from seerr on every tap, so a stale or forged button cannot delete someone else's title.
What deletion does¶
DELETE /api/v1/media/{mediaId}/file makes seerr call Radarr DELETE /api/v3/movie/{id}?deleteFiles=true (Sonarr: removeSeries), sets the media status to DELETED, and resets its service data. The bot then triggers a library refresh so the entry disappears from Jellyfin/Emby, and resolves the originating issue.
Titles can be re-requested
seerr's duplicate-request guard only blocks requests whose existing request is not DECLINED or COMPLETED. A fulfilled request is COMPLETED, so a deleted title can be requested again normally.
Manual Library Scan¶
The /scan command lets the admin trigger a library scan on demand without opening the media server UI.
Flow:
/scan→ bot asks which server: Emby or Jellyfin- Tap server → bot lists all configured libraries plus an All libraries option
- Tap a library → bot triggers the refresh and confirms
Jellyfin libraries
• Movies
• TV Shows
• Anime
Tap one to scan:
[ 📚 All libraries ]
[ Movies ]
[ TV Shows ]
[ Anime ]
Targeting a specific library calls POST /Items/{libraryId}/Refresh directly. "All libraries" falls back to POST /Library/Refresh.
Library Refresh¶
When a MEDIA_AVAILABLE webhook is received, the bot automatically triggers a library scan on the corresponding media server so the title appears without waiting for a scheduled scan.
| Instance | Media server | Auth header |
|---|---|---|
jellyseerr |
Jellyfin | Authorization: MediaBrowser Token="<key>" |
seerr |
Emby | X-Emby-Token: <key> |
Different auth headers
Jellyfin deprecated X-Emby-Token in 10.11 and will remove it in v12. The bot uses the correct header for each server type.
The refresh targets only the relevant library rather than triggering a full scan:
GET /Library/VirtualFolders— find the library whereCollectionTypeismovies(for movies) ortvshows(for TV)POST /Items/{libraryId}/Refresh— scan only that library- Falls back to
POST /Library/Refresh(full scan) if the specific library cannot be found
A Telegram message is sent on success:
Actions¶
Re-request¶
Tapping 🔄 Re-request walks through a two-step picker:
- Choose a quality profile
- Choose a root folder
Then:
- The original request is deleted from Jellyseerr
- A new request is created with the same TMDB ID, same requester, the chosen profile, and the chosen folder
This lets the admin retry with a lower quality profile or a different library path on behalf of the original requester without manual intervention.
Edit (pending approval)¶
From the Edit submenu on a pending card:
| Option | What it does |
|---|---|
| Quality Profile | Changes the Radarr/Sonarr quality profile before approving |
| Root Folder | Changes the destination folder |
| Request As | Reassigns the request to a different Jellyseerr user |
Deployment¶
-
Namespace
bots -
Source
gitea.hdhomelab.com/cicd/seerr-approver-bot -
Config
flux/apps/noah/bots/seerr-approver/ -
Port
8080(cluster-internal only, no ingress)
Configuration¶
Environment Variables¶
| Env Var | Source | Value |
|---|---|---|
TELEGRAM_BOT_TOKEN |
Vault secret | Bot token from BotFather |
TELEGRAM_CHAT_ID |
Vault secret | Admin chat or group ID |
SEERR_URL |
ConfigMap | http://seerr.media.svc |
SEERR_API_KEY |
Vault secret | Seerr API key |
JELLYSEERR_URL |
ConfigMap | http://jellyseerr.media.svc |
JELLYSEERR_API_KEY |
Vault secret | Jellyseerr API key |
RADARR_URL |
ConfigMap | http://radarr.media.svc (override for direct Radarr access) |
SONARR_URL |
ConfigMap | http://sonarr.media.svc (override for direct Sonarr access) |
JELLYFIN_URL |
ConfigMap | http://jellyfin.media.svc:8096 |
JELLYFIN_API_KEY |
Vault secret | Jellyfin API key for library refresh |
EMBY_URL |
ConfigMap | http://emby.media.svc:8096 |
EMBY_API_KEY |
Vault secret | Emby API key for library refresh |
DB_HOST |
Static | PostgreSQL host (192.168.68.7) |
DB_PORT |
Static (default: 5432) |
PostgreSQL port |
DB_NAME |
Static | Database name (seerr-approver-bot) |
DB_USER |
Vault secret (ExternalSecret) | PostgreSQL role username |
DB_PASSWORD |
Vault secret (ExternalSecret) | PostgreSQL role password |
REMOVAL_KEYWORDS |
Static (optional) | Comma-separated phrases marking an issue as a removal request. Default remove,delete,watched,done with |
Vault Secrets¶
Create at path seerr-approver-bot in Vault:
| Key | Description |
|---|---|
telegram-token |
Telegram bot token — BotFather /newbot |
telegram-chat-id |
Chat ID of the admin/family Telegram group |
seerr-api-key |
Seerr API key — Settings → API Keys |
jellyseerr-api-key |
Jellyseerr API key — Settings → API Keys |
jellyfin-api-key |
Jellyfin API key — Dashboard → API Keys |
emby-api-key |
Emby API key — Settings → API Keys |
DB credentials live at apps/psql/seerr-approver-bot (written by OpenTofu — do not create manually).
Database Provisioning¶
The bot stores Telegram↔seerr account links in PostgreSQL. Add an entry to tofu/tf-deploy/psql/locals.tf:
Then apply:
cd tofu/tf-deploy/psql
tofu init -backend-config=backend.pg.tfbackend
tofu plan -out plan.out
tofu apply plan.out
Credentials are written to Vault at apps/psql/seerr-approver-bot. The ExternalSecret in flux/apps/noah/bots/seerr-approver/ maps them into the pod as DB_USER and DB_PASSWORD.
Tables are created by the app
The cyrilgdn/postgresql provider has no table resource, so OpenTofu owns the role, database and grants while the bot applies its own DDL (CREATE TABLE IF NOT EXISTS) at startup. Same split as woot-deal-bot.
Not SQLite on a PVC
The PVCs here are NFS-backed, and SQLite depends on POSIX advisory locks that NFS implements unreliably — the failure mode is a corrupted database rather than a clean error. See SQLite on local-path.
See PostgreSQL Provisioning for the full pattern.
Request Portal Webhook Setup¶
Configure in both portals under Settings → Notifications → Webhook:
| Setting | Value |
|---|---|
| Webhook URL | http://seerr-approver.bots.svc/webhook?instance=jellyseerr |
| Notification types | Request Pending Approval, Media Approved, Media Auto-Approved, Media Failed, Media Available, Issue Reported |
| Setting | Value |
|---|---|
| Webhook URL | http://seerr-approver.bots.svc/webhook?instance=seerr |
| Notification types | Request Pending Approval, Media Approved, Media Auto-Approved, Media Failed, Media Available, Issue Reported |
Note
Without this the seerr-issue removal channel silently does nothing — the bot never receives the event.
Radarr / Sonarr Webhook Setup¶
Settings → Connect → Webhook → add:
| Setting | Value |
|---|---|
| URL | http://seerr-approver.bots.svc/webhook/arr?instance=jellyseerr&type=radarr |
| Triggers | On Grab, On Download Failure, On Manual Interaction Required |
| Setting | Value |
|---|---|
| URL | http://seerr-approver.bots.svc/webhook/arr?instance=jellyseerr&type=sonarr |
| Triggers | On Grab, On Download Failure, On Manual Interaction Required |
On Grab is required
The On Grab event is used to cancel the no-source watch when a release is successfully grabbed. Without it, the bot will send a false-positive nosource alert even when a download is in progress.
Commands¶
| Command | Description |
|---|---|
/pending |
List all pending requests (awaiting approval) from both Seerr and Jellyseerr |
/approved |
List approved-but-not-yet-available requests by title; tap one to load its action card (delete, re-request, or leave) |
/scan |
Manually trigger a library scan on Emby or Jellyfin — pick the server, then pick a library |
/link <seerr username> |
Ask the admin to link your Telegram account to a seerr user. Not needed if your seerr notification settings already carry your Telegram chat ID |
/mine |
List your available titles, each with a button to request its removal |
Command scoping
/pending, /approved and /scan are restricted to the admin chat (TELEGRAM_CHAT_ID) and are silently ignored elsewhere. /link and /mine are open to any chat — they authorise per action, against the requester's own seerr account. Admin-only callback buttons re-check the chat ID when tapped, so forwarding a card to another chat does not transfer its powers.