feat(api): GET /api/home/index for per-item rendering (Slice B)

Sibling to /api/home that returns the same five sections (recently
added, rediscover albums, rediscover artists, most played, last
played) but as flat slices of entity ID strings instead of
denormalized objects. The Flutter client uses this to drive its
per-item rendering pass — small discovery response then per-tile
hydration via the existing /api/albums/:id, /api/artists/:id,
/api/tracks/:id endpoints.

Reuses recommendation.HomeData so the DB cost is identical to
/api/home. JSON payload shrinks roughly an order of magnitude on
populated libraries (no embedded title / artist / cover URL fields).

Old /api/home stays untouched so the web client and older Flutter
builds keep working — no min-client-version bump needed until both
clients have migrated.
This commit is contained in:
2026-05-13 20:41:44 -04:00
parent 0504cae27c
commit 0119eacf14
4 changed files with 131 additions and 0 deletions
+64
View File
@@ -77,3 +77,67 @@ func TestHome_RecentlyAddedSurfacesAlbums(t *testing.T) {
t.Errorf("artist_name = %q, want ArtistOne", got.RecentlyAddedAlbums[0].ArtistName)
}
}
func newHomeIndexRouter(h *handlers) chi.Router {
r := chi.NewRouter()
r.Get("/api/home/index", h.handleGetHomeIndex)
return r
}
func doGetHomeIndex(h *handlers, user dbq.User) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, "/api/home/index", nil)
req = withUser(req, user)
w := httptest.NewRecorder()
newHomeIndexRouter(h).ServeHTTP(w, req)
return w
}
func TestHomeIndex_EmptyForNewUser(t *testing.T) {
h, pool := testHandlers(t)
user := seedUser(t, pool, "alice", "pw", false)
w := doGetHomeIndex(h, user)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
}
var got HomeIndexPayload
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("decode: %v", err)
}
// Same non-nil slice contract as /api/home so client parsers can
// trust `length == 0` instead of branching on null.
if got.RecentlyAddedAlbums == nil || got.RediscoverAlbums == nil ||
got.RediscoverArtists == nil || got.MostPlayedTracks == nil ||
got.LastPlayedArtists == nil {
t.Errorf("payload has nil slice: %+v", got)
}
if len(got.RecentlyAddedAlbums) != 0 {
t.Errorf("RecentlyAddedAlbums len = %d, want 0", len(got.RecentlyAddedAlbums))
}
}
func TestHomeIndex_RecentlyAddedReturnsIDs(t *testing.T) {
h, pool := testHandlers(t)
user := seedUser(t, pool, "alice", "pw", false)
q := dbq.New(pool)
artist, _ := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{
Name: "ArtistOne", SortName: "ArtistOne",
})
album, _ := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
Title: "Newest", SortTitle: "Newest", ArtistID: artist.ID,
})
w := doGetHomeIndex(h, user)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
}
var got HomeIndexPayload
_ = json.Unmarshal(w.Body.Bytes(), &got)
if len(got.RecentlyAddedAlbums) != 1 {
t.Fatalf("len = %d, want 1", len(got.RecentlyAddedAlbums))
}
want := uuidToString(album.ID)
if got.RecentlyAddedAlbums[0] != want {
t.Errorf("id = %q, want %q", got.RecentlyAddedAlbums[0], want)
}
}