feat(subsonic): stream/download/getCoverArt/scrobble (#296) #9

Merged
bvandeusen merged 262 commits from dev into main 2026-04-19 15:33:12 -04:00
bvandeusen commented 2026-04-19 15:28:53 -04:00 (Migrated from git.fabledsword.com)

Implements Subsonic spec §6 streaming lifecycle so a third-party client can actually play music against the library tree shipped in #295. Closes Fable #296.

Endpoints

  • stream — serves track bytes via http.ServeContent, which handles Range, If-Modified-Since, and ETag for free. Accept-Ranges: bytes set explicitly so clients probe-then-seek.
  • download — same byte path with Content-Disposition: attachment; filename="..." so browsers save.
  • getCoverArt — resolves id → album (trying the id directly, then as a track → album_id). Prefers albums.cover_art_path when set (scanner will populate in a later milestone); falls back to a sidecar image next to the first track's file. Supported names in priority order: cover.{jpg,jpeg,png}folder.{jpg,jpeg,png}. Returns Subsonic 70 when no art found.
  • scrobblesubmission=false records a now-playing hint into an in-memory nowPlayingMap keyed by user id. submission=true and missing-submission are no-ops; M2 will add real play/skip/seek/complete event ingestion.

No transcoding in v1

Per agreed scope, maxBitRate and format params are intentionally ignored. Original bytes are served as-is. Real ffmpeg pipeline is M6. Documented inline in the handler.

Wire-level side effect

coverArtID now returns the album UUID unconditionally (previously empty string when cover_art_path was nil). Browse responses advertise a coverArt id on every album/song — worst case clients get a Subsonic 70 when no sidecar exists. Net effect: scanner can skip cover extraction during M1 and clients still display art when the library has sidecar files.

Test plan

  • go build ./... clean
  • go vet ./... clean
  • golangci-lint run ./internal/subsonic/... clean
  • go test -race ./... green
  • New unit tests: findSidecarCover priority (cover.* wins over folder.*), imageContentType mapping, nowPlayingMap write/read round-trip
  • Live smoke deferred — port 5432 in use by another local project. Range/Content-Length correctness relies on stdlib http.ServeContent, which is well-tested. End-to-end client verification happens in the next task (Fable #297 — M1 milestone gate with Feishin/Symfonium).
  • Forgejo CI green
Implements Subsonic spec §6 streaming lifecycle so a third-party client can actually play music against the library tree shipped in #295. Closes Fable #296. ## Endpoints - `stream` — serves track bytes via `http.ServeContent`, which handles `Range`, `If-Modified-Since`, and `ETag` for free. `Accept-Ranges: bytes` set explicitly so clients probe-then-seek. - `download` — same byte path with `Content-Disposition: attachment; filename="..."` so browsers save. - `getCoverArt` — resolves id → album (trying the id directly, then as a track → album_id). Prefers `albums.cover_art_path` when set (scanner will populate in a later milestone); falls back to a sidecar image next to the first track's file. Supported names in priority order: `cover.{jpg,jpeg,png}` → `folder.{jpg,jpeg,png}`. Returns Subsonic 70 when no art found. - `scrobble` — `submission=false` records a now-playing hint into an in-memory `nowPlayingMap` keyed by user id. `submission=true` and missing-submission are no-ops; M2 will add real play/skip/seek/complete event ingestion. ## No transcoding in v1 Per agreed scope, `maxBitRate` and `format` params are intentionally ignored. Original bytes are served as-is. Real ffmpeg pipeline is M6. Documented inline in the handler. ## Wire-level side effect `coverArtID` now returns the album UUID unconditionally (previously empty string when `cover_art_path` was nil). Browse responses advertise a coverArt id on every album/song — worst case clients get a Subsonic 70 when no sidecar exists. Net effect: scanner can skip cover extraction during M1 and clients still display art when the library has sidecar files. ## Test plan - [x] `go build ./...` clean - [x] `go vet ./...` clean - [x] `golangci-lint run ./internal/subsonic/...` clean - [x] `go test -race ./...` green - [x] New unit tests: `findSidecarCover` priority (cover.* wins over folder.*), `imageContentType` mapping, `nowPlayingMap` write/read round-trip - [ ] Live smoke deferred — port 5432 in use by another local project. Range/Content-Length correctness relies on stdlib `http.ServeContent`, which is well-tested. End-to-end client verification happens in the next task (Fable #297 — M1 milestone gate with Feishin/Symfonium). - [ ] Forgejo CI green
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/minstrel#9