From 1abaf150517d0aa7fbb2190656c5165cc5bae488 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 1 May 2026 17:56:19 -0400 Subject: [PATCH] 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 --- internal/api/convert.go | 20 ++++++++++++++++++-- internal/api/types.go | 24 ++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/internal/api/convert.go b/internal/api/convert.go index 89421420..88511003 100644 --- a/internal/api/convert.go +++ b/internal/api/convert.go @@ -75,16 +75,31 @@ func streamURL(trackID pgtype.UUID) string { return "/api/tracks/" + uuidToString(trackID) + "/stream" } -// artistRefFrom projects a dbq.Artist into an ArtistRef. albumCount must be -// pre-computed by the caller (one query per artist in lists). +// artistRefFrom projects a dbq.Artist into an ArtistRef without cover. +// 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 { return ArtistRef{ ID: uuidToString(a.ID), Name: a.Name, + SortName: a.SortName, 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 // pre-resolved because Album only carries artist_id. trackCount and // 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{ ID: uuidToString(a.ID), Title: a.Title, + SortTitle: a.SortTitle, ArtistID: uuidToString(a.ArtistID), ArtistName: artistName, Year: yearFromDate(a.ReleaseDate), diff --git a/internal/api/types.go b/internal/api/types.go index 44444203..3e4e4f4a 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -36,18 +36,26 @@ type Page[T any] struct { } // 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 { ID string `json:"id"` Name string `json:"name"` + SortName string `json:"sort_name"` AlbumCount int `json:"album_count"` + CoverURL string `json:"cover_url"` } // AlbumRef is the lightweight album shape used in lists, artist details, -// and search results. CoverURL points to /api/albums/{id}/cover which -// Plan 3 implements. +// and search results. SortTitle is exposed so the SPA's alphabetical-grid +// divider can group rows by sort order ("The Wall" → "W"). CoverURL points +// to /api/albums/{id}/cover. type AlbumRef struct { ID string `json:"id"` Title string `json:"title"` + SortTitle string `json:"sort_title"` ArtistID string `json:"artist_id"` ArtistName string `json:"artist_name"` Year int `json:"year,omitempty"` @@ -92,3 +100,15 @@ type SearchResponse struct { Albums Page[AlbumRef] `json:"albums"` 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"` +}