feat: autotag mixed-bag splitting, search relevance fixes, and multi-library download imports
Autotag: detect "junk drawer" folders with no artist/album consensus and split them into synthetic per-cluster groups instead of forcing one match on an unrelated pile of tracks; repair tagging_items rows left behind by a prior scan orphan-cleanup gap. Explore: fix an exact artist-name search being drowned out by its own catalog entries in intent-prior scoring, and prune stale in_library bookkeeping left behind when a referenced library row is deleted. Download: fix a multi-library regression where every import failed with "no library root configured" — the importer resolved the library root from a legacy single-library config field that nothing populates in the current multi-library model. It now resolves the destination library per-request from the request's own library_id. Also widen the Soulseek search window (12s -> 20s), measured against real request history to be missing available peers on live queries. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2Agd9af5hE7qzti2ackiS
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package explore
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"yellowjacket/backend/database"
|
||||
"yellowjacket/backend/database/sql/sqlcgen"
|
||||
)
|
||||
|
||||
// TestPruneStaleLocalCrossReferences verifies that an explore_index row
|
||||
// whose local_*_id points at a library row that no longer exists gets its
|
||||
// in_library flag and local_*_id cleared, while a row still backed by a
|
||||
// real library entity is left untouched. This is the fix for stale
|
||||
// "in_library" bookkeeping surviving a rescan that removed the file it
|
||||
// was tied to (upsertIndexConflictSQL only ever adds these references,
|
||||
// never clears them).
|
||||
func TestPruneStaleLocalCrossReferences(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := database.NewTestDB(t)
|
||||
si := NewSearchIndex(db, nil, nil, slog.Default())
|
||||
|
||||
// A library artist that still exists.
|
||||
artist, err := db.Queries.UpsertArtist(t.Context(), "Still Owned")
|
||||
if err != nil {
|
||||
t.Fatalf("upsert artist: %v", err)
|
||||
}
|
||||
|
||||
// Two explore_index artist rows: one pointing at the still-existing
|
||||
// artist, one pointing at a local ID that has since been deleted
|
||||
// (simulating a rescan that removed the owning artist).
|
||||
seedArtist := func(mbid, title string, localID int64) {
|
||||
t.Helper()
|
||||
|
||||
if _, err := db.ExecContext(
|
||||
upsertIndexSQL,
|
||||
"artist", mbid, title, title, mbid, "",
|
||||
0, 0,
|
||||
0, "", "",
|
||||
"", "", "",
|
||||
"", "", "", "",
|
||||
1, 0,
|
||||
localID, 0, 0,
|
||||
0,
|
||||
); err != nil {
|
||||
t.Fatalf("seed explore_index row for %q: %v", mbid, err)
|
||||
}
|
||||
}
|
||||
|
||||
seedArtist("still-owned-mbid", "Still Owned", artist.ID)
|
||||
seedArtist("removed-mbid", "Removed Artist", 999999)
|
||||
|
||||
si.pruneStaleLocalCrossReferences()
|
||||
|
||||
stillOwned := si.LookupArtistByMBID("still-owned-mbid")
|
||||
if stillOwned == nil {
|
||||
t.Fatal("expected still-owned artist row to survive pruning")
|
||||
}
|
||||
|
||||
if !stillOwned.InLibrary || stillOwned.LocalArtistID != artist.ID {
|
||||
t.Errorf(
|
||||
"still-owned artist: InLibrary=%v LocalArtistID=%d, want InLibrary=true LocalArtistID=%d",
|
||||
stillOwned.InLibrary,
|
||||
stillOwned.LocalArtistID,
|
||||
artist.ID,
|
||||
)
|
||||
}
|
||||
|
||||
removed := si.LookupArtistByMBID("removed-mbid")
|
||||
if removed == nil {
|
||||
t.Fatal("expected removed-artist row to still exist (only cross-references cleared)")
|
||||
}
|
||||
|
||||
if removed.InLibrary || removed.LocalArtistID != 0 {
|
||||
t.Errorf(
|
||||
"removed artist: InLibrary=%v LocalArtistID=%d, want InLibrary=false LocalArtistID=0",
|
||||
removed.InLibrary, removed.LocalArtistID,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnenrichedLibraryArtistMBIDs_OrdersByOwnedTrackCount verifies the
|
||||
// backfill queue prioritizes artists by how many tracks the user actually
|
||||
// owns, not by how many duplicate-mbid artist rows happen to exist (the
|
||||
// previous "ORDER BY COUNT(*)" grouped on a.mbid, which is nearly always 1
|
||||
// per artist and so wasn't really ordering by anything meaningful).
|
||||
func TestUnenrichedLibraryArtistMBIDs_OrdersByOwnedTrackCount(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := database.NewTestDB(t)
|
||||
si := NewSearchIndex(db, nil, nil, slog.Default())
|
||||
q := db.Queries
|
||||
ctx := t.Context()
|
||||
|
||||
seedArtistWithTracks := func(name, mbid string, trackCount int) {
|
||||
t.Helper()
|
||||
|
||||
artist, err := q.UpsertArtist(ctx, name)
|
||||
if err != nil {
|
||||
t.Fatalf("upsert artist %q: %v", name, err)
|
||||
}
|
||||
|
||||
_, err = db.ExecContext("UPDATE artists SET mbid = ? WHERE id = ?", mbid, artist.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("set mbid for %q: %v", name, err)
|
||||
}
|
||||
|
||||
ac, err := q.UpsertArtistCredit(ctx, name)
|
||||
if err != nil {
|
||||
t.Fatalf("upsert artist credit %q: %v", name, err)
|
||||
}
|
||||
|
||||
if _, err := q.CreateArtistCreditArtist(ctx, sqlcgen.CreateArtistCreditArtistParams{
|
||||
ArtistID: artist.ID,
|
||||
CreditID: ac.ID,
|
||||
}); err != nil {
|
||||
t.Fatalf("link artist credit artist %q: %v", name, err)
|
||||
}
|
||||
|
||||
for i := range trackCount {
|
||||
rec, err := q.CreateRecordingFull(ctx, sqlcgen.CreateRecordingFullParams{
|
||||
Name: name,
|
||||
ArtistCreditID: ac.ID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create recording for %q: %v", name, err)
|
||||
}
|
||||
|
||||
if _, err := q.CreateAudioFile(ctx, sqlcgen.CreateAudioFileParams{
|
||||
FilePath: name + "/" + string(rune('a'+i)) + ".mp3",
|
||||
LengthMilliseconds: 180000,
|
||||
RecordingID: rec.ID,
|
||||
Basename: string(rune('a'+i)) + ".mp3",
|
||||
}); err != nil {
|
||||
t.Fatalf("create audio file for %q: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
seedArtistWithTracks("Few Tracks", "few-mbid", 1)
|
||||
seedArtistWithTracks("Many Tracks", "many-mbid", 5)
|
||||
|
||||
mbids := si.unenrichedLibraryArtistMBIDs(10)
|
||||
if len(mbids) != 2 {
|
||||
t.Fatalf("unenrichedLibraryArtistMBIDs() = %v, want 2 entries", mbids)
|
||||
}
|
||||
|
||||
if mbids[0] != "many-mbid" {
|
||||
t.Errorf(
|
||||
"unenrichedLibraryArtistMBIDs()[0] = %q, want %q (most owned tracks first)",
|
||||
mbids[0],
|
||||
"many-mbid",
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user