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"` } 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. Concatenated-without-separator inputs (e.g. // "ElectronicComplextroGlitch Hop" from broken tag-editor output) cannot // be split without a genre dictionary and stay as one opaque tag. 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 }