feat(server): similar-artists + per-user artist top-tracks endpoints
GET /api/artists/{id}/similar — in-library artists ranked by similarity
score (deduped across sources), ArtistRef list with cover + album count.
GET /api/artists/{id}/top-tracks — current user's most-played tracks for
the artist (skips excluded, quarantine filtered).
This commit is contained in:
@@ -86,6 +86,8 @@ 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("/artists/{id}/similar", h.handleGetSimilarArtists)
|
||||
authed.Get("/artists/{id}/top-tracks", h.handleGetArtistTopTracks)
|
||||
authed.Get("/albums/{id}", h.handleGetAlbum)
|
||||
authed.Get("/albums/{id}/cover", h.handleGetCover)
|
||||
authed.Get("/library/shuffle", h.handleLibraryShuffle)
|
||||
|
||||
@@ -159,6 +159,85 @@ func (h *handlers) handleGetArtistTracks(w http.ResponseWriter, r *http.Request)
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// similarArtistsDefaultLimit caps the artist-detail "similar artists" strip;
|
||||
// artistTopTracksDefaultLimit caps the per-user "top tracks" panel.
|
||||
const (
|
||||
similarArtistsDefaultLimit = 12
|
||||
artistTopTracksDefaultLimit = 5
|
||||
)
|
||||
|
||||
// handleGetSimilarArtists implements GET /api/artists/{id}/similar. Returns
|
||||
// in-library artists similar to {id} (ranked by similarity score) as a flat
|
||||
// ArtistRef list with cover + album count. Empty when the similarity ingest
|
||||
// has no matches yet; 404 when the artist doesn't exist.
|
||||
func (h *handlers) handleGetSimilarArtists(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := requireURLUUID(w, r, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
q := dbq.New(h.pool)
|
||||
if _, err := q.GetArtistByID(r.Context(), id); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
writeErr(w, apierror.NotFound("artist"))
|
||||
return
|
||||
}
|
||||
h.logger.Error("api: get artist for similar", "err", err)
|
||||
writeErr(w, apierror.InternalMsg("lookup failed", err))
|
||||
return
|
||||
}
|
||||
rows, err := q.ListSimilarArtistsForArtist(r.Context(), dbq.ListSimilarArtistsForArtistParams{
|
||||
SeedArtistID: id, ResultLimit: similarArtistsDefaultLimit,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("api: list similar artists", "err", err)
|
||||
writeErr(w, apierror.InternalMsg("lookup failed", err))
|
||||
return
|
||||
}
|
||||
out := make([]ArtistRef, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, artistRefFromCovered(row.Artist, int(row.AlbumCount), row.CoverAlbumID))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// handleGetArtistTopTracks implements GET /api/artists/{id}/top-tracks. Returns
|
||||
// the current user's most-played tracks for {id} (skips excluded, quarantine
|
||||
// filtered). Empty when the user hasn't played this artist; 404 when the
|
||||
// artist doesn't exist.
|
||||
func (h *handlers) handleGetArtistTopTracks(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := requireURLUUID(w, r, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
user, ok := requireUser(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
q := dbq.New(h.pool)
|
||||
if _, err := q.GetArtistByID(r.Context(), id); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
writeErr(w, apierror.NotFound("artist"))
|
||||
return
|
||||
}
|
||||
h.logger.Error("api: get artist for top tracks", "err", err)
|
||||
writeErr(w, apierror.InternalMsg("lookup failed", err))
|
||||
return
|
||||
}
|
||||
rows, err := q.ListMostPlayedTracksForArtist(r.Context(), dbq.ListMostPlayedTracksForArtistParams{
|
||||
ArtistID: id, UserID: user.ID, ResultLimit: artistTopTracksDefaultLimit,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("api: list artist top tracks", "err", err)
|
||||
writeErr(w, apierror.InternalMsg("lookup failed", err))
|
||||
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)
|
||||
}
|
||||
|
||||
// handleLibraryShuffle implements GET /api/library/shuffle?limit=N —
|
||||
// the online source for the client's always-present "Shuffle all"
|
||||
// (#427 S4). N random tracks across the whole library, per-user
|
||||
|
||||
Reference in New Issue
Block a user