Commit Graph

11 Commits

Author SHA1 Message Date
bvandeusen 7c11cdc4d1 fix(server): handleGetStream - auth check before DB lookup
test-go / test (push) Successful in 28s
test-go / integration (push) Successful in 9m40s
TestRoutesRegisteredInMount failed because handleGetStream did the
DB lookup (404 on missing track) BEFORE streamAuthOk (401 on
unauth). For an unauth request to a non-existent track, the test
saw 404 and concluded the route wasn't registered when actually it
was - the handler just bailed at the lookup before auth.

Reorder: extract trackID via chi.URLParam, run streamAuthOk on the
raw path id first (the HMAC token is signed over the same id
string so we don't need the resolved row yet), then do the DB
lookup. Test now sees 401 on the unauth probe as it expected.

Also closes a small info-leak: previously a 404/401 differential
let unauth callers probe which track IDs exist. Now both unknown
and known IDs return 401 for unauth requests.
2026-06-03 13:43:28 -04:00
bvandeusen 236637fcd3 feat(server): HMAC stream token auth path (UPnP slice 1/6)
test-go / test (push) Failing after 24s
test-go / integration (push) Failing after 9m54s
Adds SignStreamToken / VerifyStreamToken (HMAC-SHA256 over
trackID|exp) and modifies handleGetStream to accept either the
existing session cookie OR a valid signed token. Stream route
moved out of the authed group so the handler's own auth check
runs and the token bypass is reachable.

Enables Sonos / UPnP speakers to fetch the stream URL without
carrying the user's session cookie - they cannot. The token is
short-lived (max 24h per the design); expiry checked at request
time only, not per-byte, so long tracks play through.

streamSecret field on handlers is nil for now; Task 2 wires the
loader (env var with auto-generated fallback persisted in
app_preferences).

Adds auth.OptionalUser - the permissive sibling of RequireUser
that attaches the user to context when a valid cookie / bearer is
present but does NOT 401 on absence. The stream route is wrapped
with it so the handler can fall through to the token path when
no session is present.

newLibraryRouter (test fixture) gets a synthetic-user middleware
on the stream route so existing media_test tests keep passing
without seeding a real session row - production traffic uses
auth.OptionalUser, the test path uses auth.UserCtxKeyForTest().

Five tests cover round-trip, tampered token rejection, expiry,
wrong-track-ID, and wrong-secret rejection. CI verifies.
2026-06-03 11:34:02 -04:00
bvandeusen 1ddde12959 perf: lazy player source build + Cache-Control on byte endpoints
Two unrelated wins as a single batch.

Flutter — lazy source building in setQueueFromTracks:
Today: Future.wait builds all N AudioSource objects (drift queries +
LockCaching ctor) before the player can call setAudioSources →
play(). Measured at 83ms for a 25-track playlist, on top of the
~285ms initial-source preload.

New flow: build only the initial source, hand it to setAudioSources
([initial], initialIndex: 0) so play() can start, then background-
fill the rest. Forward direction (skipNext targets) added via
addAudioSource. Backward direction (skipPrev) inserted at index 0..
initialIndex-1 with _suppressIndexUpdates true so the unavoidable
currentIndex shifts don't push the wrong MediaItem onto the stream.

Saves the up-front source-build wait — tap-to-audio for long queues
should drop by ~80-100ms even on cache hits.

Server — Cache-Control on the three byte-serving endpoints:
- /api/albums/{id}/cover: max-age=86400, must-revalidate. Covers
  change rarely (re-scan, MBID enrichment); a day of cache is safe
  and skips conditional GETs for the bulk of a session.
- /api/playlists/{id}/cover: max-age=300, must-revalidate. Collages
  recompute when contents change; short enough for edits to feel
  fresh, long enough to skip repeat fetches during a session.
- /api/tracks/{id}/stream: max-age=31536000, immutable. Track bytes
  are immutable for a given id (scanner re-indexes by file_path; new
  files get new ids). LockCachingAudioSource on the Flutter side
  already disk-caches, but proper headers let it skip even the
  conditional 304 on repeat plays.
2026-05-11 22:18:58 -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 49218e5231 refactor(server/api): resolveByID helper for parseUUID->fetch->404 (T4) 2026-05-07 21:28:20 -04:00
bvandeusen 91f4f5c18a refactor(server/api): migrate library/media handlers to writeErr(err) (T3b) 2026-05-07 20:48:20 -04:00
bvandeusen ef6c09dbfb refactor(server/m7-353): consolidate findSidecarCover into coverart package 2026-05-04 14:46:28 -04:00
bvandeusen 3656461b20 feat(api): GET /api/tracks/{id}/stream with Range support
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 22:44:48 -04:00
bvandeusen 945b773cbd feat(api): GET /api/albums/{id}/cover with sidecar fallback 2026-04-21 22:37:53 -04:00
bvandeusen 5af6d7fac6 docs(api): document intentional audioContentType divergence from subsonic 2026-04-21 22:35:29 -04:00
bvandeusen c799e5c1c9 feat(api): scaffold media helpers for cover + stream endpoints 2026-04-21 22:30:25 -04:00