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:
+54
-10
@@ -247,20 +247,28 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
|
||||
RepopulatePlaylists: yj.playlist.RepopulateFromM3U,
|
||||
ResolvePhantoms: yj.playlist.ResolvePhantomTracksAfterScan,
|
||||
OnAllScansComplete: func() {
|
||||
// Index new library artists (blocks until done).
|
||||
yj.explore.IndexNewArtists()
|
||||
yj.explore.WaitForIndexIdle()
|
||||
|
||||
// Populate local_*_id cross-reference columns on
|
||||
// explore_index so "is this in my library?" is O(1).
|
||||
// Fold the local library into the search index: every
|
||||
// MB-verified owned artist/album/track is upserted and
|
||||
// flagged in_library, straight from the library tables
|
||||
// with no API calls. Deep discographies stay lazy.
|
||||
yj.explore.PopulateLocalCrossReferences()
|
||||
|
||||
// Always start the full build — it's incremental and
|
||||
// will skip tiers that are already fresh. This ensures
|
||||
// sitewide + similar artist tiers run even if the index
|
||||
// already has library data.
|
||||
// Start (or resume) the dump-based index build. Skips
|
||||
// itself once the one-time import has completed, so this
|
||||
// is cheap on every startup.
|
||||
yj.explore.StartIndexBuild()
|
||||
|
||||
// Fold in any new incremental listen dumps to keep
|
||||
// popularity fresh (weekly-gated, background, no API).
|
||||
// No-op while the full import above is still running.
|
||||
yj.explore.RefreshListenCounts()
|
||||
|
||||
// Refresh the lyric-search FTS index from the just-scanned
|
||||
// library, then backfill any missing lyrics from LRCLIB in
|
||||
// the background (bounded, resumable, idempotent).
|
||||
yj.explore.RebuildLyricsIndex()
|
||||
yj.explore.BackfillLibraryLyrics()
|
||||
|
||||
// Sweep the autotag queue for newly-discovered pending
|
||||
// items so the user sees match scores ready when they
|
||||
// next open the review page. The worker is idempotent
|
||||
@@ -276,6 +284,10 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
|
||||
yj.library.SetRemovalHooks(library.RemovalHooks{
|
||||
StopPlayback: func() { yj.player.UnloadTrack() },
|
||||
CompactQueue: yj.queue.CompactAfterLibraryRemoval,
|
||||
// Removal deletes owned content outside a scan, so force the
|
||||
// gated library-sync steps to re-run on the next launch and
|
||||
// clear stale in_library flags / orphaned lyric-index rows.
|
||||
PostRemove: yj.explore.InvalidateLibrarySync,
|
||||
})
|
||||
|
||||
// Register playback finished handler to drive queue auto-advance.
|
||||
@@ -333,6 +345,21 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
|
||||
func (yj *YellowJacketApp) OnBeforeClose(ctx context.Context) bool {
|
||||
w, h := wailsruntime.WindowGetSize(ctx)
|
||||
|
||||
// Guard against a bogus size clobbering a good saved one. During
|
||||
// teardown / hot-reload the runtime can report a zero or below-
|
||||
// minimum size; persisting that would shrink the window to the
|
||||
// minimum on next launch. Keep the previously-saved size instead.
|
||||
if w < config.MinWidth || h < config.MinHeight {
|
||||
yj.logger.Warn("OnBeforeClose: ignoring bogus window size",
|
||||
"width", w,
|
||||
"height", h,
|
||||
"kept_width", yj.appConfig.Window.Width,
|
||||
"kept_height", yj.appConfig.Window.Height,
|
||||
)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
yj.logger.Info("OnBeforeClose: saving window state",
|
||||
"width", w,
|
||||
"height", h,
|
||||
@@ -393,7 +420,24 @@ func (yj *YellowJacketApp) OnDomReady(ctx context.Context) {
|
||||
// index build directly. If scans WERE queued, the
|
||||
// OnAllScansComplete hook starts it after they finish.
|
||||
if yj.library.GetScanQueueLength() == 0 && !yj.library.IsScanActive() {
|
||||
// Keep the search index's in_library flags and owned-entity
|
||||
// rows in sync. Gated: the library is unchanged here, so
|
||||
// this only does work on the first launch after an upgrade
|
||||
// or index wipe — steady-state launches skip the write burst.
|
||||
yj.explore.PopulateLocalCrossReferencesIfNeeded()
|
||||
|
||||
yj.explore.StartIndexBuild()
|
||||
|
||||
// Weekly-gated incremental popularity refresh (background,
|
||||
// no API). No-op while the full import is running.
|
||||
yj.explore.RefreshListenCounts()
|
||||
|
||||
// Same for the lyric-search index. The backfill keeps it in
|
||||
// sync incrementally, so on an unchanged library the full
|
||||
// rebuild is redundant and gated out; the backfill still runs
|
||||
// to fill any remaining gaps.
|
||||
yj.explore.RebuildLyricsIndexIfNeeded()
|
||||
yj.explore.BackfillLibraryLyrics()
|
||||
}
|
||||
|
||||
// Kick off the autotag prefetch worker so any unscored
|
||||
|
||||
Reference in New Issue
Block a user