From c5e42ec8c2d88e1b1c128686366cb86e508d409f Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 1 May 2026 18:10:04 -0400 Subject: [PATCH] feat(api): wire M6a routes; alpha artist list uses covers query Replace handleListArtists alpha branch with ListArtistsAlphaWithCovers (drops N+1 per-artist album lookup). Register /api/home and /api/library/albums in api.go. Co-Authored-By: Claude Sonnet 4.6 --- internal/api/api.go | 2 + internal/api/library.go | 86 +++++++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 5e9a5a03..8b70c5f2 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -46,11 +46,13 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev authed.Get("/artists/{id}/tracks", h.handleGetArtistTracks) authed.Get("/albums/{id}", h.handleGetAlbum) authed.Get("/albums/{id}/cover", h.handleGetCover) + authed.Get("/library/albums", h.handleListLibraryAlbums) authed.Get("/tracks/{id}", h.handleGetTrack) authed.Get("/tracks/{id}/stream", h.handleGetStream) authed.Get("/search", h.handleSearch) authed.Get("/radio", h.handleRadio) authed.Get("/discover/suggestions", h.handleListSuggestions) + authed.Get("/home", h.handleGetHome) authed.Post("/events", h.handleEvents) authed.Post("/likes/tracks/{id}", h.handleLikeTrack) authed.Delete("/likes/tracks/{id}", h.handleUnlikeTrack) diff --git a/internal/api/library.go b/internal/api/library.go index 08ab15ba..e57aee78 100644 --- a/internal/api/library.go +++ b/internal/api/library.go @@ -184,8 +184,10 @@ func (h *handlers) handleGetArtistTracks(w http.ResponseWriter, r *http.Request) } // handleListArtists implements GET /api/artists with two sort modes -// (alpha|newest) and enveloped pagination. album_count per artist is -// computed via an N+1 — acceptable at limit=200. +// (alpha|newest) and enveloped pagination. The alpha branch uses +// ListArtistsAlphaWithCovers to ship cover_url + album_count in a +// single query. The newest branch keeps its N+1 album_count lookup +// (less hot path, no covers exposed). func (h *handlers) handleListArtists(w http.ResponseWriter, r *http.Request) { sort := r.URL.Query().Get("sort") if sort == "" { @@ -200,45 +202,61 @@ func (h *handlers) handleListArtists(w http.ResponseWriter, r *http.Request) { writeErr(w, http.StatusBadRequest, "bad_request", err.Error()) return } - q := dbq.New(h.pool) - var artists []dbq.Artist + switch sort { case "newest": - artists, err = q.ListArtistsNewest(r.Context(), dbq.ListArtistsNewestParams{ + artists, err := q.ListArtistsNewest(r.Context(), dbq.ListArtistsNewestParams{ Limit: int32(limit), Offset: int32(offset), }) - default: // alpha - artists, err = q.ListArtistsAlpha(r.Context(), dbq.ListArtistsAlphaParams{ - Limit: int32(limit), Offset: int32(offset), - }) - } - if err != nil { - h.logger.Error("api: list artists failed", "err", err) - writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed") - return - } - total, err := q.CountArtists(r.Context()) - if err != nil { - h.logger.Error("api: count artists failed", "err", err) - writeErr(w, http.StatusInternalServerError, "server_error", "count failed") - return - } - - items := make([]ArtistRef, 0, len(artists)) - for _, a := range artists { - albums, aerr := q.ListAlbumsByArtist(r.Context(), a.ID) - if aerr != nil { - h.logger.Error("api: list albums for artist failed", "err", aerr) + if err != nil { + h.logger.Error("api: list artists newest", "err", err) writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed") return } - items = append(items, artistRefFrom(a, len(albums))) + total, terr := q.CountArtists(r.Context()) + if terr != nil { + h.logger.Error("api: count artists", "err", terr) + writeErr(w, http.StatusInternalServerError, "server_error", "count failed") + return + } + items := make([]ArtistRef, 0, len(artists)) + for _, a := range artists { + albums, aerr := q.ListAlbumsByArtist(r.Context(), a.ID) + if aerr != nil { + h.logger.Error("api: list albums for artist", "err", aerr) + writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed") + return + } + items = append(items, artistRefFrom(a, len(albums))) + } + writeJSON(w, http.StatusOK, Page[ArtistRef]{ + Items: items, Total: int(total), Limit: limit, Offset: offset, + }) + return + + default: // alpha + rows, err := q.ListArtistsAlphaWithCovers(r.Context(), dbq.ListArtistsAlphaWithCoversParams{ + Limit: int32(limit), Offset: int32(offset), + }) + if err != nil { + h.logger.Error("api: list artists alpha", "err", err) + writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed") + return + } + total, terr := q.CountArtists(r.Context()) + if terr != nil { + h.logger.Error("api: count artists", "err", terr) + writeErr(w, http.StatusInternalServerError, "server_error", "count failed") + return + } + items := make([]ArtistRef, 0, len(rows)) + for _, row := range rows { + items = append(items, artistRefFromCovered(row.Artist, int(row.AlbumCount), row.CoverAlbumID)) + } + writeJSON(w, http.StatusOK, Page[ArtistRef]{ + Items: items, Total: int(total), Limit: limit, Offset: offset, + }) + return } - writeJSON(w, http.StatusOK, Page[ArtistRef]{ - Items: items, - Total: int(total), - Limit: limit, - Offset: offset, - }) }