Avoid re-triggering a USER_BLOCKED account by capping Qobuz to a human-looking volume and cadence. A per-album gate (worker/lyra_worker/qobuz_gate.py) allows Qobuz only within active hours, under today's cap (a warm-up ramp of a steady cap), and past a randomized spacing since the last Qobuz download; the gap spreads the day's budget across active hours with jitter. When the gate is closed the pipeline drops Qobuz candidates so the job falls through to another source (and monitored albums upgrade to Qobuz later); a Qobuz-only setup is never gated. Config keys read live each loop; counters reset at the day boundary via the DB clock. Tunable in Settings -> Qobuz -> Pacing (new /api/qobuz/pacing route). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lyra
Self-hosted, multi-source music acquisition and library tool — a replacement for Lidarr + NZBget. Lyra discovers, ranks, downloads, tags, and organizes music across sources of differing quality: Qobuz (lossless/hi-res), Soulseek (P2P), and YouTube (universal lossy fallback), with recommendation-driven discovery via ListenBrainz and Last.fm (Spotify's recommendation/related-artists APIs were retired for new apps, so discovery is MetaBrainz-native and MBID-clean — no Spotify).
It runs on your own server as a small docker compose stack. This README covers getting
it running; see docs/DEPLOYMENT.md for the full feature tour,
publishing pre-built images, and running behind a VPN.
What it does
- Acquisition — resolve a request against MusicBrainz, match/rank across
Qobuz/Soulseek/YouTube, download the best source, verify completeness, tag, and organize
into
Artist/Album (Year)/## Title.ext. - Library manager — follow artists (live MB search), per-release monitor toggles, a wanted list with retry + quality-upgrade, a background monitor that auto-grabs new releases, and a scan that ingests albums already on disk.
- Discovery (
/discover) — recommendation-driven suggestions from ListenBrainz + Last.fm similar-artists, with Follow / Want / Dismiss and a read-only artist preview. - Browse & watch — Last.fm top artists/albums (
/lastfm, with inline Follow/Want), a cover-art Library grid (/library), and The Floor (home): a live queue of in-flight acquisitions with three-phase progress and a Retry button.
Full detail on each of these is in docs/DEPLOYMENT.md → Features in depth.
Architecture
The docker compose stack is a few services on one host:
- db — Postgres, the shared source of truth (all durable state lives here).
- web — the Next.js app: UI + API (search, request, queue, progress, settings).
- worker — the Python worker: all source integration (streamrip, yt-dlp, slskd client, mutagen; MusicBrainz + ListenBrainz + Last.fm for metadata and discovery), running the acquisition pipeline plus the background monitor and discovery sweeps.
- db-backup — a sidecar that
pg_dumps the database on a schedule (see Backup & restore).
Soulseek support talks to an external slskd daemon (an off-the-shelf headless Soulseek
client) — the worker reaches it via the slskd.url + slskd.api_key credentials set in
Settings, so run slskd separately rather than as part of this stack.
A shared /music volume is the library, with a separate /staging volume for in-progress
downloads (point STAGING_DIR at a fast local disk when MUSIC_DIR is a network share).
Prerequisites
- Docker + Docker Compose.
- A directory for the music library (set
MUSIC_DIR). - Optional, entered in Settings after first run: a Qobuz account (lossless/hi-res), a running slskd daemon (Soulseek), and a Last.fm API key (browse + discovery). None are required to start — Lyra runs with whatever you configure.
Quick start
cp .env.example .env # then edit: set LYRA_SECRET_KEY (openssl rand -base64 32) and MUSIC_DIR
docker compose up -d --build # web on http://localhost:8770 ; NEVER `down -v` (wipes the Postgres volume)
Generate your own LYRA_SECRET_KEY (openssl rand -base64 32) before first run — it
encrypts your stored Qobuz/Soulseek/Last.fm credentials. .env.example ships an obvious
placeholder, and both web and worker refuse to start while the key is missing or still
the placeholder. Once set, keep it stable: changing it makes already-stored credentials
undecryptable (you'd re-enter them in Settings). Optionally set LYRA_PASSWORD too (see
Security).
The web entrypoint runs prisma migrate deploy on every rebuild, so schema changes reach
the live DB automatically. Enter Qobuz / Soulseek / Last.fm credentials in Settings (they
persist across rebuilds), then follow an artist or request an album to feed The Floor.
.env.example documents every setting (MUSIC_DIR, STAGING_DIR, SLSKD_DOWNLOADS_DIR,
backup knobs). Note in particular: point SLSKD_DOWNLOADS_DIR at slskd's own downloads
directory (Lyra must run on the same host as slskd for Soulseek delivery to work), and point
STAGING_DIR at a fast local disk if MUSIC_DIR is a network share.
Operational notes
- The monitor and the discovery sweep are both off by default — set
monitor.enabled=true(or Settings → Monitor, + optionalmonitor.*tuning) to activate auto-grab/upgrade, anddiscover.enabled=true(or Settings → Discovery, + optionaldiscover.*tuning) to activate the scheduled discovery sweep. The/discover"Discover now" button and seed-search work with the sweep off. - Credentials persist across
docker compose up -d --buildrebuilds — don't usedown -v(it wipes the Postgres volume); secrets are encrypted at rest withLYRA_SECRET_KEY. - Tests run against a separate
lyra_testdatabase (a guard blocks the live DB).
Managing the library
Open an album on the Library page and use Delete album to remove it: the folder is
deleted from disk and the album is dropped and un-monitored (so auto-monitor won't re-grab
it — re-Want it from the artist page if you change your mind). This is why the web service
also bind-mounts MUSIC_DIR at /music (read-write) — deletion is guarded to only ever
remove folders strictly inside the library root.
Security
Lyra has no per-user accounts. Two layers protect it:
-
Shared-password gate. Set
LYRA_PASSWORDin.envto require a single shared password for the entire UI and API. A request without a valid session cookie is redirected to/login(or gets401for/api/*). The cookie is httpOnly and carries an HMAC of the password keyed byLYRA_SECRET_KEY, so it can't be forged. IfLYRA_PASSWORDis unset, the app is completely open — anyone who can reach the host can browse the library, trigger downloads, and read/overwrite the stored credentials in Settings. Set it for any deployment that isn't on a fully trusted, isolated network. -
Network. The shared password is deliberately minimal (single password, bearer cookie, plain HTTP on the LAN). For anything internet-facing, also put Lyra behind a reverse proxy with TLS and/or a VPN or auth gateway — don't rely on the shared password alone. See docs/DEPLOYMENT.md for a Gluetun VPN setup.
Backup & restore
All durable state — library metadata, monitored/wanted lists, discovery data,
config, and the encrypted Qobuz/Soulseek/Last.fm credentials — lives in the single
postgres-data volume. Losing that volume (disk failure, or a stray down -v) means
losing everything, recoverable only by a full re-scan and re-entering every credential.
The db-backup sidecar guards against that. It runs pg_dump immediately on start and
then every BACKUP_INTERVAL_SECONDS (default daily), keeping the last BACKUP_KEEP
dumps (default 7) as compressed custom-format .dump files in BACKUP_DIR
(default ./backups). It starts automatically with docker compose up -d.
For off-box safety (a backup on the same disk doesn't survive a disk failure), point
BACKUP_DIR at a NAS/synced path, or rsync ./backups to another machine on a cron.
Restore into a fresh/empty database (stop web/worker first so nothing writes
mid-restore):
# with the stack up and db healthy:
docker compose stop web worker
docker compose cp ./backups/lyra-YYYYMMDD-HHMMSS.dump db:/tmp/restore.dump
docker compose exec db pg_restore --clean --if-exists -U lyra -d lyra /tmp/restore.dump
docker compose start web worker
--clean --if-exists drops and recreates each object, so restoring over an existing DB
is safe. To restore onto a brand-new host, bring up just db first
(docker compose up -d db), restore, then up -d --build the rest.
More
- docs/DEPLOYMENT.md — full feature tour, publishing pre-built images to a registry, and running the whole stack behind a Gluetun VPN.
- docs/superpowers/specs/ — the full design specs.