feat(server): "You might like" album/artist Home rows (#790)
Surface in-library albums/artists the listener doesn't actively spin but is predicted to enjoy, derived from the same similarity + like-weighted candidate engine that powers For-You — rolled up from track scores to album/artist granularity. Built in the daily 3am BuildSystemPlaylists pass, atomic-replaced alongside the system playlists, and read back by /api/home (+ /api/home/index). Cold-start gate: skips generation entirely below 20 distinct unskipped tracks AND 5 distinct artists, so a thin profile ships empty rows rather than near-random tiles. - migration 0034: you_might_like_albums / you_might_like_artists (id+rank, CASCADE, per-user rank index). - playlists/you_might_like.go: cold-start gate + similarity roll-up (sum-of-top-3 aggregation, per-artist album cap, daily-rotating via the same userIDHash jitter as For-You) + atomic-replace persist in the tx. - recommendation/home.go: two new HomePayload sections with read-time cross-section dedup vs Most Played / Rediscover / Last Played, trimmed to 10 each. - api: you_might_like_albums / you_might_like_artists on /api/home and /api/home/index, reusing albumRefFrom / artistRefFromCovered. - tests: pure roll-up/aggregation/cap unit tests + DB-backed gate, sufficiency, and atomic-replace tests (all green vs real Postgres). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,8 @@ func (h *handlers) handleGetHome(w http.ResponseWriter, r *http.Request) {
|
||||
RediscoverArtists: make([]ArtistRef, 0, len(data.RediscoverArtists)),
|
||||
MostPlayedTracks: make([]TrackRef, 0, len(data.MostPlayedTracks)),
|
||||
LastPlayedArtists: make([]ArtistRef, 0, len(data.LastPlayedArtists)),
|
||||
YouMightLikeAlbums: make([]AlbumRef, 0, len(data.YouMightLikeAlbums)),
|
||||
YouMightLikeArtists: make([]ArtistRef, 0, len(data.YouMightLikeArtists)),
|
||||
}
|
||||
for _, row := range data.RecentlyAddedAlbums {
|
||||
out.RecentlyAddedAlbums = append(out.RecentlyAddedAlbums, albumRefFrom(row.Album, row.ArtistName, 0, 0))
|
||||
@@ -46,6 +48,12 @@ func (h *handlers) handleGetHome(w http.ResponseWriter, r *http.Request) {
|
||||
for _, row := range data.LastPlayedArtists {
|
||||
out.LastPlayedArtists = append(out.LastPlayedArtists, artistRefFromCovered(row.Artist, int(row.AlbumCount), row.CoverAlbumID))
|
||||
}
|
||||
for _, row := range data.YouMightLikeAlbums {
|
||||
out.YouMightLikeAlbums = append(out.YouMightLikeAlbums, albumRefFrom(row.Album, row.ArtistName, 0, 0))
|
||||
}
|
||||
for _, row := range data.YouMightLikeArtists {
|
||||
out.YouMightLikeArtists = append(out.YouMightLikeArtists, artistRefFromCovered(row.Artist, int(row.AlbumCount), row.CoverAlbumID))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
@@ -78,6 +86,8 @@ func (h *handlers) handleGetHomeIndex(w http.ResponseWriter, r *http.Request) {
|
||||
RediscoverArtists: make([]string, 0, len(data.RediscoverArtists)),
|
||||
MostPlayedTracks: make([]string, 0, len(data.MostPlayedTracks)),
|
||||
LastPlayedArtists: make([]string, 0, len(data.LastPlayedArtists)),
|
||||
YouMightLikeAlbums: make([]string, 0, len(data.YouMightLikeAlbums)),
|
||||
YouMightLikeArtists: make([]string, 0, len(data.YouMightLikeArtists)),
|
||||
}
|
||||
for _, row := range data.RecentlyAddedAlbums {
|
||||
out.RecentlyAddedAlbums = append(out.RecentlyAddedAlbums, uuidToString(row.Album.ID))
|
||||
@@ -94,6 +104,12 @@ func (h *handlers) handleGetHomeIndex(w http.ResponseWriter, r *http.Request) {
|
||||
for _, row := range data.LastPlayedArtists {
|
||||
out.LastPlayedArtists = append(out.LastPlayedArtists, uuidToString(row.Artist.ID))
|
||||
}
|
||||
for _, row := range data.YouMightLikeAlbums {
|
||||
out.YouMightLikeAlbums = append(out.YouMightLikeAlbums, uuidToString(row.Album.ID))
|
||||
}
|
||||
for _, row := range data.YouMightLikeArtists {
|
||||
out.YouMightLikeArtists = append(out.YouMightLikeArtists, uuidToString(row.Artist.ID))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,11 @@ type HomePayload struct {
|
||||
RediscoverArtists []ArtistRef `json:"rediscover_artists"`
|
||||
MostPlayedTracks []TrackRef `json:"most_played_tracks"`
|
||||
LastPlayedArtists []ArtistRef `json:"last_played_artists"`
|
||||
// You-might-like: in-library albums/artists predicted from the user's
|
||||
// listening that they don't actively spin. Built daily; gated on a
|
||||
// minimum listening history so a thin profile gets empty rows.
|
||||
YouMightLikeAlbums []AlbumRef `json:"you_might_like_albums"`
|
||||
YouMightLikeArtists []ArtistRef `json:"you_might_like_artists"`
|
||||
}
|
||||
|
||||
// HomeIndexPayload is the response body of GET /api/home/index — the
|
||||
@@ -134,6 +139,8 @@ type HomeIndexPayload struct {
|
||||
RediscoverArtists []string `json:"rediscover_artists"`
|
||||
MostPlayedTracks []string `json:"most_played_tracks"`
|
||||
LastPlayedArtists []string `json:"last_played_artists"`
|
||||
YouMightLikeAlbums []string `json:"you_might_like_albums"`
|
||||
YouMightLikeArtists []string `json:"you_might_like_artists"`
|
||||
}
|
||||
|
||||
// HistoryEvent is one play in a user's listening history. Used by
|
||||
|
||||
Reference in New Issue
Block a user