docs: slim README for self-hosting, move advanced topics to docs/DEPLOYMENT.md
Restructure the docs ahead of sharing the project for others to self-host. The README is now a focused get-it-running path (overview, architecture, prereqs, quick start, operational notes, security, backup/restore). Moved the fuller feature tour, the maintainer pre-built-image release flow, and the Gluetun VPN guide into a new docs/DEPLOYMENT.md, cross-linked both ways. The pre-built-image section is now labelled a maintainer process with a LYRA_REGISTRY override so a self-hoster can point at their own registry (or just build from source). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,83 +7,54 @@ sources of differing quality: **Qobuz** (lossless/hi-res), **Soulseek** (P2P), a
|
|||||||
**ListenBrainz** and **Last.fm** (Spotify's recommendation/related-artists APIs were
|
**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).
|
retired for new apps, so discovery is MetaBrainz-native and MBID-clean — no Spotify).
|
||||||
|
|
||||||
## Status
|
It runs on your own server as a small `docker compose` stack. This README covers getting
|
||||||
|
it running; see **[docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)** for the full feature tour,
|
||||||
|
publishing pre-built images, and running behind a VPN.
|
||||||
|
|
||||||
Built in three slices:
|
## What it does
|
||||||
|
|
||||||
1. **Acquisition engine** — ✅ **done**. Resolve a request against MusicBrainz,
|
- **Acquisition** — resolve a request against MusicBrainz, match/rank across
|
||||||
match/rank across Qobuz/Soulseek/YouTube, download the best source into an
|
Qobuz/Soulseek/YouTube, download the best source, verify completeness, tag, and organize
|
||||||
isolated staging dir, verify completeness, tag (mutagen), and atomically promote
|
into `Artist/Album (Year)/## Title.ext`.
|
||||||
the clean album into `Artist/Album (Year)/## Title.ext`.
|
- **Library manager** — follow artists (live MB search), per-release monitor toggles, a
|
||||||
2. **Library manager** — ✅ **done**. Watched artists (live MB search + follow),
|
wanted list with retry + quality-upgrade, a background monitor that auto-grabs new
|
||||||
per-release monitor toggles + auto-monitor-future, a wanted list with retry +
|
releases, and a scan that ingests albums already on disk.
|
||||||
quality-upgrade window, and a worker "monitor" that discovers new releases and
|
- **Discovery** (`/discover`) — recommendation-driven suggestions from ListenBrainz +
|
||||||
auto-grabs through the acquisition pipeline. Plus a **library scan** that ingests
|
Last.fm similar-artists, with Follow / Want / Dismiss and a read-only artist preview.
|
||||||
existing on-disk albums as have + monitored + followed, and a per-artist
|
- **Browse & watch** — Last.fm top artists/albums (`/lastfm`, with inline Follow/Want), a
|
||||||
studio-only / show-all release-type filter.
|
cover-art **Library** grid (`/library`), and **The Floor** (home): a live queue of
|
||||||
3. **Discovery** — ✅ **done**. Recommendation-driven finding of new music via
|
in-flight acquisitions with three-phase progress and a Retry button.
|
||||||
**ListenBrainz** similar-artists and **Last.fm** `artist.getSimilar` (both
|
|
||||||
MBID-native), behind a pluggable `SimilaritySource` interface — Last.fm registers
|
|
||||||
as a second source automatically once its API key is set, and cross-source scores
|
|
||||||
are summed per candidate. A background sweep (and a "Discover now" button)
|
|
||||||
aggregates artists similar to the ones you follow into a manual review feed of
|
|
||||||
suggested **artists** and **albums** at `/discover`; **Follow** and **Want**
|
|
||||||
reuse the slice-2 follow / wanted flows, **Dismiss** is permanent. Plus a
|
|
||||||
web-side seed-search box. Every suggestion and seed-search result opens a
|
|
||||||
read-only **artist preview** (`/discover/artist/[mbid]`) — discography,
|
|
||||||
per-album tracklists, and "in library" / "monitored" badges — where you can
|
|
||||||
Follow or Want before committing. No Spotify (its recommendation APIs are dead
|
|
||||||
for new apps).
|
|
||||||
|
|
||||||
Alongside the three slices:
|
Full detail on each of these is in [docs/DEPLOYMENT.md → Features in depth](docs/DEPLOYMENT.md#features-in-depth).
|
||||||
|
|
||||||
- **Last.fm browse** (`/lastfm`) — your Last.fm top artists and albums (by period),
|
|
||||||
with a "hide items already in my library" toggle; each artist opens a modal of your
|
|
||||||
most-played songs and albums, and every album is one click to **Want**.
|
|
||||||
- **Library** (`/library`) — a cover-art grid of everything on disk, filterable, each
|
|
||||||
album opening a tracklist modal.
|
|
||||||
- **The Floor** (home) — a live queue of in-flight acquisitions with three-phase
|
|
||||||
progress (**Searching → Downloading → Finishing**), a live download percentage, a
|
|
||||||
per-job "pressing ticket" (source · format · tracks · elapsed), and a **Retry**
|
|
||||||
button on anything that needs attention.
|
|
||||||
|
|
||||||
The whole UI runs a hand-rolled "Pressing Plant" design system (warm-paper theme, self-hosted
|
|
||||||
Fraunces serif for prose vs. system mono for machine data, light/dark aware).
|
|
||||||
|
|
||||||
**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 (Qobuz, Soulseek, Last.fm) are entered in Settings and persist
|
|
||||||
across `docker compose up -d --build` rebuilds (don't use `down -v`; secrets are
|
|
||||||
encrypted at rest with `LYRA_SECRET_KEY`). Tests run against a separate `lyra_test`
|
|
||||||
database (a guard blocks the live DB). Deferred hardening + the full roadmap live in
|
|
||||||
the implementation-plan "Notes" sections under
|
|
||||||
[`docs/superpowers/plans/`](docs/superpowers/plans/).
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
The `docker compose` stack is three services on a home server:
|
The `docker compose` stack is a few services on one host:
|
||||||
|
|
||||||
- **db** — Postgres, the shared source of truth.
|
- **db** — Postgres, the shared source of truth (all durable state lives here).
|
||||||
- **web** — the Next.js app: UI + API (search, request, queue, progress, settings).
|
- **web** — the Next.js app: UI + API (search, request, queue, progress, settings).
|
||||||
- **worker** — the Python worker: all source integration (streamrip, yt-dlp, slskd
|
- **worker** — the Python worker: all source integration (streamrip, yt-dlp, slskd client,
|
||||||
client, mutagen; MusicBrainz + ListenBrainz + Last.fm for metadata and discovery),
|
mutagen; MusicBrainz + ListenBrainz + Last.fm for metadata and discovery), running the
|
||||||
running the six-stage acquisition pipeline plus the background monitor and discovery
|
acquisition pipeline plus the background monitor and discovery sweeps.
|
||||||
sweeps.
|
- **db-backup** — a sidecar that `pg_dump`s the database on a schedule (see [Backup &
|
||||||
|
restore](#backup--restore)).
|
||||||
|
|
||||||
Soulseek support talks to an **external slskd** daemon (an off-the-shelf headless
|
Soulseek support talks to an **external slskd** daemon (an off-the-shelf headless Soulseek
|
||||||
Soulseek client) — the worker reaches it via the `slskd.url` + `slskd.api_key`
|
client) — the worker reaches it via the `slskd.url` + `slskd.api_key` credentials set in
|
||||||
credentials set in Settings, so run slskd separately rather than as part of this stack.
|
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
|
A shared `/music` volume is the library, with a separate `/staging` volume for in-progress
|
||||||
in-progress downloads (point `STAGING_DIR` at a fast local disk when `MUSIC_DIR` is a
|
downloads (point `STAGING_DIR` at a fast local disk when `MUSIC_DIR` is a network share).
|
||||||
network share).
|
|
||||||
|
|
||||||
See [`docs/superpowers/specs/`](docs/superpowers/specs/) for the full design.
|
## Prerequisites
|
||||||
|
|
||||||
## Running it
|
- **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
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cp .env.example .env # then edit: set LYRA_SECRET_KEY (openssl rand -base64 32) and MUSIC_DIR
|
cp .env.example .env # then edit: set LYRA_SECRET_KEY (openssl rand -base64 32) and MUSIC_DIR
|
||||||
@@ -97,33 +68,26 @@ the placeholder. Once set, keep it stable: changing it makes already-stored cred
|
|||||||
undecryptable (you'd re-enter them in Settings). Optionally set `LYRA_PASSWORD` too (see
|
undecryptable (you'd re-enter them in Settings). Optionally set `LYRA_PASSWORD` too (see
|
||||||
[Security](#security)).
|
[Security](#security)).
|
||||||
|
|
||||||
The web entrypoint runs `prisma migrate deploy` on every rebuild, so schema changes
|
The web entrypoint runs `prisma migrate deploy` on every rebuild, so schema changes reach
|
||||||
reach the live DB automatically. Enter Qobuz / Soulseek / Last.fm credentials in
|
the live DB automatically. Enter Qobuz / Soulseek / Last.fm credentials in **Settings** (they
|
||||||
**Settings** (they persist across rebuilds), then follow an artist or request an album
|
persist across rebuilds), then follow an artist or request an album to feed The Floor.
|
||||||
to feed The Floor.
|
|
||||||
|
|
||||||
## Deploying to a server (pre-built images)
|
`.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.
|
||||||
|
|
||||||
For a server that should **pull** rather than build, the `web` and `worker` images are
|
## Operational notes
|
||||||
published to the Gitea container registry and consumed by a compose file in the
|
|
||||||
[`vm-download`](https://git.jger.nl/Jonathan/vm-download) ops repo (`docker/lyra/`).
|
|
||||||
|
|
||||||
Publish a new release from this repo (amd64):
|
- The **monitor** and the **discovery sweep** are both **off by default** — set
|
||||||
|
`monitor.enabled=true` (or Settings → Monitor, + optional `monitor.*` tuning) to activate
|
||||||
```sh
|
auto-grab/upgrade, and `discover.enabled=true` (or Settings → Discovery, + optional
|
||||||
docker login git.jger.nl # once — Gitea user + a token with package:write
|
`discover.*` tuning) to activate the scheduled discovery sweep. The `/discover` "Discover
|
||||||
./scripts/build-push.sh # builds + pushes lyra-web / lyra-worker (:latest and :<sha>)
|
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
|
||||||
Then on the server:
|
`LYRA_SECRET_KEY`.
|
||||||
|
- **Tests** run against a separate `lyra_test` database (a guard blocks the live DB).
|
||||||
```sh
|
|
||||||
cd /opt/git/vm-download && git pull
|
|
||||||
cd docker/lyra && docker compose pull && docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
`db` (Postgres) and `db-backup` use the stock `postgres:17` image, so only the two custom
|
|
||||||
images are built here. See `docker/lyra/README.md` in the ops repo for first-time setup.
|
|
||||||
|
|
||||||
## Managing the library
|
## Managing the library
|
||||||
|
|
||||||
@@ -133,6 +97,24 @@ it — re-Want it from the artist page if you change your mind). This is why the
|
|||||||
also bind-mounts `MUSIC_DIR` at `/music` (read-write) — deletion is guarded to only ever
|
also bind-mounts `MUSIC_DIR` at `/music` (read-write) — deletion is guarded to only ever
|
||||||
remove folders strictly inside the library root.
|
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](docs/DEPLOYMENT.md#running-behind-a-vpn-gluetun) for a
|
||||||
|
Gluetun VPN setup.
|
||||||
|
|
||||||
## Backup & restore
|
## Backup & restore
|
||||||
|
|
||||||
**All** durable state — library metadata, monitored/wanted lists, discovery data,
|
**All** durable state — library metadata, monitored/wanted lists, discovery data,
|
||||||
@@ -163,105 +145,8 @@ docker compose start web worker
|
|||||||
is safe. To restore onto a brand-new host, bring up just `db` first
|
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.
|
(`docker compose up -d db`), restore, then `up -d --build` the rest.
|
||||||
|
|
||||||
## Security
|
## More
|
||||||
|
|
||||||
Lyra has **no per-user accounts**. Two layers protect it:
|
- **[docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)** — full feature tour, publishing pre-built
|
||||||
|
images to a registry, and running the whole stack behind a Gluetun VPN.
|
||||||
- **Shared-password gate.** Set `LYRA_PASSWORD` in `.env` to require a single shared
|
- **[docs/superpowers/specs/](docs/superpowers/specs/)** — the full design specs.
|
||||||
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.
|
|
||||||
|
|
||||||
## Running behind a VPN (Gluetun)
|
|
||||||
|
|
||||||
Lyra runs fine with its traffic routed through a [Gluetun](https://github.com/qdm12/gluetun)
|
|
||||||
VPN container — useful for hiding the download traffic (Soulseek / Qobuz / YouTube). Nothing
|
|
||||||
in Lyra is VPN-incompatible; the only thing that changes is container networking, because a
|
|
||||||
service using `network_mode: "service:gluetun"` gives up its own network stack and shares
|
|
||||||
Gluetun's. Two consequences:
|
|
||||||
|
|
||||||
- Containers that share Gluetun's namespace reach each other over **`localhost`**, not by
|
|
||||||
Docker service name — so `DATABASE_URL` changes from `@db:5432` to **`@localhost:5432`**.
|
|
||||||
- **Published ports move to the `gluetun` container** (the individual services can no longer
|
|
||||||
publish their own).
|
|
||||||
|
|
||||||
The simplest, most consistent setup is to put the **whole stack** behind Gluetun. Sketch
|
|
||||||
(merge into your existing Gluetun compose; adjust the VPN provider block to yours):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
gluetun:
|
|
||||||
image: qmcgaw/gluetun
|
|
||||||
cap_add: [NET_ADMIN]
|
|
||||||
environment:
|
|
||||||
# ... your VPN provider / credentials ...
|
|
||||||
FIREWALL_INPUT_PORTS: "3000" # let your browser reach the web UI
|
|
||||||
FIREWALL_OUTBOUND_SUBNETS: "192.168.0.0/16" # your LAN — so the worker can reach slskd
|
|
||||||
ports:
|
|
||||||
- "8770:3000" # Lyra web (published on gluetun, not on `web`)
|
|
||||||
# - "5432:5432" # only if you want DB access from the LAN
|
|
||||||
|
|
||||||
web:
|
|
||||||
build: ./web
|
|
||||||
network_mode: "service:gluetun" # no `ports:` / `networks:` here
|
|
||||||
environment:
|
|
||||||
DATABASE_URL: postgresql://lyra:lyra@localhost:5432/lyra # `db` -> `localhost`
|
|
||||||
LYRA_SECRET_KEY: ${LYRA_SECRET_KEY}
|
|
||||||
LYRA_PASSWORD: ${LYRA_PASSWORD:-}
|
|
||||||
HOSTNAME: 0.0.0.0
|
|
||||||
LYRA_LIBRARY_ROOT: /music
|
|
||||||
volumes:
|
|
||||||
- ${MUSIC_DIR:-./music}:/music
|
|
||||||
depends_on:
|
|
||||||
gluetun: { condition: service_healthy }
|
|
||||||
db: { condition: service_healthy }
|
|
||||||
|
|
||||||
worker:
|
|
||||||
build: ./worker
|
|
||||||
network_mode: "service:gluetun"
|
|
||||||
environment:
|
|
||||||
DATABASE_URL: postgresql://lyra:lyra@localhost:5432/lyra
|
|
||||||
LYRA_SECRET_KEY: ${LYRA_SECRET_KEY}
|
|
||||||
STAGING_ROOT: /staging
|
|
||||||
SLSKD_DOWNLOADS_ROOT: /slskd-downloads
|
|
||||||
volumes:
|
|
||||||
- ${MUSIC_DIR:-./music}:/music
|
|
||||||
- ${STAGING_DIR:-${MUSIC_DIR:-./music}/.staging}:/staging
|
|
||||||
- ${SLSKD_DOWNLOADS_DIR:-./slskd-downloads}:/slskd-downloads
|
|
||||||
depends_on:
|
|
||||||
gluetun: { condition: service_healthy }
|
|
||||||
db: { condition: service_healthy }
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:17
|
|
||||||
network_mode: "service:gluetun" # shares the namespace so `localhost:5432` resolves
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${POSTGRES_USER}
|
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
||||||
POSTGRES_DB: ${POSTGRES_DB}
|
|
||||||
volumes:
|
|
||||||
- postgres-data:/var/lib/postgresql/data
|
|
||||||
```
|
|
||||||
|
|
||||||
(Give `db-backup` the same `network_mode` and `PGHOST: localhost`.) The `HOSTNAME=0.0.0.0`
|
|
||||||
and the built-in healthchecks keep working, since everything is in one namespace.
|
|
||||||
|
|
||||||
Caveats worth knowing:
|
|
||||||
|
|
||||||
- **Qobuz is geo-sensitive** — its catalog/availability varies by country, so pick a VPN
|
|
||||||
exit in a region where your Qobuz account works, or some downloads will fail to match.
|
|
||||||
- The worker's `net.ipv6.conf.all.disable_ipv6` sysctl **can't apply behind Gluetun** (a
|
|
||||||
shared netns can't set sysctls) — harmless, because Gluetun blocks IPv6 anyway, which is
|
|
||||||
exactly what that sysctl worked around (Qobuz's CDN resolving to an unroutable IPv6).
|
|
||||||
- MusicBrainz rate-limits per IP; a shared VPN exit is marginally more likely to hit the
|
|
||||||
1 req/s ceiling, but the built-in response cache makes that a non-issue in normal use.
|
|
||||||
- Bind mounts (`/music`, `/staging`, `/slskd-downloads`), the Postgres volume, and backups
|
|
||||||
are filesystem, not network — **completely unaffected** by the VPN.
|
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# Lyra — deployment & advanced guide
|
||||||
|
|
||||||
|
Topics beyond the [README](../README.md) quick-start: the full feature tour, publishing
|
||||||
|
pre-built images to a registry, and running the whole stack behind a VPN. For a basic
|
||||||
|
self-hosted setup you only need the README — start there.
|
||||||
|
|
||||||
|
- [Features in depth](#features-in-depth)
|
||||||
|
- [Publishing pre-built images](#publishing-pre-built-images)
|
||||||
|
- [Running behind a VPN (Gluetun)](#running-behind-a-vpn-gluetun)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features in depth
|
||||||
|
|
||||||
|
Lyra was built in three slices, all complete:
|
||||||
|
|
||||||
|
1. **Acquisition engine.** Resolve a request against MusicBrainz, match/rank across
|
||||||
|
Qobuz/Soulseek/YouTube, download the best source into an isolated staging dir, verify
|
||||||
|
completeness, tag (mutagen), and atomically promote the clean album into
|
||||||
|
`Artist/Album (Year)/## Title.ext`.
|
||||||
|
2. **Library manager.** Watched artists (live MB search + follow), per-release monitor
|
||||||
|
toggles + auto-monitor-future, a wanted list with retry + quality-upgrade window, and a
|
||||||
|
worker "monitor" that discovers new releases and auto-grabs through the acquisition
|
||||||
|
pipeline. Plus a **library scan** that ingests existing on-disk albums as
|
||||||
|
have + monitored + followed, and a per-artist studio-only / show-all release-type filter.
|
||||||
|
3. **Discovery.** Recommendation-driven finding of new music via **ListenBrainz**
|
||||||
|
similar-artists and **Last.fm** `artist.getSimilar` (both MBID-native), behind a
|
||||||
|
pluggable `SimilaritySource` interface — Last.fm registers as a second source
|
||||||
|
automatically once its API key is set, and cross-source scores are summed per candidate.
|
||||||
|
A background sweep (and a "Discover now" button) aggregates artists similar to the ones
|
||||||
|
you follow into a manual review feed of suggested **artists** and **albums** at
|
||||||
|
`/discover`; **Follow** and **Want** reuse the slice-2 follow / wanted flows, **Dismiss**
|
||||||
|
is permanent. Plus a web-side seed-search box. Every suggestion and seed-search result
|
||||||
|
opens a read-only **artist preview** (`/discover/artist/[mbid]`) — discography, per-album
|
||||||
|
tracklists, and "in library" / "monitored" badges — where you can Follow or Want before
|
||||||
|
committing. No Spotify (its recommendation APIs are dead for new apps).
|
||||||
|
|
||||||
|
Alongside the three slices:
|
||||||
|
|
||||||
|
- **Last.fm browse** (`/lastfm`) — your Last.fm top artists and albums (by period), with a
|
||||||
|
"hide items already in my library" toggle and inline **Follow** / **Want** buttons on each
|
||||||
|
row; each artist also opens a modal of your most-played songs and albums.
|
||||||
|
- **Library** (`/library`) — a cover-art grid of everything on disk, filterable, each album
|
||||||
|
opening a tracklist modal.
|
||||||
|
- **The Floor** (home) — a live queue of in-flight acquisitions with three-phase progress
|
||||||
|
(**Searching → Downloading → Finishing**), a live download percentage, a per-job "pressing
|
||||||
|
ticket" (source · format · tracks · elapsed), and a **Retry** button on anything that needs
|
||||||
|
attention.
|
||||||
|
|
||||||
|
The whole UI runs a hand-rolled "Pressing Plant" design system (warm-paper theme, self-hosted
|
||||||
|
Fraunces serif for prose vs. system mono for machine data, light/dark aware).
|
||||||
|
|
||||||
|
Deferred hardening and the full roadmap live in the implementation-plan "Notes" sections under
|
||||||
|
[`docs/superpowers/plans/`](superpowers/plans/); the full design specs are under
|
||||||
|
[`docs/superpowers/specs/`](superpowers/specs/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Publishing pre-built images
|
||||||
|
|
||||||
|
> **Maintainer release process** — this is how the project maintainer publishes images to a
|
||||||
|
> private registry. If you're self-hosting your own copy, ignore this and just build from
|
||||||
|
> source with `docker compose up -d --build` (see the README). You'd only need this if you
|
||||||
|
> run your own registry and want servers to **pull** rather than build.
|
||||||
|
|
||||||
|
The `web` and `worker` images can be published to a container registry and consumed by a
|
||||||
|
compose file that pulls instead of builds. The maintainer publishes to a Gitea container
|
||||||
|
registry consumed by a compose file in the
|
||||||
|
[`vm-download`](https://git.jger.nl/Jonathan/vm-download) ops repo (`docker/lyra/`).
|
||||||
|
|
||||||
|
Publish a new release from this repo (amd64):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker login <your-registry> # once — registry user + a token with package:write
|
||||||
|
./scripts/build-push.sh # builds + pushes lyra-web / lyra-worker (:latest and :<sha>)
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/build-push.sh` defaults the registry namespace to `git.jger.nl/jonathan`; override it
|
||||||
|
with `LYRA_REGISTRY=<your-registry>/<namespace>`.
|
||||||
|
|
||||||
|
Then on the server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /opt/git/vm-download && git pull
|
||||||
|
cd docker/lyra && docker compose pull && docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
`db` (Postgres) and `db-backup` use the stock `postgres:17` image, so only the two custom
|
||||||
|
images are built here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running behind a VPN (Gluetun)
|
||||||
|
|
||||||
|
Lyra runs fine with its traffic routed through a [Gluetun](https://github.com/qdm12/gluetun)
|
||||||
|
VPN container — useful for hiding the download traffic (Soulseek / Qobuz / YouTube). Nothing
|
||||||
|
in Lyra is VPN-incompatible; the only thing that changes is container networking, because a
|
||||||
|
service using `network_mode: "service:gluetun"` gives up its own network stack and shares
|
||||||
|
Gluetun's. Two consequences:
|
||||||
|
|
||||||
|
- Containers that share Gluetun's namespace reach each other over **`localhost`**, not by
|
||||||
|
Docker service name — so `DATABASE_URL` changes from `@db:5432` to **`@localhost:5432`**.
|
||||||
|
- **Published ports move to the `gluetun` container** (the individual services can no longer
|
||||||
|
publish their own).
|
||||||
|
|
||||||
|
The simplest, most consistent setup is to put the **whole stack** behind Gluetun. Sketch
|
||||||
|
(merge into your existing Gluetun compose; adjust the VPN provider block to yours):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
gluetun:
|
||||||
|
image: qmcgaw/gluetun
|
||||||
|
cap_add: [NET_ADMIN]
|
||||||
|
environment:
|
||||||
|
# ... your VPN provider / credentials ...
|
||||||
|
FIREWALL_INPUT_PORTS: "3000" # let your browser reach the web UI
|
||||||
|
FIREWALL_OUTBOUND_SUBNETS: "192.168.0.0/16" # your LAN — so the worker can reach slskd
|
||||||
|
ports:
|
||||||
|
- "8770:3000" # Lyra web (published on gluetun, not on `web`)
|
||||||
|
# - "5432:5432" # only if you want DB access from the LAN
|
||||||
|
|
||||||
|
web:
|
||||||
|
build: ./web
|
||||||
|
network_mode: "service:gluetun" # no `ports:` / `networks:` here
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://lyra:lyra@localhost:5432/lyra # `db` -> `localhost`
|
||||||
|
LYRA_SECRET_KEY: ${LYRA_SECRET_KEY}
|
||||||
|
LYRA_PASSWORD: ${LYRA_PASSWORD:-}
|
||||||
|
HOSTNAME: 0.0.0.0
|
||||||
|
LYRA_LIBRARY_ROOT: /music
|
||||||
|
volumes:
|
||||||
|
- ${MUSIC_DIR:-./music}:/music
|
||||||
|
depends_on:
|
||||||
|
gluetun: { condition: service_healthy }
|
||||||
|
db: { condition: service_healthy }
|
||||||
|
|
||||||
|
worker:
|
||||||
|
build: ./worker
|
||||||
|
network_mode: "service:gluetun"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://lyra:lyra@localhost:5432/lyra
|
||||||
|
LYRA_SECRET_KEY: ${LYRA_SECRET_KEY}
|
||||||
|
STAGING_ROOT: /staging
|
||||||
|
SLSKD_DOWNLOADS_ROOT: /slskd-downloads
|
||||||
|
volumes:
|
||||||
|
- ${MUSIC_DIR:-./music}:/music
|
||||||
|
- ${STAGING_DIR:-${MUSIC_DIR:-./music}/.staging}:/staging
|
||||||
|
- ${SLSKD_DOWNLOADS_DIR:-./slskd-downloads}:/slskd-downloads
|
||||||
|
depends_on:
|
||||||
|
gluetun: { condition: service_healthy }
|
||||||
|
db: { condition: service_healthy }
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:17
|
||||||
|
network_mode: "service:gluetun" # shares the namespace so `localhost:5432` resolves
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
```
|
||||||
|
|
||||||
|
(Give `db-backup` the same `network_mode` and `PGHOST: localhost`.) The `HOSTNAME=0.0.0.0`
|
||||||
|
and the built-in healthchecks keep working, since everything is in one namespace.
|
||||||
|
|
||||||
|
Caveats worth knowing:
|
||||||
|
|
||||||
|
- **Qobuz is geo-sensitive** — its catalog/availability varies by country, so pick a VPN
|
||||||
|
exit in a region where your Qobuz account works, or some downloads will fail to match.
|
||||||
|
- The worker's `net.ipv6.conf.all.disable_ipv6` sysctl **can't apply behind Gluetun** (a
|
||||||
|
shared netns can't set sysctls) — harmless, because Gluetun blocks IPv6 anyway, which is
|
||||||
|
exactly what that sysctl worked around (Qobuz's CDN resolving to an unroutable IPv6).
|
||||||
|
- MusicBrainz rate-limits per IP; a shared VPN exit is marginally more likely to hit the
|
||||||
|
1 req/s ceiling, but the built-in response cache makes that a non-issue in normal use.
|
||||||
|
- Bind mounts (`/music`, `/staging`, `/slskd-downloads`), the Postgres volume, and backups
|
||||||
|
are filesystem, not network — **completely unaffected** by the VPN.
|
||||||
Reference in New Issue
Block a user