68 lines
2.2 KiB
Go
68 lines
2.2 KiB
Go
// 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)
|
|
})
|
|
}
|