• Import a shared playlist link and build it in Navidrome
    Paste a Spotify, Apple Music or Deezer playlist URL; muse resolves it, matches
    every track through the same Deezer path a pasted line takes, and writes an
    .m3u the library scanner picks up.
    
    Written per import rather than at the end, so the playlist appears in Navidrome
    with the first track that lands and grows as the rest arrive -- twenty minutes
    of nothing, then everything, is the worse shape. A track rejected in review
    drops out on the next rewrite, and one that never resolves is reported instead
    of silently vanishing.
    
    Resolve is split from queue on purpose: a shared link is opaque, you cannot
    tell twelve tracks from four hundred by looking at it, and four hundred is a
    quota decision.
    
    Two of the three sources are scrapes and the code says so plainly. Deezer has a
    real API. Spotify and Apple Music have no free way to read a public playlist --
    Apple's MusicKit needs a paid developer account -- so those read the public page
    and parse the JSON it already embeds for its own player. No key, no OAuth, no
    account, and no guarantee: they break whenever either company reshuffles their
    frontend. So the resolvers fail loudly and name themselves, because an empty
    playlist and a changed page must not produce the same message.
    
    The .m3u uses MUSE_PLAYLIST_PREFIX (/music), not muse's own library path. Both
    containers mount the same directory at different points and a playlist written
    with muse's view resolves to nothing on Navidrome's side.
    
    Also reorders the hooks in search-bar so the URL-seeding effect is declared
    after the state it sets, rather than closing over a binding declared below it.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
+203
View File
@@ -0,0 +1,203 @@
# Build prompt — a self-hostable music request stack
Paste this into Claude Code in a **fresh empty repo**. It assumes no prior
context. Replace `<NAME>` with the project name before you start.
---
Build `<NAME>`: a one-command, self-hostable stack that turns "I want this
song" into a correctly-tagged file in a personal music library, reachable from
any Subsonic client.
You are creating a public open-source project that strangers will run on their
own hardware. Optimise for *someone else's first hour*, not for my convenience.
## What it is
Four containers, orchestrated:
| service | role | image |
|---|---|---|
| `gluetun` | VPN killswitch that slskd lives behind | `qmcgaw/gluetun` (pin the tag) |
| `slskd` | the Soulseek client — the only thing that speaks the protocol | `slskd/slskd` |
| `muse` | web front door: search, request, review, admin | `ghcr.io/geneticglitch1/muse` |
| `lidarr` | optional: album-level wishlist, retag, import | `lscr.io/linuxserver/lidarr` |
`muse` is an existing project (github.com/geneticglitch1/muse) — pull its image,
do not rebuild or vendor it. Read its README for its environment contract.
Optionally include `soularr` (`mrusse08/soularr`) to bridge Lidarr's wanted
list to slskd, and `navidrome` (`deluan/navidrome`) so the stack is complete
end to end for someone with no existing library server.
## Non-negotiable constraints
These are not preferences. Each one is a bug I have already paid for.
**1. Sharing is ON by default, and this is a compliance requirement.**
Soulseek's rules tolerate alternative clients that "implement the full range of
features … including chat, search, wishlist, download, **upload**, and respect /
recognition of privileges". A download-only deployment is the thing that rule
exists to prevent. So:
- `slskd` ships with the music library mounted read-only and shared.
- The README states plainly, near the top, that the stack shares by default and
that this is both the network's rule and the reason the network works.
- Disabling sharing must require deliberately editing a value that is commented
as antisocial and rule-breaking. Never make it a convenient flag.
- Do not add any feature that hides, fakes, or minimises upload participation.
**2. The VPN must fail closed.**
If the tunnel drops, slskd must lose network access, not fall back to the naked
connection. `network_mode: "service:gluetun"` plus a healthcheck, plus
`depends_on: {condition: service_healthy, restart: true}`. Verify this is real
by documenting a test that stops gluetun and shows slskd cannot reach the
internet.
**3. AirVPN (and similar) use STATIC reserved ports — gluetun cannot fetch them.**
gluetun's dynamic port forwarding does not work for AirVPN. Do not set
`VPN_PORT_FORWARDING` or slskd's `SLSKD_VPN_PORT_FORWARDING` for those
providers — slskd will wait forever for a port that never arrives. Instead the
user reserves a port in the provider's control panel and sets it in `.env`, and
the compose passes it to **both** `FIREWALL_VPN_INPUT_PORTS` (so inbound peers
are admitted on `tun0`) and `SLSKD_SLSK_LISTEN_PORT`.
Do **not** publish the Soulseek listen port with a docker `ports:` entry. Docker
DNAT does nothing for traffic arriving over the tunnel. Say this in a comment,
because everyone tries it.
This distinction — static-reservation providers vs. dynamic-forwarding
providers — needs its own docs section with a table, because getting it wrong
presents as "downloads are just slow and flaky", never as an error.
**4. One `/data` mount, never two.**
`link()` between two different *mounts* returns `EXDEV` even when both are on
the same filesystem. Split downloads and library into separate mounts and every
import silently degrades to a full copy, storing the library twice. Mount a
single `/data` and use subdirectories. Ship a preflight script that creates a
hardlink across the two subdirectories and fails loudly if it can't.
**5. Bind-mount targets must pre-exist, owned by the runtime UID.**
Docker creates missing bind targets as `root:root`, which breaks every
container running as a non-root user. The setup script creates them with the
right ownership before first start.
## Repository shape
```
docker-compose.yml profiles: vpn (default), no-vpn, lidarr, navidrome
.env.example every variable, commented, no real values
setup.sh interactive first-run: dirs, perms, secrets, preflight
docs/
README.md landing: what it is, 5-minute quickstart
vpn.md providers, static vs dynamic ports, verification
exposing.md reverse proxy, TLS, auth — the safe path
sharing.md why sharing is on, what the rules say, etiquette
troubleshooting.md symptom -> cause table
architecture.md what each container does and why it's separate
.github/workflows/ lint compose, shellcheck, link-check the docs
LICENSE AGPL-3.0 or MIT — ask me which before writing it
```
## The quickstart must actually be five minutes
```bash
git clone … && cd <NAME>
./setup.sh # asks for VPN creds, reserved port, Soulseek login
docker compose up -d
```
`setup.sh` is the difference between a project people use and a project people
star. It should:
- detect whether `docker compose` exists and is v2
- ask which VPN provider, and branch the port question on static vs dynamic
- generate random values for every secret that can be random (`slskd` API key,
admin password) rather than asking
- create directories with correct ownership
- run the hardlink preflight and **refuse to continue** if it fails, explaining
why in terms of the consequence ("your library will be stored twice")
- write `.env` and print exactly what to do next
- be idempotent — safe to re-run
## Exposing it safely
This is the section that decides whether the project harms its users, so it
gets real care. Cover, in this order of preference:
1. **Don't.** Tailscale / WireGuard / LAN-only. Recommended default, stated as
such. Most people asking do not actually need public access.
2. **Reverse proxy with OAuth in front.** Caddy and nginx-proxy-manager configs.
muse has GitHub/Google OAuth built in and fails closed without it — show how
to configure that, and note the callback URL rule.
3. **Never expose slskd or Lidarr directly.** Their auth is not built for the
open internet. Only muse, and only behind OAuth.
Include a short "if you expose this, understand" list: the VPN protects your
Soulseek traffic, not your web UI; your reverse proxy sees your real IP; the
admin panel can delete files.
## Compliance and legality — one honest section, no hedging
A `docs/legal.md`, linked from the README, stating plainly:
- Soulseek's rules tolerate full-featured alternative clients; slskd is one, and
this stack does not modify its protocol behaviour.
- Uploading is part of that; this stack shares by default and you should not
turn it off.
- Whether any given file is legal to download is on the operator, exactly as
with any download client. Quote Soulseek's own line: users should only share
and download files they are legally allowed to.
- This project is not affiliated with or endorsed by Soulseek, and does not
bundle or redistribute the official Soulseek client.
Say it once, clearly, and do not repeat it apologetically elsewhere.
## Engineering standards
- **Multi-arch.** amd64 and arm64. A large share of self-hosters are on a Pi or
an Apple-silicon Mac; an amd64-only compose is a wall of exec-format errors.
- **Pin every image tag.** `:latest` on a VPN container means an unattended pull
can drop the tunnel. Pin, and document how to update deliberately.
- **Healthchecks on everything** that something else depends on.
- **Resource limits** and log rotation (`max-size`, `max-file`) — an unbounded
json-file log has filled a homelab disk before.
- **No secrets in the repo, ever.** `.env` is gitignored; `.env.example` holds
placeholders only. Add a CI check that greps for anything key-shaped.
- **Comment the non-obvious, not the obvious.** Every constraint above earns a
comment explaining the *consequence* of getting it wrong, not a restatement of
the line of YAML.
## Verification — do this, don't assume it
Before telling me it works:
1. `docker compose config` renders with `.env.example` values filled in.
2. Both VPN profiles come up; `docker exec gluetun wget -qO- ifconfig.me` shows
a non-residential IP, and the same command in a no-VPN profile shows yours.
3. Kill gluetun; confirm slskd has no internet. This is the killswitch test.
4. The hardlink preflight passes, and a file imported into the library shows
link count 2.
5. slskd's UI reports connected, logged in, and a non-zero shared file count.
6. muse loads, search returns results, a request reaches slskd.
7. `shellcheck setup.sh` is clean; `setup.sh` is idempotent.
Report what you actually ran and what it printed. If something can't be tested
in your environment, say so explicitly rather than implying it passed.
## Work order
Ask me about the licence and the name first if I haven't said. Then:
1. `docker-compose.yml` + `.env.example` + `setup.sh` — the working core.
2. Verify it, per the list above.
3. Docs, written against the thing that now exists rather than the plan.
4. CI.
Show me the compose file and get agreement before writing the docs. Don't
generate all of it and ask me to review a hundred files at once.