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.
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 |
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 |
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 |
| 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 |
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 |