The same rewrite as d191a282, for four packages whose comments carried the
most history: tuning stories, quotes from the operator, single-install
measurements in library_scale.go, and 'used to' accounts of bugs. The
constraint each story protected stays as a present-tense reason (why tier 3
is reduced not removed, why the randomised arms take an order seed, why the
seed artist is eligible in Songs-like). Comments naming system_cron.go and
config.RecommendationConfig, neither of which holds what they claimed, now
name the scheduler and internal/recsettings.
Checked: non-comment tokens identical to HEAD; gofmt clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
70 lines
1.9 KiB
Go
70 lines
1.9 KiB
Go
package recommendation
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
)
|
|
|
|
type SessionVector struct {
|
|
Seed bool `json:"seed"`
|
|
Artists []string `json:"artists"`
|
|
Tags map[string]int `json:"tags"`
|
|
RecentTrackIDs []string `json:"recent_track_ids"`
|
|
// DeviceClass is the current request's device, set by the radio
|
|
// handler from the user's latest play; drives the device dimension of the
|
|
// context-affinity term. Empty (omitted) for the daily mixes and for the
|
|
// snapshot stored on play_events, so it never narrows those.
|
|
DeviceClass string `json:"device_class,omitempty"`
|
|
}
|
|
|
|
func BuildSessionVector(priorTracks []dbq.Track) SessionVector {
|
|
v := SessionVector{
|
|
Seed: len(priorTracks) < 3,
|
|
Artists: []string{},
|
|
Tags: map[string]int{},
|
|
RecentTrackIDs: []string{},
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, t := range priorTracks {
|
|
artistID := uuidString(t.ArtistID)
|
|
if !seen[artistID] {
|
|
seen[artistID] = true
|
|
v.Artists = append(v.Artists, artistID)
|
|
}
|
|
if t.Genre != nil && *t.Genre != "" {
|
|
for _, g := range splitGenres(*t.Genre) {
|
|
v.Tags[g]++
|
|
}
|
|
}
|
|
v.RecentTrackIDs = append(v.RecentTrackIDs, uuidString(t.ID))
|
|
}
|
|
return v
|
|
}
|
|
|
|
func uuidString(u pgtype.UUID) string {
|
|
if !u.Valid {
|
|
return ""
|
|
}
|
|
return u.String()
|
|
}
|
|
|
|
// splitGenres splits a track's denormalized genre string on the common
|
|
// multi-genre delimiters (`;`, `,`) used by various tag editors. Trims
|
|
// whitespace; drops empty fragments. Strings with no delimiter come back
|
|
// as a single-element slice.
|
|
func splitGenres(s string) []string {
|
|
parts := strings.FieldsFunc(s, func(r rune) bool {
|
|
return r == ';' || r == ','
|
|
})
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
if p = strings.TrimSpace(p); p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|