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>
This commit is contained in:
Aryan SinghandClaude Opus 5 committed 2026-09-02 15:40:29 -05:00
1 parent e9fba3bb1d
commit 4aaf3e0998
7 files changed
+815 -19

No files matched your search

+33
View File
@@ -152,6 +152,37 @@ Failed jobs get a **Retry** button, but far fewer jobs reach that state now:
a request that loses its peer falls through to the next candidate on its own.
See "When a peer says no" below.
## Shared playlist links
Paste a Spotify, Apple Music or Deezer playlist link and muse resolves it,
matches every track the same way a pasted line is matched, and writes an `.m3u`
into the library that Navidrome picks up on its next scan — within seconds, in
practice. The playlist is written **per import**, not at the end, so it appears
with the first track that lands and grows as the rest arrive. Tracks that never
resolve are reported rather than silently dropped, and a track rejected in
review disappears from the playlist on the next rewrite.
Resolving is deliberately separate from queueing. A shared link is opaque — you
cannot tell twelve tracks from four hundred by looking at it — so the track list
and count are shown before anything is committed against a quota.
**Two of the three sources are scrapes, and this is worth understanding before
you rely on it.** Deezer has a real public API. Spotify and Apple Music have no
free way to read a public playlist — Apple's MusicKit requires a paid developer
account — so muse reads the public web page and parses the JSON blob the page
already embeds for its own player. No key, no OAuth, no account. It also means
those two will break the day either company reshuffles their frontend. The
resolvers therefore fail loudly and specifically ("Spotify's embed page no
longer carries `__NEXT_DATA__` — the resolver in app.py needs updating") rather
than quietly returning nothing, because an empty playlist and a changed page
must not look the same to whoever has to fix it.
One detail that is easy to get wrong: the `.m3u` has to use **Navidrome's**
mount path, not muse's. Both containers see the same directory, muse at
`/data/media/music` and Navidrome at `/music`, and a playlist written with the
former resolves to nothing on the other side. `MUSE_PLAYLIST_PREFIX` is that
path.
### Everything goes through a bounded queue
`MUSE_WORKERS` (default 3) caps concurrent Soulseek searches. This matters:
@@ -599,6 +630,8 @@ stat -c "%h" "/data/media/music/<Artist>/<Album> (<Year>)/<NN> - <Title>.flac"
| `MUSE_AUTO_CONFIDENCE` | `0.95` | at or above this, import without asking |
| `MUSE_REVIEW_CONFIDENCE` | `0.70` | below this, held but probably wrong |
| `MUSE_LENGTH_TOLERANCE` | `0.2` | duration drift before a file is called a different recording |
| `MUSE_MAX_PLAYLIST_TRACKS` | `500` | cap on one playlist link |
| `MUSE_PLAYLIST_PREFIX` | `/music` | the library path *Navidrome* sees, for `.m3u` lines |
| `MUSE_WORKERS` | `3` | concurrent Soulseek searches |
| `MUSE_SMTP_HOST` | — | set to enable email; see **Email** above |
| `MUSE_SMTP_PORT` | `587` | |