feat(api): extend ArtistRef/AlbumRef + add HomePayload for M6a

- ArtistRef gains SortName and CoverURL fields
- AlbumRef gains SortTitle field
- albumRefFrom now populates SortTitle from dbq.Album.SortTitle
- artistRefFrom now populates SortName from dbq.Artist.SortName
- New artistRefFromCovered helper builds CoverURL from nullable cover_album_id
- New HomePayload view type for GET /api/home response body
This commit is contained in:
2026-05-01 17:56:19 -04:00
parent 302a15a209
commit 1abaf15051
2 changed files with 40 additions and 4 deletions
+18 -2
View File
@@ -75,16 +75,31 @@ func streamURL(trackID pgtype.UUID) string {
return "/api/tracks/" + uuidToString(trackID) + "/stream" return "/api/tracks/" + uuidToString(trackID) + "/stream"
} }
// artistRefFrom projects a dbq.Artist into an ArtistRef. albumCount must be // artistRefFrom projects a dbq.Artist into an ArtistRef without cover.
// pre-computed by the caller (one query per artist in lists). // albumCount must be pre-computed by the caller. Used by code paths that
// don't have a representative-album lookup at hand (artist detail, search,
// liked-artists list).
func artistRefFrom(a dbq.Artist, albumCount int) ArtistRef { func artistRefFrom(a dbq.Artist, albumCount int) ArtistRef {
return ArtistRef{ return ArtistRef{
ID: uuidToString(a.ID), ID: uuidToString(a.ID),
Name: a.Name, Name: a.Name,
SortName: a.SortName,
AlbumCount: albumCount, AlbumCount: albumCount,
} }
} }
// artistRefFromCovered projects a dbq.Artist into an ArtistRef with the
// derived CoverURL populated from a representative album id (nullable).
// Used by /api/home and /api/artists?sort=alpha which carry the lookup
// in the same query.
func artistRefFromCovered(a dbq.Artist, albumCount int, coverAlbumID pgtype.UUID) ArtistRef {
ref := artistRefFrom(a, albumCount)
if coverAlbumID.Valid {
ref.CoverURL = coverURL(coverAlbumID)
}
return ref
}
// albumRefFrom projects a dbq.Album into an AlbumRef. artistName must be // albumRefFrom projects a dbq.Album into an AlbumRef. artistName must be
// pre-resolved because Album only carries artist_id. trackCount and // pre-resolved because Album only carries artist_id. trackCount and
// durationSec may be 0 when the caller does not compute them. // durationSec may be 0 when the caller does not compute them.
@@ -92,6 +107,7 @@ func albumRefFrom(a dbq.Album, artistName string, trackCount, durationSec int) A
return AlbumRef{ return AlbumRef{
ID: uuidToString(a.ID), ID: uuidToString(a.ID),
Title: a.Title, Title: a.Title,
SortTitle: a.SortTitle,
ArtistID: uuidToString(a.ArtistID), ArtistID: uuidToString(a.ArtistID),
ArtistName: artistName, ArtistName: artistName,
Year: yearFromDate(a.ReleaseDate), Year: yearFromDate(a.ReleaseDate),
+22 -2
View File
@@ -36,18 +36,26 @@ type Page[T any] struct {
} }
// ArtistRef is the lightweight artist shape used in lists and search results. // ArtistRef is the lightweight artist shape used in lists and search results.
// SortName is exposed so the SPA's alphabetical-grid divider can group rows
// by the catalogue sort order ("The Beatles" → "B"). CoverURL is derived from
// a representative album (most-recent with cover_art_path); empty when the
// artist's discography has no cover art.
type ArtistRef struct { type ArtistRef struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
SortName string `json:"sort_name"`
AlbumCount int `json:"album_count"` AlbumCount int `json:"album_count"`
CoverURL string `json:"cover_url"`
} }
// AlbumRef is the lightweight album shape used in lists, artist details, // AlbumRef is the lightweight album shape used in lists, artist details,
// and search results. CoverURL points to /api/albums/{id}/cover which // and search results. SortTitle is exposed so the SPA's alphabetical-grid
// Plan 3 implements. // divider can group rows by sort order ("The Wall" → "W"). CoverURL points
// to /api/albums/{id}/cover.
type AlbumRef struct { type AlbumRef struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` Title string `json:"title"`
SortTitle string `json:"sort_title"`
ArtistID string `json:"artist_id"` ArtistID string `json:"artist_id"`
ArtistName string `json:"artist_name"` ArtistName string `json:"artist_name"`
Year int `json:"year,omitempty"` Year int `json:"year,omitempty"`
@@ -92,3 +100,15 @@ type SearchResponse struct {
Albums Page[AlbumRef] `json:"albums"` Albums Page[AlbumRef] `json:"albums"`
Tracks Page[TrackRef] `json:"tracks"` Tracks Page[TrackRef] `json:"tracks"`
} }
// HomePayload is the response body of GET /api/home. Field counts:
// 50 (recently_added) / 25 (rediscover_albums) / 25 (rediscover_artists)
// / 75 (most_played) / 25 (last_played). All slices are non-nil at JSON
// encode time so empty sections render as [] rather than null.
type HomePayload struct {
RecentlyAddedAlbums []AlbumRef `json:"recently_added_albums"`
RediscoverAlbums []AlbumRef `json:"rediscover_albums"`
RediscoverArtists []ArtistRef `json:"rediscover_artists"`
MostPlayedTracks []TrackRef `json:"most_played_tracks"`
LastPlayedArtists []ArtistRef `json:"last_played_artists"`
}