feat(api): add GET /api/artists/{id}/tracks handler
Wires ListArtistTracksForUser (Task 3 SQL) to a new handler that returns all quarantine-filtered tracks for an artist as a flat TrackRef list. 404 when the artist doesn't exist. Route registered in Mount(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev
|
||||
|
||||
authed.Get("/artists", h.handleListArtists)
|
||||
authed.Get("/artists/{id}", h.handleGetArtist)
|
||||
authed.Get("/artists/{id}/tracks", h.handleGetArtistTracks)
|
||||
authed.Get("/albums/{id}", h.handleGetAlbum)
|
||||
authed.Get("/albums/{id}/cover", h.handleGetCover)
|
||||
authed.Get("/tracks/{id}", h.handleGetTrack)
|
||||
|
||||
@@ -143,6 +143,46 @@ func (h *handlers) handleGetArtist(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, detail)
|
||||
}
|
||||
|
||||
// handleGetArtistTracks implements GET /api/artists/{id}/tracks. Returns
|
||||
// every track the artist has across albums (per-user-quarantine filtered)
|
||||
// as a flat list. Used by ArtistCard's play affordance, which shuffles
|
||||
// client-side. 404 when the artist doesn't exist.
|
||||
func (h *handlers) handleGetArtistTracks(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := parseUUID(chi.URLParam(r, "id"))
|
||||
if !ok {
|
||||
writeErr(w, http.StatusBadRequest, "bad_request", "invalid artist id")
|
||||
return
|
||||
}
|
||||
user, ok := auth.UserFromContext(r.Context())
|
||||
if !ok {
|
||||
writeErr(w, http.StatusUnauthorized, "unauthorized", "authentication required")
|
||||
return
|
||||
}
|
||||
q := dbq.New(h.pool)
|
||||
if _, err := q.GetArtistByID(r.Context(), id); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
writeErr(w, http.StatusNotFound, "not_found", "artist not found")
|
||||
return
|
||||
}
|
||||
h.logger.Error("api: get artist for tracks", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed")
|
||||
return
|
||||
}
|
||||
rows, err := q.ListArtistTracksForUser(r.Context(), dbq.ListArtistTracksForUserParams{
|
||||
ArtistID: id, UserID: user.ID,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("api: list artist tracks", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed")
|
||||
return
|
||||
}
|
||||
out := make([]TrackRef, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, trackRefFrom(row.Track, row.AlbumTitle, row.ArtistName))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/config"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/playevents"
|
||||
)
|
||||
|
||||
@@ -465,3 +467,103 @@ func TestRoutesRegisteredInMount(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetArtistTracks_HappyPath(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",
|
||||
})
|
||||
album, _ := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
|
||||
Title: "AlbumA", SortTitle: "AlbumA", ArtistID: artist.ID,
|
||||
})
|
||||
_, _ = q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
|
||||
Title: "T1", AlbumID: album.ID, ArtistID: artist.ID,
|
||||
DurationMs: 1000, FilePath: "/tmp/m6a-T1.mp3",
|
||||
FileSize: 1, FileFormat: "mp3",
|
||||
})
|
||||
_, _ = q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
|
||||
Title: "T2", AlbumID: album.ID, ArtistID: artist.ID,
|
||||
DurationMs: 1000, FilePath: "/tmp/m6a-T2.mp3",
|
||||
FileSize: 1, FileFormat: "mp3",
|
||||
})
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Get("/api/artists/{id}/tracks", h.handleGetArtistTracks)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/artists/"+uuidToString(artist.ID)+"/tracks", nil)
|
||||
req = withUser(req, user)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
|
||||
}
|
||||
var got []TrackRef
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("len = %d, want 2", len(got))
|
||||
}
|
||||
if got[0].ArtistName != "ArtistA" || got[0].AlbumTitle != "AlbumA" {
|
||||
t.Errorf("got = %+v", got[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetArtistTracks_HonorsQuarantine(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: "ArtistB", SortName: "ArtistB",
|
||||
})
|
||||
album, _ := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
|
||||
Title: "AlbumB", SortTitle: "AlbumB", ArtistID: artist.ID,
|
||||
})
|
||||
track, _ := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
|
||||
Title: "Q", AlbumID: album.ID, ArtistID: artist.ID,
|
||||
DurationMs: 1000, FilePath: "/tmp/m6a-Q.mp3",
|
||||
FileSize: 1, FileFormat: "mp3",
|
||||
})
|
||||
if _, err := pool.Exec(context.Background(),
|
||||
`INSERT INTO lidarr_quarantine (user_id, track_id, reason) VALUES ($1, $2, 'bad_rip')`,
|
||||
user.ID, track.ID,
|
||||
); err != nil {
|
||||
t.Fatalf("quarantine insert: %v", err)
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Get("/api/artists/{id}/tracks", h.handleGetArtistTracks)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/artists/"+uuidToString(artist.ID)+"/tracks", nil)
|
||||
req = withUser(req, user)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d", w.Code)
|
||||
}
|
||||
var got []TrackRef
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &got)
|
||||
if len(got) != 0 {
|
||||
t.Errorf("len = %d, want 0 (quarantined)", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetArtistTracks_NotFound(t *testing.T) {
|
||||
h, pool := testHandlers(t)
|
||||
user := seedUser(t, pool, "alice", "pw", false)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Get("/api/artists/{id}/tracks", h.handleGetArtistTracks)
|
||||
// Random UUID — no such artist exists.
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/artists/00000000-0000-0000-0000-000000000000/tracks", nil)
|
||||
req = withUser(req, user)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("status = %d, want 404", w.Code)
|
||||
}
|
||||
_ = pool
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user