Files
minstrel/internal/playlists/seed_missing_db_test.go
T
bvandeusenandClaude Opus 5.5 efa3bf54ce
release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m11s
release / go (push) Successful in 1m29s
release / integration (push) Successful in 4m27s
release / android (push) Successful in 5m2s
release / Build signed APK (releases and dev) (push) Successful in 5m17s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m15s
release / Verify release artifacts (tag releases only) (push) Skipped
fix(playlists): a missing track never seeds For You or Songs-like (#2701)
PickTopPlayedTracksForUser's liked tier read general_likes without
joining tracks. With no plays to seed from, For You could pick a liked
track whose file is gone. The play tiers were already safe: their
play_events join filters missing_since.

The same gap was in PickTopPlayedTrackForArtistByUser's fallback, which
seeds Songs-like from the artist's newest album when there are no recent
plays. It could pick a missing track, and since #5296 a missing track is
never fetched for similarity, so that seed has no edges either. The
caller already skips an empty seed, so an artist whose tracks are all
missing gets no Songs-like mix instead of one aimed at nothing.

Integration tests cover both cases: a liked-but-missing track is not a
seed, and the Songs-like fallback moves to the next album once the
newest one's track goes missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:10:17 -04:00

86 lines
3.0 KiB
Go

package playlists_test
import (
"context"
"testing"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)
func likeTrack(t *testing.T, pool *pgxpool.Pool, userID, trackID pgtype.UUID) {
t.Helper()
if _, err := pool.Exec(context.Background(),
`INSERT INTO general_likes (user_id, track_id) VALUES ($1, $2)`,
userID, trackID); err != nil {
t.Fatalf("like track: %v", err)
}
}
func markMissing(t *testing.T, pool *pgxpool.Pool, trackID pgtype.UUID) {
t.Helper()
if _, err := pool.Exec(context.Background(),
`UPDATE tracks SET missing_since = now() WHERE id = $1`, trackID); err != nil {
t.Fatalf("mark missing: %v", err)
}
}
// TestPickTopPlayedTracksForUser_LikedTierSkipsMissing: with no plays, For
// You seeds from likes. A liked track whose file is gone is not a seed (#2701).
func TestPickTopPlayedTracksForUser_LikedTierSkipsMissing(t *testing.T) {
pool := newPool(t)
u := seedUser(t, pool, "seedmissing")
present := seedTrack(t, pool, "Present", "Seed Artist A")
gone := seedTrack(t, pool, "Gone", "Seed Artist B")
likeTrack(t, pool, u.ID, present.ID)
likeTrack(t, pool, u.ID, gone.ID)
markMissing(t, pool, gone.ID)
seeds, err := dbq.New(pool).PickTopPlayedTracksForUser(context.Background(), u.ID)
if err != nil {
t.Fatalf("pick seeds: %v", err)
}
if len(seeds) != 1 || seeds[0] != present.ID {
t.Fatalf("seeds = %v, want only the present liked track %v", seeds, present.ID)
}
}
// TestPickTopPlayedTrackForArtistByUser_FallbackSkipsMissing: with no plays,
// Songs-like seeds from the artist's newest album. When that album's track is
// missing, the seed comes from the next album rather than the missing file.
func TestPickTopPlayedTrackForArtistByUser_FallbackSkipsMissing(t *testing.T) {
pool := newPool(t)
ctx := context.Background()
u := seedUser(t, pool, "songslikemissing")
older := seedTrack(t, pool, "Older", "Songs Like Artist")
newerAlbum := seedAlbumForArtist(t, pool, "Newer Album", older.ArtistID)
newer := seedTrackForArtist(t, pool, "Newer", newerAlbum, older.ArtistID)
if _, err := pool.Exec(ctx, `UPDATE albums SET release_date = '2010-01-01' WHERE id = $1`, older.AlbumID); err != nil {
t.Fatalf("date older album: %v", err)
}
if _, err := pool.Exec(ctx, `UPDATE albums SET release_date = '2020-01-01' WHERE id = $1`, newerAlbum); err != nil {
t.Fatalf("date newer album: %v", err)
}
q := dbq.New(pool)
args := dbq.PickTopPlayedTrackForArtistByUserParams{UserID: u.ID, ArtistID: older.ArtistID}
got, err := q.PickTopPlayedTrackForArtistByUser(ctx, args)
if err != nil {
t.Fatalf("pick seed: %v", err)
}
if got != newer.ID {
t.Fatalf("before marking: seed = %v, want the newest album's track %v", got, newer.ID)
}
markMissing(t, pool, newer.ID)
got, err = q.PickTopPlayedTrackForArtistByUser(ctx, args)
if err != nil {
t.Fatalf("pick seed: %v", err)
}
if got != older.ID {
t.Fatalf("after marking: seed = %v, want the present older track %v", got, older.ID)
}
}