0119eacf14
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.
103 lines
4.3 KiB
Go
103 lines
4.3 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/recommendation"
|
|
)
|
|
|
|
// handleGetHome implements GET /api/home. Returns five sections in one
|
|
// payload; sections that have no rows render as []. 500 on any underlying
|
|
// DB failure — the SPA should render either empty states or full content,
|
|
// not a half-broken page.
|
|
func (h *handlers) handleGetHome(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
data, err := recommendation.HomeData(r.Context(), h.pool, user.ID)
|
|
if err != nil {
|
|
h.logger.Error("api: home data", "err", err)
|
|
writeErr(w, apierror.InternalMsg("failed to load home", err))
|
|
return
|
|
}
|
|
|
|
out := HomePayload{
|
|
RecentlyAddedAlbums: make([]AlbumRef, 0, len(data.RecentlyAddedAlbums)),
|
|
RediscoverAlbums: make([]AlbumRef, 0, len(data.RediscoverAlbums)),
|
|
RediscoverArtists: make([]ArtistRef, 0, len(data.RediscoverArtists)),
|
|
MostPlayedTracks: make([]TrackRef, 0, len(data.MostPlayedTracks)),
|
|
LastPlayedArtists: make([]ArtistRef, 0, len(data.LastPlayedArtists)),
|
|
}
|
|
for _, row := range data.RecentlyAddedAlbums {
|
|
out.RecentlyAddedAlbums = append(out.RecentlyAddedAlbums, albumRefFrom(row.Album, row.ArtistName, 0, 0))
|
|
}
|
|
for _, row := range data.RediscoverAlbums {
|
|
out.RediscoverAlbums = append(out.RediscoverAlbums, albumRefFrom(row.Album, row.ArtistName, 0, 0))
|
|
}
|
|
for _, row := range data.RediscoverArtists {
|
|
out.RediscoverArtists = append(out.RediscoverArtists, artistRefFromCovered(row.Artist, int(row.AlbumCount), row.CoverAlbumID))
|
|
}
|
|
for _, row := range data.MostPlayedTracks {
|
|
out.MostPlayedTracks = append(out.MostPlayedTracks, trackRefFrom(row.Track, row.AlbumTitle, row.ArtistName))
|
|
}
|
|
for _, row := range data.LastPlayedArtists {
|
|
out.LastPlayedArtists = append(out.LastPlayedArtists, artistRefFromCovered(row.Artist, int(row.AlbumCount), row.CoverAlbumID))
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
// handleGetHomeIndex implements GET /api/home/index. Returns the same
|
|
// five sections /api/home computes, but as flat slices of entity IDs
|
|
// rather than denormalized objects. Drives the Flutter per-item
|
|
// rendering path — the client hydrates each tile against the
|
|
// per-entity endpoints, so this response is tiny (a few KB at most)
|
|
// and the cold-visit round-trip is correspondingly short.
|
|
//
|
|
// Shares the recommendation.HomeData computation with /api/home; the
|
|
// DB cost is identical. A future optimisation could add ID-only
|
|
// query variants, but the JSON savings already shrink the wire
|
|
// payload by roughly an order of magnitude on populated libraries.
|
|
func (h *handlers) handleGetHomeIndex(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
data, err := recommendation.HomeData(r.Context(), h.pool, user.ID)
|
|
if err != nil {
|
|
h.logger.Error("api: home index data", "err", err)
|
|
writeErr(w, apierror.InternalMsg("failed to load home", err))
|
|
return
|
|
}
|
|
|
|
out := HomeIndexPayload{
|
|
RecentlyAddedAlbums: make([]string, 0, len(data.RecentlyAddedAlbums)),
|
|
RediscoverAlbums: make([]string, 0, len(data.RediscoverAlbums)),
|
|
RediscoverArtists: make([]string, 0, len(data.RediscoverArtists)),
|
|
MostPlayedTracks: make([]string, 0, len(data.MostPlayedTracks)),
|
|
LastPlayedArtists: make([]string, 0, len(data.LastPlayedArtists)),
|
|
}
|
|
for _, row := range data.RecentlyAddedAlbums {
|
|
out.RecentlyAddedAlbums = append(out.RecentlyAddedAlbums, uuidToString(row.Album.ID))
|
|
}
|
|
for _, row := range data.RediscoverAlbums {
|
|
out.RediscoverAlbums = append(out.RediscoverAlbums, uuidToString(row.Album.ID))
|
|
}
|
|
for _, row := range data.RediscoverArtists {
|
|
out.RediscoverArtists = append(out.RediscoverArtists, uuidToString(row.Artist.ID))
|
|
}
|
|
for _, row := range data.MostPlayedTracks {
|
|
out.MostPlayedTracks = append(out.MostPlayedTracks, uuidToString(row.Track.ID))
|
|
}
|
|
for _, row := range data.LastPlayedArtists {
|
|
out.LastPlayedArtists = append(out.LastPlayedArtists, uuidToString(row.Artist.ID))
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
// Compile-time guard that dbq has the row types we need; if the SQL is
|
|
// regenerated with a different name, this fails fast at build time.
|
|
var _ = dbq.ListMostPlayedTracksForUserRow{}.Track
|