The Soulseek server silently drops searches containing certain blocklisted terms
(major-label artists / album titles — Adele, Rihanna, Beyoncé, "Lemonade", …),
returning 0 responses ("TimedOut"). The blocked term poisons the whole query even
though the OTHER field usually isn't blocked and the content is on the network
(e.g. searching "Beyoncé …" → 0, but "I Am… Sasha Fierce" alone → 250 with 136
real Beyoncé folders).
When the primary "{artist} {album}" search returns nothing, retry with each field
alone (normalized to fold accents/punctuation) and keep only results whose file
path still matches the dropped field (_keep_matching), so a same-titled album by a
different artist is never grabbed. Recovers blocklisted albums with a distinctive
title; safely yields nothing when the title is too generic (e.g. Adele "21") rather
than mis-grabbing. Fallback only fires on a 0-response primary, so the happy path is
unchanged.
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.