Skip to content

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]
Hold "Alt" / "Option" to enable pan & zoom

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_FAILED webhook (Jellyseerr could not reach Radarr/Sonarr)
  • Radarr/Sonarr DownloadFailure or ManualInteractionRequired webhook
  • 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]
Hold "Alt" / "Option" to enable pan & zoom
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:

  1. /scan → bot asks which server: Emby or Jellyfin
  2. Tap server → bot lists all configured libraries plus an All libraries option
  3. Tap a library → bot triggers the refresh and confirms
Which server do you want to scan?

[ Emby ]  [ Jellyfin ]
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:

  1. GET /Library/VirtualFolders — find the library where CollectionType is movies (for movies) or tvshows (for TV)
  2. POST /Items/{libraryId}/Refresh — scan only that library
  3. Falls back to POST /Library/Refresh (full scan) if the specific library cannot be found

A Telegram message is sent on success:

✅ 🎬 Dune: Part Two (2024) is now available — movie library refresh triggered on Jellyfin.

Actions

Re-request

Tapping 🔄 Re-request walks through a two-step picker:

  1. Choose a quality profile
  2. Choose a root folder

Then:

  1. The original request is deleted from Jellyseerr
  2. 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