fix(server): unify discovery-mix producers + daily-rotate the deterministic ones
test-go / test (push) Failing after 19s
test-go / integration (push) Has been cancelled

The five discovery-mix producers (Deep Cuts, Rediscover, New for you,
On this day, First listens) were near-identical boilerplate that
differed only in (a) which SQL query they ran and (b) whether to
diversity-cap the result. Folded into one produceDiscoveryMix(spec)
factory + a per-mix discoveryMixSpec slice. The registry composes the
factory over the spec list so adding a new mix is one struct literal
+ a SQL query, never a new func.

Also fixes the user-reported bug that several mixes 'show the same
content from yesterday'. Audit of the SQL queries:

  - Deep Cuts:   ORDER BY md5(t.id::text || $2::text)   → day-keyed
  - On this day: ORDER BY w.c DESC, md5(...)              → day-keyed
  - Rediscover:  ORDER BY tier, c DESC, id                → invariant
  - New for you: ORDER BY al.created_at DESC, disc, track → invariant
  - First listens: ORDER BY tier, al.id, disc, track      → invariant

The three invariant ones produced identical content day-over-day. The
unified spec carries a dailyRotate bool: when set, the producer
applies a daily-deterministic offset rotate-left of the candidate
pool BEFORE diversify+truncate. Rotation (not shuffle) preserves
contiguous-block ordering inside each day's slice — matters for First
listens which is album-coherent.

Set on Rediscover + First listens (where same-content-every-day is
clearly a bug). Left off New for you because 'newest album first
regardless of day' is the intended UX for that surface — daily
rotation there would feel wrong.

Daily rotation seed: rand.New(NewSource(int64(userIDHash(userID,
dateStr)))) — same primitive used by For-You's pickHeadAndTail
sampling so behavior is consistent across the system playlist family.

No test file referenced the deleted produceXxx functions directly,
only the registry, so this is a closed refactor.
This commit is contained in:
2026-06-03 13:18:17 -04:00
parent 3df5e5cb3c
commit 7473e98d91
2 changed files with 188 additions and 108 deletions
+17 -10
View File
@@ -278,16 +278,23 @@ func RefreshableSystemKind(key string) bool {
// here (plus its candidate query). Order is the materialize order;
// it has no functional effect (atomic replace + per-playlist
// collage are order-independent).
var systemPlaylistRegistry = []systemPlaylistKind{
{Key: "for_you", Singleton: true, Produce: produceForYou},
{Key: "songs_like_artist", Singleton: false, Produce: produceSeedMixes},
{Key: "discover", Singleton: true, Produce: produceDiscover},
{Key: "deep_cuts", Singleton: true, Produce: produceDeepCuts},
{Key: "rediscover", Singleton: true, Produce: produceRediscover},
{Key: "new_for_you", Singleton: true, Produce: produceNewForYou},
{Key: "on_this_day", Singleton: true, Produce: produceOnThisDay},
{Key: "first_listens", Singleton: true, Produce: produceFirstListens},
}
var systemPlaylistRegistry = func() []systemPlaylistKind {
out := []systemPlaylistKind{
{Key: "for_you", Singleton: true, Produce: produceForYou},
{Key: "songs_like_artist", Singleton: false, Produce: produceSeedMixes},
{Key: "discover", Singleton: true, Produce: produceDiscover},
}
// The five discovery mixes share one Produce closure factory keyed
// by a per-mix spec; spec list + factory live in system_mixes.go.
for _, spec := range discoveryMixSpecs {
out = append(out, systemPlaylistKind{
Key: spec.variant,
Singleton: true,
Produce: produceDiscoveryMix(spec),
})
}
return out
}()
// systemForYouSourceLimits is a deeper candidate pool than the radio
// default. On a self-hosted library without ListenBrainz similarity