fix(mixes): close three intent gaps found in the system-playlists audit
Three discovery-mix defects from the intent audit (Scribe note #1254), all sharing the same root pattern — skips treated as non-events: - Deep Cuts (#1257): eligibility counted only unskipped plays, so a track skipped twice with zero completed listens read as "barely heard" and kept being re-offered. Tracks with >=2 skips no longer qualify; a single accidental skip doesn't banish. - Rediscover (#1258): a skip on a rediscover-sourced play — the user explicitly declining the resurfacing invitation — changed nothing, so declined tracks re-qualified the next day forever. Such tracks now sit out 90 days. - On This Day (#1256): day-of-year distance used plain ABS, so Dec 28 vs Jan 3 read as 359 days apart and the window silently gutted itself for ~3 weeks around every New Year. Now circular (LEAST(d, 365-d)), anchored on the build-date parameter instead of now() so it's testable and consistent with the mix's daily determinism. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsF3cNoKrqCYsU78cXC8U6
This commit is contained in:
@@ -27,12 +27,20 @@ play_counts AS (
|
||||
FROM play_events
|
||||
WHERE user_id = $1 AND was_skipped = false
|
||||
GROUP BY track_id
|
||||
),
|
||||
skip_counts AS (
|
||||
SELECT track_id, COUNT(*) AS c
|
||||
FROM play_events
|
||||
WHERE user_id = $1 AND was_skipped = true
|
||||
GROUP BY track_id
|
||||
)
|
||||
SELECT t.id, t.album_id, t.artist_id
|
||||
FROM tracks t
|
||||
JOIN affinity_artists aa ON aa.artist_id = t.artist_id
|
||||
LEFT JOIN play_counts pc ON pc.track_id = t.id
|
||||
LEFT JOIN skip_counts sc ON sc.track_id = t.id
|
||||
WHERE COALESCE(pc.c, 0) <= 2
|
||||
AND COALESCE(sc.c, 0) < 2
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||
@@ -60,6 +68,11 @@ type ListDeepCutsTracksRow struct {
|
||||
// gives per-play variety on top.
|
||||
// #419 Deep Cuts: low-play tracks (<=2 plays) from artists the user
|
||||
// has liked OR played heavily (>=5 non-skip plays across the artist).
|
||||
// Tracks the user has skipped twice or more don't qualify (#1257):
|
||||
// eligibility used to count only unskipped plays, so the most actively
|
||||
// rejected tracks read as "barely heard" and kept being re-offered.
|
||||
// Threshold 2 so a single accidental skip doesn't banish a track;
|
||||
// passive-signal only (no dislike UI, rule #101).
|
||||
// $1 user_id, $2 date string.
|
||||
func (q *Queries) ListDeepCutsTracks(ctx context.Context, arg ListDeepCutsTracksParams) ([]ListDeepCutsTracksRow, error) {
|
||||
rows, err := q.db.Query(ctx, listDeepCutsTracks, arg.UserID, arg.Column2)
|
||||
@@ -203,7 +216,10 @@ WITH windowed AS (
|
||||
FROM play_events
|
||||
WHERE user_id = $1 AND was_skipped = false
|
||||
AND started_at < now() - interval '30 days'
|
||||
AND ABS(EXTRACT(DOY FROM started_at) - EXTRACT(DOY FROM now())) <= 10
|
||||
AND LEAST(
|
||||
ABS(EXTRACT(DOY FROM started_at) - EXTRACT(DOY FROM $2::date)),
|
||||
365 - ABS(EXTRACT(DOY FROM started_at) - EXTRACT(DOY FROM $2::date))
|
||||
) <= 10
|
||||
GROUP BY track_id
|
||||
)
|
||||
SELECT t.id, t.album_id, t.artist_id
|
||||
@@ -235,6 +251,13 @@ type ListOnThisDayTracksRow struct {
|
||||
// and window ±7→±10 so it surfaces on a months-old library instead
|
||||
// of needing a full year of history; still skips cleanly (no rows →
|
||||
// no playlist) when there's no qualifying history yet.
|
||||
// Day-of-year distance is circular (#1256): plain ABS made Dec 28 vs
|
||||
// Jan 3 read as 359 days apart, silently gutting the window for ~3
|
||||
// weeks around every New Year — precisely when holiday nostalgia is
|
||||
// the point. LEAST(d, 365-d) wraps the boundary; leap-year drift of
|
||||
// ±1 is absorbed by the ±10 window. The build date ($2, already here
|
||||
// for the md5 rotation) anchors "today" instead of now() so the
|
||||
// window is testable and consistent with the mix's daily determinism.
|
||||
// $1 user_id, $2 date string.
|
||||
func (q *Queries) ListOnThisDayTracks(ctx context.Context, arg ListOnThisDayTracksParams) ([]ListOnThisDayTracksRow, error) {
|
||||
rows, err := q.db.Query(ctx, listOnThisDayTracks, arg.UserID, arg.Column2)
|
||||
@@ -272,6 +295,19 @@ SELECT t.id, t.album_id, t.artist_id
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||
)
|
||||
-- Declined-resurfacing cooldown (#1258): a skip on a
|
||||
-- rediscover-sourced play is the user explicitly saying "I've moved
|
||||
-- on" to the exact invitation this mix extends — such tracks sit
|
||||
-- out ~90 days instead of re-qualifying the next day. Passive
|
||||
-- signal only (rule #101).
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM play_events rp
|
||||
WHERE rp.user_id = $1
|
||||
AND rp.track_id = t.id
|
||||
AND rp.source = 'rediscover'
|
||||
AND rp.was_skipped = true
|
||||
AND rp.started_at > now() - interval '90 days'
|
||||
)
|
||||
ORDER BY (s.last_at <= now() - interval '6 months') DESC,
|
||||
(s.c >= 5) DESC,
|
||||
s.c DESC, t.id
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
-- name: ListDeepCutsTracks :many
|
||||
-- #419 Deep Cuts: low-play tracks (<=2 plays) from artists the user
|
||||
-- has liked OR played heavily (>=5 non-skip plays across the artist).
|
||||
-- Tracks the user has skipped twice or more don't qualify (#1257):
|
||||
-- eligibility used to count only unskipped plays, so the most actively
|
||||
-- rejected tracks read as "barely heard" and kept being re-offered.
|
||||
-- Threshold 2 so a single accidental skip doesn't banish a track;
|
||||
-- passive-signal only (no dislike UI, rule #101).
|
||||
-- $1 user_id, $2 date string.
|
||||
WITH affinity_artists AS (
|
||||
SELECT artist_id FROM general_likes_artists WHERE user_id = $1
|
||||
@@ -23,12 +28,20 @@ play_counts AS (
|
||||
FROM play_events
|
||||
WHERE user_id = $1 AND was_skipped = false
|
||||
GROUP BY track_id
|
||||
),
|
||||
skip_counts AS (
|
||||
SELECT track_id, COUNT(*) AS c
|
||||
FROM play_events
|
||||
WHERE user_id = $1 AND was_skipped = true
|
||||
GROUP BY track_id
|
||||
)
|
||||
SELECT t.id, t.album_id, t.artist_id
|
||||
FROM tracks t
|
||||
JOIN affinity_artists aa ON aa.artist_id = t.artist_id
|
||||
LEFT JOIN play_counts pc ON pc.track_id = t.id
|
||||
LEFT JOIN skip_counts sc ON sc.track_id = t.id
|
||||
WHERE COALESCE(pc.c, 0) <= 2
|
||||
AND COALESCE(sc.c, 0) < 2
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||
@@ -63,6 +76,19 @@ SELECT t.id, t.album_id, t.artist_id
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||
)
|
||||
-- Declined-resurfacing cooldown (#1258): a skip on a
|
||||
-- rediscover-sourced play is the user explicitly saying "I've moved
|
||||
-- on" to the exact invitation this mix extends — such tracks sit
|
||||
-- out ~90 days instead of re-qualifying the next day. Passive
|
||||
-- signal only (rule #101).
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM play_events rp
|
||||
WHERE rp.user_id = $1
|
||||
AND rp.track_id = t.id
|
||||
AND rp.source = 'rediscover'
|
||||
AND rp.was_skipped = true
|
||||
AND rp.started_at > now() - interval '90 days'
|
||||
)
|
||||
ORDER BY (s.last_at <= now() - interval '6 months') DESC,
|
||||
(s.c >= 5) DESC,
|
||||
s.c DESC, t.id
|
||||
@@ -103,13 +129,23 @@ SELECT t.id, t.album_id, t.artist_id
|
||||
-- and window ±7→±10 so it surfaces on a months-old library instead
|
||||
-- of needing a full year of history; still skips cleanly (no rows →
|
||||
-- no playlist) when there's no qualifying history yet.
|
||||
-- Day-of-year distance is circular (#1256): plain ABS made Dec 28 vs
|
||||
-- Jan 3 read as 359 days apart, silently gutting the window for ~3
|
||||
-- weeks around every New Year — precisely when holiday nostalgia is
|
||||
-- the point. LEAST(d, 365-d) wraps the boundary; leap-year drift of
|
||||
-- ±1 is absorbed by the ±10 window. The build date ($2, already here
|
||||
-- for the md5 rotation) anchors "today" instead of now() so the
|
||||
-- window is testable and consistent with the mix's daily determinism.
|
||||
-- $1 user_id, $2 date string.
|
||||
WITH windowed AS (
|
||||
SELECT track_id, COUNT(*) AS c
|
||||
FROM play_events
|
||||
WHERE user_id = $1 AND was_skipped = false
|
||||
AND started_at < now() - interval '30 days'
|
||||
AND ABS(EXTRACT(DOY FROM started_at) - EXTRACT(DOY FROM now())) <= 10
|
||||
AND LEAST(
|
||||
ABS(EXTRACT(DOY FROM started_at) - EXTRACT(DOY FROM $2::date)),
|
||||
365 - ABS(EXTRACT(DOY FROM started_at) - EXTRACT(DOY FROM $2::date))
|
||||
) <= 10
|
||||
GROUP BY track_id
|
||||
)
|
||||
SELECT t.id, t.album_id, t.artist_id
|
||||
|
||||
Reference in New Issue
Block a user