feat(subsonic): browse endpoints — getArtists/getAlbum/search3/… (#295) #8

Merged
bvandeusen merged 262 commits from dev into main 2026-04-19 15:13:13 -04:00
bvandeusen commented 2026-04-19 15:09:05 -04:00 (Migrated from git.fabledsword.com)

Implements Subsonic spec §8 browse surface so a third-party Subsonic client can browse artist → album → track after a scan. Closes Fable #295.

Endpoints

  • getMusicFolders — single folder (id=1, "Music")
  • getIndexes / getArtists — alphabetical artist buckets; getIndexes adds lastModified (max artists.updated_at, unix-ms) and ignoredArticles
  • getArtist — artist + nested albums (with track counts)
  • getAlbum — album + full track list, parent artist name
  • getSong — single track + parent album/artist names
  • getAlbumList2newest | alphabeticalByName | alphabeticalByArtist | random | byGenre backed by sqlc queries; recent | frequent stub to [] pending M2 play history
  • search3 — independent artist/album/song facets, each with its own count/offset

Shape

  • Bare UUID strings on wire (no ar-/al-/tr- prefixes)
  • Dual JSON+XML struct tags on every response type; both formats verified against a live seeded DB
  • Empty slices serialize as [] not null (clients reject the latter) — enforced at albumRefs and in search3's result initializer
  • OpenSubsonic envelope fields (type, serverVersion, openSubsonic:true, apiKey) preserved

Test plan

  • go vet ./... clean
  • golangci-lint run clean (errcheck, etc.)
  • go test -race ./internal/subsonic/... green — covers UUID round-trip, index-letter bucketing (including # for digits, ? for non-ASCII), content-type mapping, date/timestamp conversion, atoi/clamp, and getMusicFolders wire shape
  • Live smoke against seeded DB — all 8 endpoints + variants (newest/alphaByName/alphaByArtist/random/byGenre/recent/frequent) + error paths (missing id, invalid UUID, missing type/query) return correct Subsonic error codes (10, 70)
  • Forgejo CI green

Known non-regressions (pre-existing): album duration: 0 (scanner doesn't extract DurationMs yet — tracked separately); a couple of seeded artists bucket under "A" because their sort_name literally starts with "Artist" (test-data artifact, not bug).

Implements Subsonic spec §8 browse surface so a third-party Subsonic client can browse artist → album → track after a scan. Closes Fable #295. ## Endpoints - `getMusicFolders` — single folder (id=1, "Music") - `getIndexes` / `getArtists` — alphabetical artist buckets; `getIndexes` adds `lastModified` (max `artists.updated_at`, unix-ms) and `ignoredArticles` - `getArtist` — artist + nested albums (with track counts) - `getAlbum` — album + full track list, parent artist name - `getSong` — single track + parent album/artist names - `getAlbumList2` — `newest | alphabeticalByName | alphabeticalByArtist | random | byGenre` backed by sqlc queries; `recent | frequent` stub to `[]` pending M2 play history - `search3` — independent artist/album/song facets, each with its own `count`/`offset` ## Shape - Bare UUID strings on wire (no `ar-`/`al-`/`tr-` prefixes) - Dual JSON+XML struct tags on every response type; both formats verified against a live seeded DB - Empty slices serialize as `[]` not `null` (clients reject the latter) — enforced at `albumRefs` and in `search3`'s result initializer - OpenSubsonic envelope fields (`type`, `serverVersion`, `openSubsonic:true`, `apiKey`) preserved ## Test plan - [x] `go vet ./...` clean - [x] `golangci-lint run` clean (errcheck, etc.) - [x] `go test -race ./internal/subsonic/...` green — covers UUID round-trip, index-letter bucketing (including `#` for digits, `?` for non-ASCII), content-type mapping, date/timestamp conversion, atoi/clamp, and `getMusicFolders` wire shape - [x] Live smoke against seeded DB — all 8 endpoints + variants (newest/alphaByName/alphaByArtist/random/byGenre/recent/frequent) + error paths (missing id, invalid UUID, missing type/query) return correct Subsonic error codes (10, 70) - [ ] Forgejo CI green Known non-regressions (pre-existing): album `duration: 0` (scanner doesn't extract DurationMs yet — tracked separately); a couple of seeded artists bucket under "A" because their `sort_name` literally starts with "Artist" (test-data artifact, not bug).
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/minstrel#8