// Package playlists' system.go implements the system-generated mix // builder (M7 #352 slice 2). The cron loop and lazy fallback both call // BuildSystemPlaylists; the helpers in this file (pickSeedArtists, // pickRepresentativeTrack, tieBreakHash) are the pure parts. package playlists import ( "hash/fnv" "sort" "github.com/jackc/pgx/v5/pgtype" ) // seedArtistRow mirrors the sqlc-generated PickSeedArtistsRow shape. // Defined locally so unit tests don't need a real DB. type seedArtistRow struct { ArtistID pgtype.UUID Score int64 } // pickSeedArtistsFromRows projects sqlc rows into the seed list. The // SQL already orders by score DESC + artist_id and LIMIT 3, 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. func pickSeedArtistsFromRows(rows []seedArtistRow) []pgtype.UUID { out := make([]pgtype.UUID, 0, len(rows)) for _, r := range rows { out = append(out, r.ArtistID) } return out } // tieBreakHash returns a deterministic 64-bit hash of (track_id, date_str). // Used to break score ties in mix candidate ranking. Same inputs always // produce the same output; different inputs almost always differ. // // Uses FNV-1a 64-bit (stdlib hash/fnv) — fast, deterministic, no extra // dependencies. The exact hash isn't load-bearing; any deterministic // 64-bit hash works. func tieBreakHash(trackID pgtype.UUID, dateStr string) uint64 { h := fnv.New64a() if trackID.Valid { _, _ = h.Write(trackID.Bytes[:]) } _, _ = h.Write([]byte(dateStr)) return h.Sum64() } // rankedCandidate is a (track_id, score) pair used during in-memory // sorting before insert into playlist_tracks. T5 fills these from // recommendation.Candidate scores. type rankedCandidate struct { TrackID pgtype.UUID Score float64 } // stableSortByScoreThenHash sorts candidates in-place by score DESC, // breaking ties with tieBreakHash(track_id, dateStr). func stableSortByScoreThenHash(cands []rankedCandidate, dateStr string) { sort.SliceStable(cands, func(i, j int) bool { if cands[i].Score != cands[j].Score { return cands[i].Score > cands[j].Score } return tieBreakHash(cands[i].TrackID, dateStr) < tieBreakHash(cands[j].TrackID, dateStr) }) }