diff --git a/internal/api/library_albums.go b/internal/api/library_albums.go new file mode 100644 index 00000000..dcb816e4 --- /dev/null +++ b/internal/api/library_albums.go @@ -0,0 +1,41 @@ +package api + +import ( + "net/http" + + "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" +) + +// handleListLibraryAlbums implements GET /api/library/albums. Mirrors +// /api/artists?sort=alpha but for albums. The new wrapping-grid page on +// the SPA infinite-scrolls against this endpoint via TanStack +// createInfiniteQuery. +func (h *handlers) handleListLibraryAlbums(w http.ResponseWriter, r *http.Request) { + limit, offset, err := parsePaging(r.URL.Query()) + if err != nil { + writeErr(w, http.StatusBadRequest, "bad_request", err.Error()) + return + } + q := dbq.New(h.pool) + rows, err := q.ListAlbumsAlphaWithArtist(r.Context(), dbq.ListAlbumsAlphaWithArtistParams{ + Limit: int32(limit), Offset: int32(offset), + }) + if err != nil { + h.logger.Error("api: list library albums", "err", err) + writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed") + return + } + total, err := q.CountAlbums(r.Context()) + if err != nil { + h.logger.Error("api: count albums", "err", err) + writeErr(w, http.StatusInternalServerError, "server_error", "count failed") + return + } + items := make([]AlbumRef, 0, len(rows)) + for _, row := range rows { + items = append(items, albumRefFrom(row.Album, row.ArtistName, 0, 0)) + } + writeJSON(w, http.StatusOK, Page[AlbumRef]{ + Items: items, Total: int(total), Limit: limit, Offset: offset, + }) +} diff --git a/internal/api/library_albums_test.go b/internal/api/library_albums_test.go new file mode 100644 index 00000000..95def123 --- /dev/null +++ b/internal/api/library_albums_test.go @@ -0,0 +1,95 @@ +package api + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + + "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" +) + +func newLibraryAlbumsRouter(h *handlers) chi.Router { + r := chi.NewRouter() + r.Get("/api/library/albums", h.handleListLibraryAlbums) + return r +} + +func doListLibraryAlbums(h *handlers, user dbq.User, query string) *httptest.ResponseRecorder { + url := "/api/library/albums" + if query != "" { + url += "?" + query + } + req := httptest.NewRequest(http.MethodGet, url, nil) + req = withUser(req, user) + w := httptest.NewRecorder() + newLibraryAlbumsRouter(h).ServeHTTP(w, req) + return w +} + +func TestLibraryAlbums_AlphaSorted(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: "ArtistA", SortName: "ArtistA", + }) + _, _ = q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{ + Title: "Zoo", SortTitle: "Zoo", ArtistID: artist.ID, + }) + _, _ = q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{ + Title: "Apple", SortTitle: "Apple", ArtistID: artist.ID, + }) + _, _ = q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{ + Title: "Mango", SortTitle: "Mango", ArtistID: artist.ID, + }) + + w := doListLibraryAlbums(h, user, "limit=10") + if w.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) + } + var got Page[AlbumRef] + if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { + t.Fatalf("decode: %v", err) + } + if got.Total != 3 { + t.Errorf("total = %d, want 3", got.Total) + } + titles := []string{got.Items[0].Title, got.Items[1].Title, got.Items[2].Title} + if titles[0] != "Apple" || titles[1] != "Mango" || titles[2] != "Zoo" { + t.Errorf("titles = %v, want [Apple Mango Zoo]", titles) + } + if got.Items[0].SortTitle != "Apple" { + t.Errorf("sort_title = %q, want Apple", got.Items[0].SortTitle) + } +} + +func TestLibraryAlbums_Paging(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: "ArtistA", SortName: "ArtistA", + }) + for _, title := range []string{"A", "B", "C", "D"} { + _, _ = q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{ + Title: title, SortTitle: title, ArtistID: artist.ID, + }) + } + + w := doListLibraryAlbums(h, user, "limit=2&offset=2") + if w.Code != http.StatusOK { + t.Fatalf("status = %d", w.Code) + } + var got Page[AlbumRef] + _ = json.Unmarshal(w.Body.Bytes(), &got) + if got.Total != 4 || got.Limit != 2 || got.Offset != 2 { + t.Errorf("envelope = %+v, want total=4 limit=2 offset=2", got) + } + if len(got.Items) != 2 || got.Items[0].Title != "C" || got.Items[1].Title != "D" { + t.Errorf("items = %v, want [C D]", got.Items) + } +}