MediaBot¶
A Discord bot that surfaces download status, failures, health warnings, and media requests directly from Discord. Includes interactive buttons for managing requests and stuck downloads, and an optional natural language interface powered by Claude for conversational control.
Repo: rampantlemming/mediabot
Deployment¶
| Property | Value |
|---|---|
| Host | arrstack (192.168.1.252) |
| Container | mediabot |
| Network | arr (shared Docker network with media stack) |
| Working directory | /opt/mediabot |
| Managed by | Ansible arrstack role |
The Ansible role clones the repo to /opt/mediabot and the container is built and run locally. Secrets are injected via .env from Ansible Vault.
Commands¶
| Command | Description |
|---|---|
/queue |
Active downloads across Sonarr and Radarr with progress % |
/failures |
Items with errors or import warnings — with action buttons |
/health |
Aggregated health warnings from Sonarr, Radarr, and Seerr |
/requests |
Pending Seerr media requests with approve/decline buttons |
/history |
Recent download history (grabs, imports, failures) |
/rescan |
Trigger a disk rescan in Sonarr, Radarr, or both |
/search |
Search library by title and trigger a download search |
@MediaBot <message> |
Natural language control via Claude AI (optional) |
Interactive actions¶
/failures — Queue failure management¶
Stuck or failed queue items show three action buttons per item:
| Button | Action |
|---|---|
| Retry | Removes the item from the queue (no blocklist) and triggers a fresh automatic search. Use when an import failed due to a transient issue. |
| Remove | Removes the item without blocklisting. The release can be grabbed again if a search runs later. |
| Blocklist | Removes the item and blocklists the specific release. Sonarr/Radarr will automatically search for an alternative. Use when the release itself is the problem (bad encode, wrong format, etc.). |
All action responses are ephemeral — only visible to the person who clicked. Buttons have a 5-minute timeout; run the command again if the embed is stale.
/requests — Seerr request management¶
Pending requests display with approve/decline buttons:
| Button | Action |
|---|---|
| Approve | Approves the request, triggering Sonarr/Radarr to begin searching |
| Decline | Declines the request |
Buttons disable automatically after use. Already-approved or declined requests display as read-only.
/search — Library search and trigger¶
Searches Sonarr and Radarr libraries by title. Each result shows a Search button that triggers an automatic download search for that item. Results include year, series status, and whether a file exists on disk for movies.
Use this when something is in your library but missing from disk, or to force a fresh search for a better release.
/rescan — Disk rescan¶
Triggers RescanSeries / RescanMovie commands in Sonarr and/or Radarr. Useful after manually moving or renaming files — forces the services to re-read the filesystem and update their databases.
Natural language interface¶
When ANTHROPIC_API_KEY is configured, you can @mention the bot with plain English instead of using slash commands. The bot sends your message to Claude (Sonnet), which decides which tools to call, executes them, and responds conversationally.
Claude can chain multiple actions in a single message — e.g. "check if anything is stuck, blocklist it, and search for a replacement" works as one request.
Monitoring examples¶
@MediaBot anything stuck?
@MediaBot what's downloading right now?
@MediaBot what's the overall health of the system?
@MediaBot what's been requested but isn't available yet?
@MediaBot is Severance available to watch?
Action examples¶
@MediaBot approve all pending requests
@MediaBot remove that stuck download and blocklist it
@MediaBot grab The Bear
@MediaBot can you get me Severance season 2
@MediaBot find me some sci-fi movies from 2025
Request submission goes through Seerr — Claude searches TMDB, finds the title, and submits a request. Seerr then routes it to Sonarr or Radarr automatically.
Note
This feature is entirely optional. All slash commands work without an Anthropic API key.
Cost¶
Claude Sonnet costs ~$3 per million input tokens. A typical interaction is 2,000–3,000 tokens, so each query costs well under a cent. Normal homelab usage is pennies per month.
Discord setup requirement¶
The Message Content Intent must be enabled in the Discord Developer Portal for the bot to read @mention messages:
→ Developer Portal → Your Application → Bot → Privileged Gateway Intents → Message Content Intent ✓
Proactive alerts¶
When ALERT_CHANNEL_ID is configured, a background task checks the Sonarr and Radarr queues on an interval. Any item in warning or error state for longer than STUCK_THRESHOLD_MINUTES triggers an alert embed to the configured channel.
- Each stuck item is alerted once only until it leaves the queue and reappears
- Alerts are pruned automatically when items resolve or are removed
- The alert embed references
/failuresfor interactive management - Set
ALERT_CHANNEL_ID=0to disable entirely
Configuration¶
All configuration is via environment variables, injected at runtime from the Ansible-managed .env file. Secrets are sourced from Ansible Vault.
| Variable | Required | Default | Description |
|---|---|---|---|
DISCORD_TOKEN |
✅ | — | Discord bot authentication token |
SONARR_URL |
✅ | http://sonarr:8989 |
Sonarr base URL |
SONARR_API_KEY |
✅ | — | Sonarr API key |
RADARR_URL |
✅ | http://radarr:7878 |
Radarr base URL |
RADARR_API_KEY |
✅ | — | Radarr API key |
SEERR_URL |
✅ | http://seerr:5055 |
Seerr base URL |
SEERR_API_KEY |
✅ | — | Seerr API key |
ALERT_CHANNEL_ID |
❌ | 0 (disabled) |
Discord channel ID for proactive stuck-item alerts |
STUCK_THRESHOLD_MINUTES |
❌ | 30 |
Minutes stuck before an alert fires |
CHECK_INTERVAL_MINUTES |
❌ | 5 |
Background check frequency |
ANTHROPIC_API_KEY |
❌ | — | Anthropic API key — enables @mention NLP interface |
Note
Default URLs use Docker container names (sonarr, radarr, seerr) because MediaBot runs on the same arr Docker network as the media stack. If on a different network, use IP addresses.
Deployment (manual)¶
The Ansible role handles this automatically, but for reference:
Discord bot setup (one-time)¶
- Go to discord.com/developers/applications
- New Application → name it (e.g. "Media Bot")
- Bot sidebar → Reset Token → copy token → this is
DISCORD_TOKEN - Under Privileged Gateway Intents, enable Message Content Intent (required for @mention NLP)
- OAuth2 → URL Generator → scopes:
bot,applications.commands→ permissions:Send Messages,Embed Links - Open the generated URL to invite the bot to your server
Getting API keys¶
- Sonarr / Radarr: Settings → General → API Key
- Seerr: Settings → General → API Key
- Anthropic (optional): console.anthropic.com → Settings → API Keys
Build and run¶
cd /opt/mediabot
git pull
docker build -t mediabot:latest .
docker stop mediabot && docker rm mediabot
docker run -d \
--name mediabot \
--restart unless-stopped \
--network arr \
--env-file /opt/mediabot/.env \
mediabot:latest
Slash commands sync automatically on startup (allow a few minutes; global Discord propagation can take up to an hour).
Tech stack¶
| Component | Detail |
|---|---|
| Language | Python 3.12 |
| Discord framework | discord.py 2.3+ |
| HTTP client | aiohttp 3.9+ |
| AI integration | anthropic 0.39+ |
| Base image | python:3.12-slim |
| Architecture | Async/await with asyncio.gather() for concurrent API calls |
API endpoints used¶
Sonarr / Radarr (v3)¶
| Method | Endpoint | Used by |
|---|---|---|
GET |
/api/v3/queue |
/queue, /failures, proactive alerts, Claude |
GET |
/api/v3/health |
/health, Claude |
GET |
/api/v3/history |
/history, Claude |
GET |
/api/v3/series |
/search, Claude |
GET |
/api/v3/movie |
/search, Claude |
DELETE |
/api/v3/queue/{id} |
Retry / Remove / Blocklist buttons, Claude |
POST |
/api/v3/command |
/rescan, Retry button, Claude |
Seerr (v1)¶
| Method | Endpoint | Used by |
|---|---|---|
GET |
/api/v1/request |
/requests, Claude |
GET |
/api/v1/status |
/health, Claude |
GET |
/api/v1/media |
Claude (availability checks) |
GET |
/api/v1/search |
Claude (TMDB search for request submission) |
GET |
/api/v1/movie/{tmdbId} |
/requests, Claude (title resolution) |
GET |
/api/v1/tv/{tmdbId} |
/requests, Claude (title resolution) |
POST |
/api/v1/request |
Claude (submit new media request) |
POST |
/api/v1/request/{id}/approve |
Approve button, Claude |
POST |
/api/v1/request/{id}/decline |
Decline button, Claude |
Troubleshooting¶
Slash commands don't appear
: Wait a few minutes after startup. Check container logs for Synced X slash command(s). If it shows 0 or an error, verify the bot has applications.commands OAuth scope.
"Forbidden" or empty responses : Double-check API keys. For Seerr — if CSRF protection is enabled, API access from external clients may be blocked.
Bot can't reach services
: Confirm the bot container is on the arr Docker network. If using IPs instead of container names, verify they are reachable from arrstack.
Buttons not working / "This interaction failed" : Buttons time out after 5 minutes. Run the command again to get fresh buttons. Check bot logs for API errors.
Seerr shows TMDB IDs instead of titles
: The bot resolves titles via Seerr's media endpoints. If IDs still appear, check SEERR_API_KEY is correct and Seerr is reachable.
@mention doesn't respond
: Check that ANTHROPIC_API_KEY is set in .env, Message Content Intent is enabled in the Discord Developer Portal, and bot logs show Claude natural language interface enabled. If the bot says the interface isn't configured, the key isn't being loaded — recreate the container to pick up .env changes.
Request submission fails (via Claude)
: The media may already be requested or available. Check /requests or ask the bot to check availability. Also verify Seerr has default Sonarr/Radarr servers configured under Settings → Services.
Related¶
- Arrstack — MediaBot deploys alongside the arr stack on arrstack
- Ansible arrstack role — Ansible deployment and update procedure
- Ansible variables —
vault_anthropic_api_keyvault entry