29 Commits

Author SHA1 Message Date
bvandeusen 024493f2a7 refactor(server): unify stream URL builders + MIME tables + cover-path helper
test-go / test (push) Successful in 28s
test-go / integration (push) Has been cancelled
Closes Scribe #614, #615, server half of #616 surfaced by the 2026-06-04 divergent-provider audit.

- streamURL helper now used everywhere /api/tracks/{id}/stream is built (was inline concat in playlists.go and cast_token.go); add streamURLWithExt for the .ext cast variant.

- audioContentType in media.go is the canonical file_format -> MIME lookup; mimeForFormat in cast_token.go is now a thin wrapper that overrides the unknown-format fallback to audio/mpeg (Sonos rejects octet-stream). Adds mpeg/vorbis/wave aliases. Subsonic's contentTypeForFormat stays frozen per docs.

- coverart.ResolveAlbumPath extracted; api and subsonic both delegate to it.
2026-06-04 08:29:51 -04:00
bvandeusen 4ce7be4296 fix(server): repair go vet failures from A1+A2 cleanup (orphan imports + needle deref) 2026-05-08 10:46:43 -04:00
bvandeusen bffa397250 refactor(server/sql): unify *ForUser track queries via nullable user_id (A1) 2026-05-08 08:15:19 -04:00
bvandeusen 4e3bd46d69 fix(server,web/m7-353): post-review fixes (test compilation, AlbumRef field, bulk handler, design system)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:21:24 -04:00
bvandeusen ef6c09dbfb refactor(server/m7-353): consolidate findSidecarCover into coverart package 2026-05-04 14:46:28 -04:00
bvandeusen 937a1930ad test: add dbtest.ResetDB helper that preserves admin user
Integration tests previously TRUNCATEd the users table at setup,
which wiped the operator's admin login every time the suite ran
against a Postgres instance shared with their dev environment.

This commit:

- Adds internal/dbtest/reset.go exposing ResetDB(t, pool) and
  TestUserPrefix. ResetDB truncates every data table EXCEPT users,
  then deletes only users whose username starts with TestUserPrefix.
- Migrates 9 test files (subsonic/scrobble, subsonic/star,
  recommendation/candidates, scrobble/queue, similarity/worker,
  playevents/writer, playsessions/service, api/auth, api/me) to
  call ResetDB instead of issuing TRUNCATE-with-users.
- Renames hardcoded test usernames to use TestUserPrefix so ResetDB
  cleans them between runs. seedUser in api/auth_test now prefixes
  internally; existing call sites pass bare names.
- Leaves auth/bootstrap_test.go alone — it legitimately tests
  first-time admin bootstrap and must wipe users.

Verified locally: full integration suite with -p 1 runs cleanly
under the existing MINSTREL_TEST_DATABASE_URL setup, and the admin
row in the shared dev DB survives the run.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:55:06 -04:00
bvandeusen bf7c5ad47d feat(subsonic): wire contextual_likes capture/soft-delete into star/unstar
handleStar's track branch calls playevents.CaptureContextualLikeIfPlaying
when the underlying LikeTrack actually inserted a row. handleUnstar
calls SoftDeleteContextualLikes after every track-id unstar. Same
helpers as the api surface — single source of truth.

mediaHandlers struct gains a logger field threaded through Mount.
2026-04-27 11:25:10 -04:00
bvandeusen d43d8df6d5 feat(db): add session-vector capture queries; LikeTrack returns row count
ListRecentSessionTracks + UpdatePlayEventVector for the vector capture
path inside RecordPlayStarted. LikeTrack switches to :execrows so the
contextual-likes capture can detect insert vs already-exists. Existing
callers updated to ignore the count for now; later tasks consume it.
2026-04-27 11:16:05 -04:00
bvandeusen c7f4adbcc3 feat(subsonic): add getStarred and getStarred2 handlers
Both return user's starred artists/albums/songs sorted liked_at DESC.
Cap at 500 entries per category for v1 (Subsonic spec doesn't define
pagination on these endpoints; M3+ can revisit if needed).
2026-04-26 16:45:04 -04:00
bvandeusen 32fb3fec20 feat(subsonic): add /rest/star and /rest/unstar handlers
Validate-all-first atomicity on star: a missing entity refuses the whole
call with Subsonic error 70. Unstar is a best-effort delete (missing
entities are no-ops, matching client expectations after library moves).
2026-04-26 16:43:20 -04:00
bvandeusen 599a19f780 feat(subsonic): wire /rest/scrobble into playevents.Writer
submission=false → play_started (replaces in-memory nowPlayingMap).
submission=true (default) → synthetic completed play. The nowPlayingMap
struct + factory + the nowPlaying field on mediaHandlers are deleted;
play_events WHERE ended_at IS NULL is now the source of truth for
'currently playing,' reachable from M3+ work.

api.Mount now takes the shared *playevents.Writer instead of cfg so
the same writer instance feeds both surfaces.
2026-04-26 00:23:20 -04:00
bvandeusen dfcdf4d3ca feat(subsonic): getUser + envelope-shaped 404 for /rest/*
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>
2026-04-19 21:56:31 -04:00
bvandeusen 3c95740ebe feat(subsonic): emit coverArt id unconditionally (#296)
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.
2026-04-19 19:28:30 +00:00
bvandeusen 547adb74ac feat(subsonic): register media handlers (#296)
Wire /stream, /download, /getCoverArt, /scrobble onto /rest via mediaHandlers.
2026-04-19 19:27:15 +00:00
bvandeusen eb776bdaa2 test(subsonic): cover stream helpers (#296)
findSidecarCover priority (cover.* wins over folder.*), imageContentType
mapping, and nowPlayingMap write/read round-trip.
2026-04-19 19:26:59 +00:00
bvandeusen 0760b48037 feat(subsonic): add stream/download/getCoverArt/scrobble (#296)
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.
2026-04-19 19:26:39 +00:00
bvandeusen 9997781ab8 test(subsonic): cover browse type helpers (#295)
UUID round-trip, index letter bucketing, content-type mapping, date/ts
conversions, and small number utilities used by browse handlers.
2026-04-19 19:08:38 +00:00
bvandeusen d982bcb2ff test(subsonic): pin getMusicFolders wire shape (#295) 2026-04-19 19:08:18 +00:00
bvandeusen ca92cec159 feat(subsonic): register browse endpoints (#295)
Wire getMusicFolders, getIndexes, getArtists, getArtist, getAlbum,
getAlbumList2, getSong, and search3 onto the /rest router via browseHandlers.
2026-04-19 19:08:08 +00:00
bvandeusen 252428a104 feat(subsonic): add browse handlers (#295)
Implement getMusicFolders, getIndexes, getArtists, getArtist, getAlbum,
getSong, getAlbumList2, and search3. Album list types supported: newest,
alphabeticalByName, alphabeticalByArtist, random, byGenre, recent, frequent
(recent/frequent stub to [] pending M2 play history).
2026-04-19 19:07:50 +00:00
bvandeusen 71e8849dca subsonic: browse response shapes and conversion helpers (#295)
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.
2026-04-19 19:05:03 +00:00
bvandeusen 98818fe28c fix(subsonic): check fmt.Fprintf return (errcheck lint) 2026-04-19 18:07:36 +00:00
bvandeusen 841a3d8165 test(subsonic): decodePassword + md5(password+salt) derivation 2026-04-19 17:39:12 +00:00
bvandeusen 1885d197d6 test(subsonic): envelope JSON/XML/JSONP + WriteFail 2026-04-19 17:39:03 +00:00
bvandeusen da2bb672f0 feat(subsonic): mount /rest router with /ping and /getLicense
Exposes each handler at /rest/name and /rest/name.view, GET+POST, for
client convention compatibility.
2026-04-19 17:38:44 +00:00
bvandeusen 2a75fc6687 feat(subsonic): ping.view + getLicense.view handlers 2026-04-19 17:38:34 +00:00
bvandeusen cdf56801cd feat(subsonic): auth middleware (apiKey / t+s / p gated)
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.
2026-04-19 17:38:28 +00:00
bvandeusen fd408d78af feat(subsonic): response envelope with JSON/XML/JSONP emit
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.
2026-04-19 17:38:07 +00:00
bvandeusen 4a447e081d feat(subsonic): error code constants 2026-04-19 17:37:49 +00:00