Files
minstrel/internal/playlists/system.go
T

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)
})
}