Claude f8bef94e7f Fix quality extraction crashing on string-typed quality fields
Sonarr (and potentially Radarr/Lidarr) can send the quality field as
a plain string instead of the expected nested dict. Added a helper that
handles both formats gracefully.

https://claude.ai/code/session_019ANRdyL2jfi7ysWhqN4PfD
2026-02-28 09:52:26 +00:00
2026-02-27 23:04:35 -08:00

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)

  1. Open Radarr → SettingsConnect
  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)
  1. Under Request Headers, add a header:
    • Key: X-Arr-Webhook-Secret
    • Value: The same value you set for WEBHOOK_SECRET in your .env
  2. Click Test — you should see a green check. Then click Save.

Sonarr (TV Shows)

  1. Open Sonarr → SettingsConnect
  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)
  1. Under Request Headers, add a header:
    • Key: X-Arr-Webhook-Secret
    • Value: The same value you set for WEBHOOK_SECRET in your .env
  2. Click Test — you should see a green check. Then click Save.

Lidarr (Music)

  1. Open Lidarr → SettingsConnect
  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)
  1. Under Request Headers, add a header:
    • Key: X-Arr-Webhook-Secret
    • Value: The same value you set for WEBHOOK_SECRET in your .env
  2. 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      # 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.

Description
Media addition announcement Matrix bot
Readme 67 KiB
Languages
Python 99.1%
Dockerfile 0.9%