perf(smartplaylist): batch-load cover art + MBIDs instead of per-row subquery

The autotag overhaul added cover-art and MusicBrainz-ID columns to
leanTrackQuery to support the new track-row styling, reintroducing the
per-row correlated subquery anti-pattern (artist_mbid) plus a cover_art
join inside the whole-library derived table. Both ran for every track
before WHERE/LIMIT, so smart-playlist evaluation cost scaled with
library size rather than result size — several seconds for a 500-track
playlist that was previously sub-second.

Move these presentation-only fields into a batched fetchArtwork pass
keyed by the matched recording_ids, mirroring the existing fetchGenres
batch. Cost is now proportional to results. Add TestEvaluate_ArtworkEnrichment
(no prior coverage of these fields) and an artwork_ms debug metric.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yonluandClaude Opus 4.8 committed 2026-07-24 14:31:33 -04:00
1 parent 5b161f50c9
commit 16886c92cf
2 files changed
+254 -50

No files matched your search

@@ -4,6 +4,7 @@ import (
"strings"
"testing"
"yellowjacket/backend/coverart"
"yellowjacket/backend/database"
)
@@ -812,6 +813,85 @@ func TestEvaluate_TextIs(t *testing.T) {
}
}
// TestEvaluate_ArtworkEnrichment verifies the presentation-only
// cover-art and MusicBrainz-ID fields are attached to matched tracks
// by the batched fetchArtwork pass (they are no longer part of the
// lean filter query).
func TestEvaluate_ArtworkEnrichment(t *testing.T) {
t.Parallel()
db := database.NewTestDB(t)
// Minimal FK chain: cover_art → release_group(mbid) →
// release_group_recordings → recording(mbid) → audio_file, plus
// artist_credit → artist_credit_artist → artist(mbid).
exec := func(query string, args ...any) {
t.Helper()
if _, err := db.ExecContext(query, args...); err != nil {
t.Fatalf("seed %q: %v", query, err)
}
}
// file_types are pre-seeded by the schema (id 0 = .mp3).
exec("INSERT INTO cover_art (id, file_path, mime_type) " +
"VALUES (1, '/covers/abc123.jpg', 'image/jpeg')")
exec("INSERT INTO artists (id, name, mbid) " +
"VALUES (1, 'Queen', 'artist-mbid-1')")
exec("INSERT INTO artist_credit (id, text) VALUES (1, 'Queen')")
exec("INSERT INTO artist_credit_artist (credit_id, artist_id) " +
"VALUES (1, 1)")
exec("INSERT INTO release_groups (id, name, cover_art_id, mbid) " +
"VALUES (1, 'A Night at the Opera', 1, 'rg-mbid-1')")
exec("INSERT INTO recordings (id, name, artist_credit_id, mbid) " +
"VALUES (1, 'Bohemian Rhapsody', 1, 'rec-mbid-1')")
exec("INSERT INTO release_group_recordings " +
"(release_group_id, recording_id) VALUES (1, 1)")
exec("INSERT INTO audio_files (id, file_path, " +
"length_milliseconds, recording_id, file_type_id) " +
"VALUES (1, '/music/bohemian.mp3', 354000, 1, 0)")
tracks, err := Evaluate(db, RuleSet{
Rules: []Rule{
{Field: "artist", Operator: "is", Value: "Queen"},
},
})
if err != nil {
t.Fatalf("Evaluate: %v", err)
}
if len(tracks) != 1 {
t.Fatalf("got %d tracks, want 1", len(tracks))
}
tr := tracks[0]
if tr.ArtistMBID != "artist-mbid-1" {
t.Errorf("ArtistMBID = %q, want artist-mbid-1", tr.ArtistMBID)
}
if tr.ReleaseGroupMBID != "rg-mbid-1" {
t.Errorf("ReleaseGroupMBID = %q, want rg-mbid-1",
tr.ReleaseGroupMBID)
}
if tr.RecordingMBID != "rec-mbid-1" {
t.Errorf("RecordingMBID = %q, want rec-mbid-1",
tr.RecordingMBID)
}
wantURLs := coverart.ResolveURLs("/covers/abc123.jpg")
if tr.CoverArtPath != wantURLs.Original {
t.Errorf("CoverArtPath = %q, want %q",
tr.CoverArtPath, wantURLs.Original)
}
if tr.CoverArtSmall != wantURLs.Small {
t.Errorf("CoverArtSmall = %q, want %q",
tr.CoverArtSmall, wantURLs.Small)
}
}
func TestEvaluate_TextContains(t *testing.T) {
t.Parallel()