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