-- 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. -- name: ListGenreMixCandidates :many -- 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. 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; -- name: ListGenreMixTracks :many -- 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, each half in a daily-deterministic order so the mix -- changes day to day and holds still within a day. -- -- 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. WITH play_counts AS ( SELECT pe.track_id, COUNT(*) AS c FROM play_events pe WHERE pe.user_id = sqlc.arg(user_id) 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 = sqlc.arg(user_id) AND pe.was_skipped = true GROUP BY pe.track_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, md5(t.id::text || sqlc.arg(date_seed)::text) AS tiebreak 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 = sqlc.arg(user_id) AND gl.track_id = t.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)) = sqlc.arg(genre_key)::text ) AND NOT EXISTS ( SELECT 1 FROM quarantine q WHERE q.user_id = sqlc.arg(user_id) AND q.track_id = t.id ) ), ranked AS ( SELECT e.*, row_number() OVER (PARTITION BY e.familiar ORDER BY e.tiebreak) 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.tiebreak;