Jonathan 73f1ae6e2f feat(web): order the In-queue tab next-up-first with a "Next up" marker
The queue tab showed newest-first (API returns createdAt desc) but the worker claims
the oldest queued job next (claim_next: ORDER BY createdAt ASC) — so the item that ran
next was at the BOTTOM. Sort the queue bucket ascending so it matches processing
order, and badge the first claimable row (oldest, not individually paused) as "Next up".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 20:56:42 +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%