feat: autotag scoring overhaul, dump-based explore index, and lyrics search
Consolidates in-progress work across autotag, explore, and library: - autotag: beets/Picard-informed scoring engine — ID-first matching, VA handling, recommendation tiers, and a merged distance/rank cascade, with an eval harness for regression tracking. - explore: offline MusicBrainz dump import/incremental refresh replaces the legacy tier crawl; index-first local search with fuzzy matching and a dedicated ranker; disk-free guards for dump downloads. - library: artist-credit extraction and matching. - lyrics: owned-library lyric search (FTS) with LRCLIB backfill. Also: rewrite README to be user-focused, and migrate upstream to git.ljones.me/yonlu/yellowjacket. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
package explore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"yellowjacket/backend/database"
|
||||
)
|
||||
|
||||
// newTestLRCLib builds an LRCLIB client whose HTTP calls hit the given
|
||||
// test server, sharing a real (in-memory) cache so caching behaviour is
|
||||
// exercised.
|
||||
func newTestLRCLib(t *testing.T, handler http.HandlerFunc) (*LRCLibClient, *httptest.Server) {
|
||||
t.Helper()
|
||||
|
||||
srv := httptest.NewServer(handler)
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
db := database.NewTestDB(t)
|
||||
cache := NewCache(db, testLogger())
|
||||
|
||||
c := NewLRCLibClient(cache, testLogger())
|
||||
// Point the client at the test server instead of the real API by
|
||||
// overriding its transport to rewrite the host.
|
||||
c.http = srv.Client()
|
||||
c.baseURL = srv.URL
|
||||
|
||||
return c, srv
|
||||
}
|
||||
|
||||
func TestLRCLibGetLyrics(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var calls int
|
||||
|
||||
c, _ := newTestLRCLib(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
|
||||
if r.URL.Path != "/api/get" {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("track_name") == "Missing" {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"id": 42,
|
||||
"trackName": "Yesterday",
|
||||
"artistName": "The Beatles",
|
||||
"albumName": "Help!",
|
||||
"duration": 125,
|
||||
"instrumental": false,
|
||||
"plainLyrics": "Yesterday, all my troubles seemed so far away",
|
||||
"syncedLyrics": "[00:00.00] Yesterday"
|
||||
}`))
|
||||
})
|
||||
|
||||
t.Run("hit returns plain + synced lyrics", func(t *testing.T) {
|
||||
lyrics, err := c.GetLyrics(context.Background(), "The Beatles", "Yesterday", "Help!", 125)
|
||||
if err != nil {
|
||||
t.Fatalf("GetLyrics: %v", err)
|
||||
}
|
||||
|
||||
if lyrics.Plain == "" || lyrics.Synced == "" {
|
||||
t.Errorf("expected plain and synced lyrics, got %+v", lyrics)
|
||||
}
|
||||
|
||||
if lyrics.Instrumental {
|
||||
t.Error("expected non-instrumental")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("second identical request is served from cache", func(t *testing.T) {
|
||||
before := calls
|
||||
|
||||
if _, err := c.GetLyrics(
|
||||
context.Background(),
|
||||
"The Beatles",
|
||||
"Yesterday",
|
||||
"Help!",
|
||||
125,
|
||||
); err != nil {
|
||||
t.Fatalf("GetLyrics: %v", err)
|
||||
}
|
||||
|
||||
if calls != before {
|
||||
t.Errorf("expected cache hit (no new HTTP call), calls went %d → %d", before, calls)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("404 maps to ErrLyricsNotFound", func(t *testing.T) {
|
||||
_, err := c.GetLyrics(context.Background(), "Nobody", "Missing", "", 0)
|
||||
if !errors.Is(err, ErrLyricsNotFound) {
|
||||
t.Errorf("expected ErrLyricsNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing artist/title short-circuits without a request", func(t *testing.T) {
|
||||
before := calls
|
||||
|
||||
if _, err := c.GetLyrics(
|
||||
context.Background(),
|
||||
"",
|
||||
"Yesterday",
|
||||
"",
|
||||
0,
|
||||
); !errors.Is(
|
||||
err,
|
||||
ErrLyricsNotFound,
|
||||
) {
|
||||
t.Errorf("expected ErrLyricsNotFound, got %v", err)
|
||||
}
|
||||
|
||||
if calls != before {
|
||||
t.Error("expected no HTTP call for empty artist")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user