package playlists import ( "context" "log/slog" "time" "github.com/jackc/pgx/v5/pgtype" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) // Discovery mixes (#419-423). Each is one candidate query + a thin // producer registered in systemPlaylistRegistry. They follow the // Discover model: a SQL query returns ordered (id, album_id, // artist_id) rows; we optionally diversity-cap, truncate, and emit // rankedCandidate (Score unused — SQL gave the ranking). All are // singleton kinds, so the generic by-kind refresh/shuffle endpoints // and per-tile refresh affordance work with zero client changes. // discoveryMixLen caps each mix at the same depth as For-You / // Discover so shuffle-on-play has a varied pool within a day. const discoveryMixLen = 100 // finishMix caps (per-album<=2 / per-artist<=3) when diversify is // set, truncates to discoveryMixLen, and converts to the insert // type. Album-coherent mixes (New for you, First Listens) pass // diversify=false so whole albums survive. func finishMix(rows []discoverTrack, diversify bool) []rankedCandidate { pool := rows if diversify { pool = capByAlbumAndArtist(pool) } if len(pool) > discoveryMixLen { pool = pool[:discoveryMixLen] } if len(pool) == 0 { return nil } tracks := make([]rankedCandidate, len(pool)) for i, t := range pool { tracks[i] = rankedCandidate{TrackID: t.ID} } return tracks } // emit wraps the finished track list in a single builtPlaylist (the // discovery mixes are all singletons). nil tracks → no playlist. func emit(name, variant string, tracks []rankedCandidate) []builtPlaylist { if len(tracks) == 0 { return nil } return []builtPlaylist{{Name: name, Variant: variant, Tracks: tracks}} } func produceDeepCuts( ctx context.Context, q *dbq.Queries, logger *slog.Logger, userID pgtype.UUID, dateStr string, _ time.Time, ) ([]builtPlaylist, error) { rows, err := q.ListDeepCutsTracks(ctx, dbq.ListDeepCutsTracksParams{ UserID: userID, Column2: dateStr, }) if err != nil { logger.Warn("system playlist: deep-cuts query failed; skipping", "user_id", uuidStringPL(userID), "err", err) return nil, nil } dt := make([]discoverTrack, len(rows)) for i, r := range rows { dt[i] = discoverTrack{ID: r.ID, AlbumID: r.AlbumID, ArtistID: r.ArtistID} } return emit("Deep Cuts", "deep_cuts", finishMix(dt, true)), nil } func produceRediscover( ctx context.Context, q *dbq.Queries, logger *slog.Logger, userID pgtype.UUID, _ string, _ time.Time, ) ([]builtPlaylist, error) { rows, err := q.ListRediscoverTracks(ctx, userID) if err != nil { logger.Warn("system playlist: rediscover query failed; skipping", "user_id", uuidStringPL(userID), "err", err) return nil, nil } dt := make([]discoverTrack, len(rows)) for i, r := range rows { dt[i] = discoverTrack{ID: r.ID, AlbumID: r.AlbumID, ArtistID: r.ArtistID} } return emit("Rediscover", "rediscover", finishMix(dt, true)), nil } func produceNewForYou( ctx context.Context, q *dbq.Queries, logger *slog.Logger, userID pgtype.UUID, _ string, _ time.Time, ) ([]builtPlaylist, error) { rows, err := q.ListNewForYouTracks(ctx, userID) if err != nil { logger.Warn("system playlist: new-for-you query failed; skipping", "user_id", uuidStringPL(userID), "err", err) return nil, nil } dt := make([]discoverTrack, len(rows)) for i, r := range rows { dt[i] = discoverTrack{ID: r.ID, AlbumID: r.AlbumID, ArtistID: r.ArtistID} } // Album-coherent: no diversity cap so whole new albums survive. return emit("New for you", "new_for_you", finishMix(dt, false)), nil } func produceOnThisDay( ctx context.Context, q *dbq.Queries, logger *slog.Logger, userID pgtype.UUID, dateStr string, _ time.Time, ) ([]builtPlaylist, error) { rows, err := q.ListOnThisDayTracks(ctx, dbq.ListOnThisDayTracksParams{ UserID: userID, Column2: dateStr, }) if err != nil { logger.Warn("system playlist: on-this-day query failed; skipping", "user_id", uuidStringPL(userID), "err", err) return nil, nil } dt := make([]discoverTrack, len(rows)) for i, r := range rows { dt[i] = discoverTrack{ID: r.ID, AlbumID: r.AlbumID, ArtistID: r.ArtistID} } return emit("On this day", "on_this_day", finishMix(dt, true)), nil } func produceFirstListens( ctx context.Context, q *dbq.Queries, logger *slog.Logger, userID pgtype.UUID, _ string, _ time.Time, ) ([]builtPlaylist, error) { rows, err := q.ListFirstListensTracks(ctx, userID) if err != nil { logger.Warn("system playlist: first-listens query failed; skipping", "user_id", uuidStringPL(userID), "err", err) return nil, nil } dt := make([]discoverTrack, len(rows)) for i, r := range rows { dt[i] = discoverTrack{ID: r.ID, AlbumID: r.AlbumID, ArtistID: r.ArtistID} } // Album-coherent (tiered by liked/played artist in SQL): no cap. return emit("First listens", "first_listens", finishMix(dt, false)), nil }