# 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 ```bash git clone https://github.com/prosolis/Melora.git cd Melora cp .env.example .env ``` Edit `.env` with your actual values (see [Environment Variables](#environment-variables) below). ### 2. Run locally ```bash pip install -r requirements.txt uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` ### 3. Run with Docker ```bash docker build -t melora . 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: > ```bash > curl http://melora-host:8000/health > # Expected: {"status":"ok"} > ``` --- ### Radarr (Movies) 1. Open Radarr → **Settings** → **Connect** 2. Click **+** to add a new connection and select **Webhook** 3. 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)_ | 4. Under **Request Headers**, add a header: - **Key:** `X-Arr-Webhook-Secret` - **Value:** The same value you set for `WEBHOOK_SECRET` in your `.env` 5. Click **Test** — you should see a green check. Then click **Save**. --- ### Sonarr (TV Shows) 1. Open Sonarr → **Settings** → **Connect** 2. Click **+** to add a new connection and select **Webhook** 3. 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)_ | 4. Under **Request Headers**, add a header: - **Key:** `X-Arr-Webhook-Secret` - **Value:** The same value you set for `WEBHOOK_SECRET` in your `.env` 5. Click **Test** — you should see a green check. Then click **Save**. --- ### Lidarr (Music) 1. Open Lidarr → **Settings** → **Connect** 2. Click **+** to add a new connection and select **Webhook** 3. 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)_ | 4. Under **Request Headers**, add a header: - **Key:** `X-Arr-Webhook-Secret` - **Value:** The same value you set for `WEBHOOK_SECRET` in your `.env` 5. 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 a `200 OK` but 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 # FastAPI app, lifespan, startup │ ├── 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.