feat(server/m7-352): system mix helpers (tieBreakHash, pickSeedArtists)
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user