// Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.31.1 // source: genre_mixes.sql package dbq import ( "context" "github.com/jackc/pgx/v5/pgtype" ) const listGenreMixCandidates = `-- name: ListGenreMixCandidates :many WITH library_genres AS ( SELECT lower(trim(g.g)) AS genre_key, min(trim(g.g))::text AS label, COUNT(DISTINCT t.id) AS track_count, array_agg(DISTINCT t.artist_id)::uuid[] AS artist_ids FROM tracks t CROSS JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g(g) WHERE t.missing_since IS NULL AND trim(g.g) <> '' GROUP BY lower(trim(g.g)) HAVING COUNT(DISTINCT t.id) >= 25 AND COUNT(DISTINCT t.artist_id) >= 3 ), taste AS ( SELECT lower(trim(tpt.tag)) AS genre_key, SUM(tpt.weight) AS weight FROM taste_profile_tags tpt WHERE tpt.user_id = $1 GROUP BY lower(trim(tpt.tag)) HAVING SUM(tpt.weight) > 0 ), listened AS ( SELECT t.id FROM play_events pe JOIN tracks t ON t.id = pe.track_id WHERE pe.user_id = $1 AND pe.was_skipped = false UNION ALL SELECT gl.track_id FROM general_likes gl WHERE gl.user_id = $1 ), history AS ( SELECT lower(trim(g.g)) AS genre_key, COUNT(*) AS plays FROM listened l JOIN tracks t ON t.id = l.id CROSS JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g(g) WHERE trim(g.g) <> '' GROUP BY lower(trim(g.g)) ) SELECT lg.genre_key::text AS genre_key, lg.label AS label, (CASE WHEN ta.genre_key IS NOT NULL THEN 1 WHEN h.genre_key IS NOT NULL THEN 2 ELSE 3 END)::int AS tier, (CASE WHEN ta.genre_key IS NOT NULL THEN ta.weight WHEN h.genre_key IS NOT NULL THEN h.plays::float8 ELSE lg.track_count::float8 END)::float8 AS score, lg.artist_ids FROM library_genres lg LEFT JOIN taste ta ON ta.genre_key = lg.genre_key LEFT JOIN history h ON h.genre_key = lg.genre_key ORDER BY tier, score DESC, lg.genre_key LIMIT 60 ` type ListGenreMixCandidatesRow struct { GenreKey string Label string Tier int32 Score float64 ArtistIds []pgtype.UUID } // Candidate queries for the genre mixes: one daily system playlist per genre // the user listens to. Genres live inline on tracks.genre as a delimited // string and are split on [;,] the same way the Discover and radio tag arms // split them. Matching is case-insensitive, so the ID3 "Post-Punk" and the // folksonomy "post-punk" in the taste profile count as one genre. // Library genres big enough to carry a mix (>= 25 playable tracks across // >= 3 artists), each tagged with how close it is to the user's listening: // // tier 1: a positive taste-profile genre, scored by its taste weight // tier 2: a genre of tracks the user has played through or liked, scored // by how many — catches a profile that has not been built yet or // has decayed past its window // tier 3: any other library genre, scored by its size // // artist_ids lets the caller skip a genre whose artists mostly repeat one // already picked (Indie Rock after Alternative Rock). // $1 user_id. func (q *Queries) ListGenreMixCandidates(ctx context.Context, userID pgtype.UUID) ([]ListGenreMixCandidatesRow, error) { rows, err := q.db.Query(ctx, listGenreMixCandidates, userID) if err != nil { return nil, err } defer rows.Close() var items []ListGenreMixCandidatesRow for rows.Next() { var i ListGenreMixCandidatesRow if err := rows.Scan( &i.GenreKey, &i.Label, &i.Tier, &i.Score, &i.ArtistIds, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listGenreMixTracks = `-- name: ListGenreMixTracks :many WITH play_counts AS ( SELECT pe.track_id, COUNT(*) AS c FROM play_events pe WHERE pe.user_id = $1 AND pe.was_skipped = false GROUP BY pe.track_id ), skip_counts AS ( SELECT pe.track_id, COUNT(*) AS c FROM play_events pe WHERE pe.user_id = $1 AND pe.was_skipped = true GROUP BY pe.track_id ), artist_plays AS ( SELECT t.artist_id, COUNT(*) FILTER (WHERE NOT pe.was_skipped) AS plays, COUNT(*) FILTER (WHERE pe.was_skipped) AS skips FROM play_events pe JOIN tracks t ON t.id = pe.track_id WHERE pe.user_id = $1 GROUP BY t.artist_id ), artist_likes AS ( SELECT t.artist_id, COUNT(*) AS likes FROM general_likes gl JOIN tracks t ON t.id = gl.track_id WHERE gl.user_id = $1 GROUP BY t.artist_id ), own_artists AS ( SELECT ap.artist_id FROM artist_plays ap WHERE ap.plays > 0 UNION SELECT al.artist_id FROM artist_likes al ), artist_sim AS ( SELECT asim.artist_b_id AS artist_id, MAX(LEAST(GREATEST(asim.score, 0), 1)) AS sim FROM artist_similarity asim JOIN own_artists oa ON oa.artist_id = asim.artist_a_id GROUP BY asim.artist_b_id ), eligible AS ( SELECT t.id, t.album_id, t.artist_id, (COALESCE(pc.c, 0) > 0 OR gl.track_id IS NOT NULL) AS familiar, CASE WHEN COALESCE(pc.c, 0) > 0 OR gl.track_id IS NOT NULL THEN (1 + ln(1 + COALESCE(pc.c, 0)::float8) + CASE WHEN gl.track_id IS NOT NULL THEN 1 ELSE 0 END) * ((COALESCE(pc.c, 0) + 1)::float8 / (COALESCE(pc.c, 0) + COALESCE(sc.c, 0) + 1)) ELSE (1 + ln(1 + COALESCE(ap.plays, 0)::float8 + 2 * COALESCE(al.likes, 0)::float8) + 2 * COALESCE(asm.sim, 0)) * ((COALESCE(ap.plays, 0) + 1)::float8 / (COALESCE(ap.plays, 0) + COALESCE(ap.skips, 0) + 1)) END::float8 AS weight, -- u in (0, 1): the first 32 bits of md5(track, date), shifted off 0. ((('x' || substr(md5(t.id::text || $2::text), 1, 8))::bit(32)::bigint)::float8 + 1) / 4294967297.0 AS u FROM tracks t LEFT JOIN play_counts pc ON pc.track_id = t.id LEFT JOIN skip_counts sc ON sc.track_id = t.id LEFT JOIN general_likes gl ON gl.user_id = $1 AND gl.track_id = t.id LEFT JOIN artist_plays ap ON ap.artist_id = t.artist_id LEFT JOIN artist_likes al ON al.artist_id = t.artist_id LEFT JOIN artist_sim asm ON asm.artist_id = t.artist_id WHERE t.missing_since IS NULL -- never offer a file that is gone AND NOT (COALESCE(sc.c, 0) >= 2 AND COALESCE(sc.c, 0) > COALESCE(pc.c, 0)) AND EXISTS ( SELECT 1 FROM regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g(g) WHERE lower(trim(g.g)) = $3::text ) AND NOT EXISTS ( SELECT 1 FROM quarantine q WHERE q.user_id = $1 AND q.track_id = t.id ) ), ranked AS ( SELECT e.id, e.album_id, e.artist_id, e.familiar, -ln(e.u) / e.weight AS draw, row_number() OVER (PARTITION BY e.familiar ORDER BY -ln(e.u) / e.weight, e.id) AS rn FROM eligible e ) SELECT r.id, r.album_id, r.artist_id, r.familiar::bool AS familiar FROM ranked r WHERE r.rn <= 200 ORDER BY r.familiar DESC, r.draw, r.id ` type ListGenreMixTracksParams struct { UserID pgtype.UUID DateSeed string GenreKey string } type ListGenreMixTracksRow struct { ID pgtype.UUID AlbumID pgtype.UUID ArtistID pgtype.UUID Familiar bool } // Playable tracks carrying one genre, split into the two halves a genre mix // blends: familiar (the user has played it through or liked it) and unheard. // Up to 200 of each, drawn by a weighted lottery seeded with the date, so the // mix changes day to day, holds still within a day, and leans toward what the // user likes without becoming the same 50 songs every day. // // The lottery is the Efraimidis-Spirakis draw: each track gets // -ln(u) / weight with u uniform in (0, 1) from md5(track, date), and the // smallest keys win. A track of weight 3 is three times as likely as one of // weight 1 to come before it, and every track keeps some chance. // // Familiar weight: 1 + ln(1 + plays through) + 1 if liked, times the share of // its plays that were not skips. A song played 40 times weighs about 4.7, one // played once about 1.7. // // Unheard weight: 1 + ln(1 + the artist's plays through + 2 × its liked // tracks) + 2 × how similar the artist is to the user's own artists (the best // artist_similarity score from any artist they have played through or liked, // 0 to 1), times the share of the artist's plays that were not skips. An // artist the user has never met weighs 1, so new artists still come up; an // artist they keep skipping weighs well under 1. // // A track the user keeps skipping stays out: twice-skipped and skipped more // often than played through. The second half of that test keeps a favourite // with a couple of stray skips in. func (q *Queries) ListGenreMixTracks(ctx context.Context, arg ListGenreMixTracksParams) ([]ListGenreMixTracksRow, error) { rows, err := q.db.Query(ctx, listGenreMixTracks, arg.UserID, arg.DateSeed, arg.GenreKey) if err != nil { return nil, err } defer rows.Close() var items []ListGenreMixTracksRow for rows.Next() { var i ListGenreMixTracksRow if err := rows.Scan( &i.ID, &i.AlbumID, &i.ArtistID, &i.Familiar, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil }