Fix sonarr formatter crash when episodeFile.quality is a string
Melora
A webhook receiver that listens for media import events from Radarr, Sonarr, and Lidarr and announces new arrivals to a Matrix room via a bot. Each media type posts into its own persistent thread, keeping the room tidy.
Architecture
Radarr ─┐
Sonarr ──┼─→ POST webhook → Melora (FastAPI) → Matrix room (threaded)
Lidarr ─┘
No polling. All three *arr instances push events to Melora via their built-in webhook/Connect system.
Requirements
- Python 3.12+
- A Matrix homeserver with an unencrypted room and a bot account
- Radarr, Sonarr, and/or Lidarr instances configured to send webhooks
Quick Start
1. Clone and configure
git clone https://github.com/prosolis/Melora.git
cd Melora
cp .env.example .env
Edit .env with your actual values (see Environment Variables below).
2. Set up a virtual environment
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
3. Verify your configuration
python -m app --check
This validates that all required environment variables are set, the Matrix homeserver is reachable, the bot token is valid, the bot has joined the announcements room, and the database path is writable. Fix any failing checks before starting the server.
4. Run locally
source .venv/bin/activate
uvicorn app.main:app --host 0.0.0.0 --port 8000
5. Run with Docker
docker build -t melora .
# Verify configuration first
docker run --rm --env-file .env melora python -m app --check
# Start the service
docker run -d \
--name melora \
--env-file .env \
-p 8000:8000 \
-v melora-data:/app \
melora
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
MATRIX_HOMESERVER_URL |
Yes | — | Base URL of your Matrix homeserver |
MATRIX_BOT_USER_ID |
Yes | — | Bot's Matrix user ID (e.g. @melora-bot:example.com) |
MATRIX_BOT_ACCESS_TOKEN |
Yes | — | Pre-authenticated access token for the bot |
MATRIX_ARRIVALS_ROOM_ID |
Yes | — | Room ID for arrival announcements (e.g. !abc123:example.com) |
WEBHOOK_SECRET |
Yes | — | Shared secret for *arr webhook authentication |
DATABASE_PATH |
No | melora.db |
Path to the SQLite database file |
*arr Configuration
Melora receives push notifications from each *arr app via their built-in Connect / Webhook system. The setup is nearly identical across Radarr, Sonarr, and Lidarr — only the webhook URL path differs.
Prerequisite: Before configuring any *arr instance, make sure Melora is running and reachable from the machine that hosts your *arr apps. You can verify by hitting the health endpoint:
curl http://melora-host:8000/health # Expected: {"status":"ok"}
Radarr (Movies)
- Open Radarr → Settings → Connect
- Click + to add a new connection and select Webhook
- Fill in the following fields:
| Field | Value |
|---|---|
| Name | Melora (or any label you like) |
| On Grab | Off |
| On Import | On |
| On Upgrade | On (if you want upgrade notifications) |
| On Rename | Off |
| On Movie Added | Off |
| On Movie Delete | Off |
| On Movie File Delete | Off |
| On Health Issue | Off |
| Tags | Leave blank (all movies) or choose specific tags |
| URL | http://melora-host:8000/webhook/radarr |
| Method | POST |
| Username | (leave blank) |
| Password | (leave blank) |
- Under Request Headers, add a header:
- Key:
X-Arr-Webhook-Secret - Value: The same value you set for
WEBHOOK_SECRETin your.env
- Key:
- Click Test — you should see a green check. Then click Save.
Sonarr (TV Shows)
- Open Sonarr → Settings → Connect
- Click + to add a new connection and select Webhook
- Fill in the following fields:
| Field | Value |
|---|---|
| Name | Melora |
| On Grab | Off |
| On Import | On |
| On Upgrade | On (if you want upgrade notifications) |
| On Rename | Off |
| On Series Add | Off |
| On Series Delete | Off |
| On Episode File Delete | Off |
| On Health Issue | Off |
| Tags | Leave blank (all series) or choose specific tags |
| URL | http://melora-host:8000/webhook/sonarr |
| Method | POST |
| Username | (leave blank) |
| Password | (leave blank) |
- Under Request Headers, add a header:
- Key:
X-Arr-Webhook-Secret - Value: The same value you set for
WEBHOOK_SECRETin your.env
- Key:
- Click Test — you should see a green check. Then click Save.
Lidarr (Music)
- Open Lidarr → Settings → Connect
- Click + to add a new connection and select Webhook
- Fill in the following fields:
| Field | Value |
|---|---|
| Name | Melora |
| On Grab | Off |
| On Import | On |
| On Upgrade | On (if you want upgrade notifications) |
| On Rename | Off |
| On Album Delete | Off |
| On Artist Delete | Off |
| On Health Issue | Off |
| Tags | Leave blank (all artists) or choose specific tags |
| URL | http://melora-host:8000/webhook/lidarr |
| Method | POST |
| Username | (leave blank) |
| Password | (leave blank) |
- Under Request Headers, add a header:
- Key:
X-Arr-Webhook-Secret - Value: The same value you set for
WEBHOOK_SECRETin your.env
- Key:
- Click Test — you should see a green check. Then click Save.
Troubleshooting *arr Webhooks
| Symptom | Likely Cause | Fix |
|---|---|---|
| Test button shows red X | Melora is unreachable from the *arr host | Verify the URL, port, and any firewall rules between the hosts |
| Test passes but no Matrix messages appear | The Test event type is Test, not Download — Melora ignores it by design |
Import a real file or trigger a manual import to generate a Download event |
| 401 Unauthorized in Melora logs | Secret mismatch | Ensure X-Arr-Webhook-Secret header value exactly matches the WEBHOOK_SECRET in Melora's .env |
| Duplicate notifications | Same media re-imported | Melora deduplicates by media ID — duplicates are silently ignored. If you see duplicates, check if the media has a different internal ID |
Note: Melora only processes events with
eventType: "Download". This is the event type that Radarr/Sonarr/Lidarr send when a file is actually imported (not when it is grabbed from an indexer). All other event types are acknowledged with a200 OKbut silently ignored.
Webhook Endpoints
POST /webhook/radarr — receives Radarr movie import events
POST /webhook/sonarr — receives Sonarr episode import events
POST /webhook/lidarr — receives Lidarr album import events
GET /health — returns {"status": "ok"} when the service is running
Each webhook endpoint validates the X-Arr-Webhook-Secret header, processes only Download events, and posts to the appropriate Matrix thread.
Matrix Room Structure
On first startup, Melora creates three thread root messages in the configured room. All subsequent announcements reply into the appropriate thread.
#new-arrivals:your.domain
├── 🎬 Movies ← Radarr imports
├── 📺 Shows ← Sonarr imports
└── 🎵 Music ← Lidarr imports
Thread root event_id values are stored in SQLite, so threads persist across restarts.
Message Format
Messages include both plain text and HTML (Matrix-flavored Markdown). New additions and quality upgrades are distinguished:
New movie:
🎬 The Substance (2024)
✅ New addition
🎞️ Quality: Bluray-1080p
Quality upgrade:
🎬 The Substance (2024)
⬆️ Quality upgrade
🎞️ Quality: Bluray-2160p
Project Structure
Melora/
├── app/
│ ├── __init__.py
│ ├── __main__.py # CLI entry point (--check flag)
│ ├── main.py # FastAPI app, lifespan, startup
│ ├── check.py # Configuration and connectivity checker
│ ├── config.py # Environment variable loading
│ ├── database.py # Async SQLite for thread roots and dedup
│ ├── matrix.py # matrix-nio posting and thread management
│ ├── formatters.py # Message formatting for each media type
│ └── webhooks.py # Webhook route handlers
├── Dockerfile
├── requirements.txt
├── .env.example
└── README.md
Error Handling
- Unknown or malformed payloads return 200 (prevents *arr retry storms)
- Parsing and Matrix posting errors are logged but don't crash the service
- Missing thread roots on startup halt with a clear error
License
See repository for license details.