Jonathan d2a86932e0 fix(worker): rank Soulseek peers by estimated transfer time, not raw speed
Peer selection ranked (free-slot, advertised-speed, short-queue), ignoring file
size. But Soulseek FLAC rips of one album vary ~2x in size (a 24-bit/bloated rip vs
a standard CD rip) and Lyra scores them the SAME quality class (the slskd search
exposes no bit-depth, so every FLAC is class 2). So a peer advertising high speed
but serving 40MB/track files would out-rank a smaller standard rip that actually
finishes sooner — then blow past the 60-min per-peer backstop, time out, fall
through, and restart at 0% (Linkin Park "Hybrid Theory" looping for hours).

Rank by estimated transfer time instead: album bytes ÷ advertised speed, after the
free-slot check. Prefers whichever peer FINISHES first — fast peers and/or smaller
standard-edition rips — with no quality loss (all class 2). Equal-size case still
reduces to speed order, so existing ranking behaviour is preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 22:17:44 +02:00

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, + optional monitor.* tuning) to activate auto-grab/upgrade, and discover.enabled=true (or Settings → Discovery, + optional discover.* 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 --build rebuilds — don't use down -v (it wipes the Postgres volume); secrets are encrypted at rest with LYRA_SECRET_KEY.
  • Tests run against a separate lyra_test database (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_PASSWORD in .env to require a single shared password for the entire UI and API. A request without a valid session cookie is redirected to /login (or gets 401 for /api/*). The cookie is httpOnly and carries an HMAC of the password keyed by LYRA_SECRET_KEY, so it can't be forged. If LYRA_PASSWORD is 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

S
Description
Self-hosted multi-source music acquisition tool replacing Lidarr (Qobuz/Soulseek/YouTube/Spotify)
Readme 2.5 MiB
Languages
Python 50.7%
TypeScript 45.4%
CSS 3.6%
Dockerfile 0.2%
Shell 0.1%