Feishin's first-login flow hits getUser to discover the authenticated
identity's roles. We never registered the route, so chi returned a
plain-text 404 — Feishin's parser treats anything non-Subsonic as a
generic auth failure ("Failed to log in"), masking the real cause.
- Implement /rest/getUser with the full role bag. Admins get every
role; non-admins get the play-music subset. Single-user M1 means
cross-user lookups by admins return the caller's roles for now;
revisit when user management lands.
- Set sub.NotFound on /rest/* to emit a Subsonic envelope (code 0,
"Method not implemented") instead of plain text. Any future client
probing an unimplemented endpoint now sees a parseable failure.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bootstrap was creating password_hash + api_token but leaving
subsonic_password nil, which meant Subsonic clients (Feishin, Symfonium)
got ErrTokenNotSupported on t+s auth — the server had no plaintext to
hash against. Mirror the bootstrap password into subsonic_password so
the admin can sign in to Subsonic clients with the same credential
printed on first boot. Plaintext at rest is the cost of Subsonic's
legacy auth; matches Navidrome's posture.
Also folds in the local dev compose tweaks: dedicated bridge network
with postgres unpublished from the host, and a bind-mount aimed at the
operator's real library path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dev stack can now `docker compose up --build` with no extra env: mounts
./music read-only, enables startup scan, and bootstraps admin with a
generated password printed to stderr on first boot.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
getCoverArt now falls back to sidecar images next to the first track in
an album, so browse responses can advertise a coverArt id whether or not
albums.cover_art_path is set. Worst case the client gets a Subsonic 70
when no sidecar exists.
http.ServeContent handles Range/ETag/If-Modified-Since; download forces
attachment disposition. getCoverArt tries albums.cover_art_path, then
falls back to cover.{jpg,jpeg,png}/folder.{jpg,jpeg,png} next to the first
track's file. scrobble submission=false records a track into an in-memory
nowPlaying map keyed by user — M2 will read this and wire real events.
Adds the Subsonic browse wire types (MusicFolder, Index, ArtistRef,
ArtistDetail, AlbumRef, AlbumDetail, SongRef, and their response
envelopes) with dual json+xml tags so a single struct serves both
formats. Includes UUID wire helpers (uuidToID/parseUUID), indexing
helpers (indexLetter), MIME mapping (contentTypeForFormat), and the
artist-name resolver used by album listings.
IDs are bare UUID strings on the wire — endpoint context disambiguates.
Adds ListAlbumsAlphaByName, ListAlbumsAlphaByArtist (with sqlc.embed
for the joined artist sort_name), ListAlbumsNewest, ListAlbumsRandom,
ListAlbumsByGenre, and SearchAlbums. Powers getAlbumList2 type filters
and the album facet of search3.
apiKey (OpenSubsonic) is preferred. t+s uses md5(subsonic_password+salt)
with a constant-time compare. p is disabled by default and gated by
SubsonicConfig.AllowPlaintextPassword; enc:HEX obfuscation decoded.
Auth failures write a Subsonic "failed" envelope so clients don't see
HTTP 401.
Renders one wire format per ?f= parameter; JSON wraps in
{"subsonic-response":...}, XML emits with xmlns, JSONP wraps in callback
or falls back to JSON when callback is empty.
Subsonic token auth (t=md5(password+salt)) needs a reversibly stored
credential; bcrypt password_hash can't serve. NULL means the user has
not opted in, forcing apiKey (OpenSubsonic) instead.