This commit was merged in pull request #95.
This commit is contained in:
@@ -180,9 +180,93 @@ func HomeData(ctx context.Context, pool *pgxpool.Pool, userID pgtype.UUID) (*Hom
|
||||
out.YouMightLikeAlbums, out.MostPlayedTracks, out.RediscoverAlbums)
|
||||
out.YouMightLikeArtists = applyYouMightLikeArtistFilters(
|
||||
out.YouMightLikeArtists, out.MostPlayedTracks, out.RediscoverArtists, out.LastPlayedArtists)
|
||||
// The taste roll-up surfaces top-similar entities, which for a heavy
|
||||
// listener are mostly ones they already play — so the dedup above can
|
||||
// leave the section thin (often a single artist). Top up from the much
|
||||
// larger pool of liked entities, applying the same exclusions so the
|
||||
// fallback neither duplicates a tile nor suggests an actively-played one.
|
||||
if len(out.YouMightLikeArtists) < HomeYouMightLikeLimit {
|
||||
out.YouMightLikeArtists = topUpYouMightLikeArtists(ctx, q, userID, out)
|
||||
}
|
||||
if len(out.YouMightLikeAlbums) < HomeYouMightLikeLimit {
|
||||
out.YouMightLikeAlbums = topUpYouMightLikeAlbums(ctx, q, userID, out)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// youMightLikeFallbackFetch is the liked-entity pool the fallback draws from.
|
||||
// Generous so the dedup still leaves enough to fill the row.
|
||||
const youMightLikeFallbackFetch = 100
|
||||
|
||||
// topUpYouMightLikeArtists fills a thin You-might-like artist row from the
|
||||
// user's liked artists (daily-stable sample), excluding what's already shown
|
||||
// on Home (the section itself, Rediscover, Most/Last Played) so the fallback
|
||||
// neither duplicates a tile nor suggests an actively-played artist. The liked
|
||||
// pool dwarfs the similarity roll-up, so the same exclusions still fill it.
|
||||
// Best-effort: a query error leaves the section as-is.
|
||||
func topUpYouMightLikeArtists(
|
||||
ctx context.Context, q *dbq.Queries, userID pgtype.UUID, out *HomePayload,
|
||||
) []dbq.ListYouMightLikeArtistsForUserRow {
|
||||
excluded := mostPlayedArtistIDs(out.MostPlayedTracks)
|
||||
for _, r := range out.RediscoverArtists {
|
||||
excluded[r.Artist.ID] = struct{}{}
|
||||
}
|
||||
for _, r := range out.LastPlayedArtists {
|
||||
excluded[r.Artist.ID] = struct{}{}
|
||||
}
|
||||
result := out.YouMightLikeArtists
|
||||
for _, r := range result {
|
||||
excluded[r.Artist.ID] = struct{}{}
|
||||
}
|
||||
fb, err := q.ListRediscoverArtistsFallbackForUser(ctx, dbq.ListRediscoverArtistsFallbackForUserParams{
|
||||
UserID: userID, Limit: youMightLikeFallbackFetch,
|
||||
})
|
||||
if err != nil {
|
||||
return result
|
||||
}
|
||||
for _, r := range fb {
|
||||
if _, dup := excluded[r.Artist.ID]; dup {
|
||||
continue
|
||||
}
|
||||
result = append(result, dbq.ListYouMightLikeArtistsForUserRow(r))
|
||||
if len(result) >= HomeYouMightLikeLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// topUpYouMightLikeAlbums is the album-row counterpart to
|
||||
// topUpYouMightLikeArtists, drawing from the user's liked albums.
|
||||
func topUpYouMightLikeAlbums(
|
||||
ctx context.Context, q *dbq.Queries, userID pgtype.UUID, out *HomePayload,
|
||||
) []dbq.ListYouMightLikeAlbumsForUserRow {
|
||||
excluded := mostPlayedAlbumIDs(out.MostPlayedTracks)
|
||||
for _, r := range out.RediscoverAlbums {
|
||||
excluded[r.Album.ID] = struct{}{}
|
||||
}
|
||||
result := out.YouMightLikeAlbums
|
||||
for _, r := range result {
|
||||
excluded[r.Album.ID] = struct{}{}
|
||||
}
|
||||
fb, err := q.ListRediscoverAlbumsFallbackForUser(ctx, dbq.ListRediscoverAlbumsFallbackForUserParams{
|
||||
UserID: userID, Limit: youMightLikeFallbackFetch,
|
||||
})
|
||||
if err != nil {
|
||||
return result
|
||||
}
|
||||
for _, r := range fb {
|
||||
if _, dup := excluded[r.Album.ID]; dup {
|
||||
continue
|
||||
}
|
||||
result = append(result, dbq.ListYouMightLikeAlbumsForUserRow(r))
|
||||
if len(result) >= HomeYouMightLikeLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// applyYouMightLikeAlbumFilters drops albums the user actively plays
|
||||
// (Most Played) or that already appear in Rediscover, then trims to
|
||||
// HomeYouMightLikeLimit. Build-time rank order is preserved.
|
||||
|
||||
@@ -2,6 +2,7 @@ package recommendation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -10,6 +11,43 @@ import (
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
)
|
||||
|
||||
// TestHomeData_YouMightLike_FallbackFillsFromLikedArtists: when the taste
|
||||
// roll-up is empty/thin (no you_might_like_artists rows here), the section
|
||||
// fills from the user's liked artists. Rediscover takes its share first; the
|
||||
// You-might-like fallback fills the rest disjointly so no artist is shown
|
||||
// twice. 20 liked artists, none played → Rediscover 10 + You-might-like 10.
|
||||
func TestHomeData_YouMightLike_FallbackFillsFromLikedArtists(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
user := seedUser(t, pool, "home-yml-fallback")
|
||||
liked := make(map[pgtype.UUID]bool, 20)
|
||||
for i := 0; i < 20; i++ {
|
||||
a := seedArtist(t, pool, fmt.Sprintf("YmlArtist%02d", i), "")
|
||||
likeArtist(t, pool, user.ID, a.ID)
|
||||
liked[a.ID] = true
|
||||
}
|
||||
|
||||
got, err := HomeData(context.Background(), pool, user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("HomeData: %v", err)
|
||||
}
|
||||
if len(got.YouMightLikeArtists) != HomeYouMightLikeLimit {
|
||||
t.Fatalf("you-might-like artists = %d, want %d (filled by fallback)",
|
||||
len(got.YouMightLikeArtists), HomeYouMightLikeLimit)
|
||||
}
|
||||
inRediscover := map[pgtype.UUID]bool{}
|
||||
for _, r := range got.RediscoverArtists {
|
||||
inRediscover[r.Artist.ID] = true
|
||||
}
|
||||
for _, r := range got.YouMightLikeArtists {
|
||||
if !liked[r.Artist.ID] {
|
||||
t.Errorf("you-might-like artist %s not from the liked pool", r.Artist.Name)
|
||||
}
|
||||
if inRediscover[r.Artist.ID] {
|
||||
t.Errorf("artist %s shown in both Rediscover and You-might-like", r.Artist.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomeData_NewUser_AllSectionsEmpty(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
user := seedUser(t, pool, "home-newuser")
|
||||
|
||||
Reference in New Issue
Block a user