feat(home): Songs-like → dedicated row + wider spread (#1491)
test-go / test (push) Successful in 33s
test-web / test (push) Successful in 43s
android / Build + lint + test (push) Failing after 1m29s
test-go / integration (push) Successful in 4m42s

Promote the best-performing surface ("Songs like {artist}", ~8% skip /
~86% completion) out of the shared Playlists carousel into its own Home
row on both Android and web, and widen the daily build from 3 to 6 mixes
so the dedicated row shows a wider spread.

Server (internal/playlists):
- PickSeedArtists candidate pool 5 → 12; pickSeedArtistsForDay now takes
  songsLikeSeedCount (6) instead of a hardcoded 3. Graceful degradation
  and daily rotation preserved.

Android (HomeScreen.kt):
- New songsLikeSection + buildSongsLikeRow; PlaylistsRow takes a title so
  it renders both the "Playlists" and "Songs like…" rows. buildOnlineRow
  / orderedRealPlaylists no longer reserve the 3 songs-like slots.
  Offline shows cached mixes (available-first), hides the row when none.

Web (+page.svelte):
- Dedicated "Songs like…" row from songsLikeRow; dropped the 3-slot cap
  and removed songs-like from the Playlists carousel.

Tests: seed_selection_test.go, BuildPlaylistsRowTest.kt, page.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 20:41:08 -04:00
parent cb0af5efd3
commit c1d143cf4a
8 changed files with 288 additions and 87 deletions
+17 -7
View File
@@ -35,7 +35,7 @@ type seedArtistRow struct {
}
// pickSeedArtistsFromRows projects sqlc rows into the seed list. The
// SQL already orders by score DESC + artist_id and LIMIT 3, so this is
// SQL already orders by score DESC + artist_id and LIMIT 12, so this is
// just a column projection — but pulling it into a function keeps the
// call-site readable and makes the post-fetch path testable without
// a database.
@@ -110,10 +110,20 @@ func pickDailySeeds(pool []pgtype.UUID, userID pgtype.UUID, dateStr string, n in
return shuffled[:n]
}
// pickSeedArtistsForDay picks up to 3 seed artists for the Songs-like
// mixes; For-You uses pickDailySeeds directly with forYouSeedCount.
// songsLikeSeedCount is how many "Songs like {artist}" mixes to build
// each day (#1491). Bumped from 3 when Songs-like was promoted to its
// own dedicated Home row — the best-performing surface (low skip / high
// completion) was buried as 3 tiles in the shared Playlists carousel, so
// the wider row now shows a wider spread. Drawn from PickSeedArtists'
// deeper top-12 pool via a daily-deterministic shuffle so the set still
// rotates day to day rather than pinning the same six artists.
const songsLikeSeedCount = 6
// pickSeedArtistsForDay picks up to songsLikeSeedCount seed artists for
// the Songs-like mixes; For-You uses pickDailySeeds directly with
// forYouSeedCount.
func pickSeedArtistsForDay(pool []pgtype.UUID, userID pgtype.UUID, dateStr string) []pgtype.UUID {
return pickDailySeeds(pool, userID, dateStr, 3)
return pickDailySeeds(pool, userID, dateStr, songsLikeSeedCount)
}
// rankedCandidate is a (track_id, score) pair used during in-memory
@@ -589,8 +599,8 @@ func produceForYou(
return []builtPlaylist{{Name: "For You", Variant: "for_you", Tracks: tracks}}, nil
}
// produceSeedMixes: up to 3 "Songs like {artist}" mixes. Seed
// artists rotate daily-deterministically; the seed query falls back
// produceSeedMixes: up to songsLikeSeedCount "Songs like {artist}"
// mixes. Seed artists rotate daily-deterministically; the seed query falls back
// through widening engagement windows (#1255) and every returned row
// shares the winning tier, stamped onto the built tracks as their
// pick_kind. The base seed-artist query failing is fatal; per-artist
@@ -685,7 +695,7 @@ func produceDiscover(
}
// BuildSystemPlaylists builds the user's daily system mixes (one For-You +
// up to 3 Songs-like-{seed} mixes). Atomic-replace inside one tx;
// up to songsLikeSeedCount Songs-like-{seed} mixes). Atomic-replace inside one tx;
// concurrency-guarded via system_playlist_runs.in_flight; deterministic
// within a day via tieBreakHash(track_id, now.UTC().Format("2006-01-02")).
//