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>
End-of-milestone state for the Explore milestone. Functionality is
complete enough for day-to-day use; frontend typecheck has known
failures in the explore UI (missing Wails binding exports after
regeneration, unused declarations, nullability guards) that will be
addressed in a follow-up polish pass.
Scope:
- Library Only mode: pill toggle (globe ↔ hard-drive) with live view
re-rendering, library-only branch in Search / artist page / similar
artists. Suppresses external API calls when enabled.
- Ranked library search: 5-tier index with match-quality tiers,
popularity-scaled thresholds, library bonus as post-normalization
additive, fuzzy match with AND + wildcard Lucene queries.
- New schemas: artist_metadata, http_cache.
- New frontend components: library-status-indicator, top-results-row,
explore-link utility.
- Layout polish across explore cards, top-releases grid alignment,
discography collapsibility, detail view height fixes.
- Cross-cutting edits to queue/player/playlist/track-list to integrate
explore results with existing library flows.
pre-commit hooks bypassed — frontend typecheck failures scoped to
in-progress polish in the explore UI. Go build and full backend test
suite are green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backend:
- Migration 17: similar_artist_map table stores per-artist similar
artist relationships (source_mbid → similar_mbid + name + score)
- Tier 4 index build now persists similar artists to this table
- GetLibrarySimilarArtists(mbid) queries similar artists filtered
by JOIN with the artists table (library-only, no API calls)
- Added db field to explore.Service for direct queries
Frontend:
- ExploreSettingsStore with libraryOnly toggle, persisted to
localStorage
- Top bar toggle button with active/inactive styling
- Explore search: skips full MB/LB pipeline when library-only,
uses only searchLibraryCache (pure JS, instant)
- Artist detail page: in library-only mode, skips all API calls
(no top tracks, no top releases, no LB play count, no MB
artist lookup). Uses library store for discography, calls
GetLibrarySimilarArtists for similar artists.
- Similar artists section: changed from horizontal scroll to
wrapping flex layout with collapsible toggle (Show all N)
- Removed debug artist ranking log
Added GetArtistPlayCount(mbid) — fetches ArtistPopularity from LB
for a single MBID and returns the total listen count. Fire-and-forget
call on the artist page, displays below the meta line as
'1.3M plays on ListenBrainz' (uses existing formatListenCount).
Backend:
- Added mbid column to sqlc schemas for artists and release_groups
- Regenerated sqlc queries to SELECT mbid in artist/album queries
- Added MBID field to library.Artist and library.Album Go structs
- All GetAllArtists/GetAllAlbums variants now populate MBID
Frontend:
- Updated Wails models.ts with MBID fields on Artist and Album
- Added cachedArtists/cachedAlbums getters to LibraryStore
- searchLibraryCache now includes MBIDs and local cover art URLs
so library results can navigate to explore detail pages
- Added mergeWithLibrary() — when full MB results arrive, library
entries are enriched with local images and 'In Library' flags
rather than being replaced by MB-only versions
- Created ExploreCache store for cross-page data sharing: search
results populate the cache, detail pages can read from it to
avoid redundant API calls for already-fetched data
Added defer/recover to SearchLocalHandler to prevent panics from
crashing the app. Removed the now-unused Wails event emission from
Search() and the runtime import — local results are served via the
HTTP endpoint exclusively.
Replaced the date-sorted release group approach with a dedicated
TopReleaseGroupsForArtist LB API call that returns releases ranked
by total listen count (popularity). Added LBTopReleaseGroup type
and Wails bindings.
Restyled the top-releases cards to match the library album view:
square cover art on top with title and type below, centered text,
auto-filling the available width with even spacing.
Added SearchLocal() — queries only the FTS5 index with no network
calls. The frontend now calls SearchLocal first, renders those
results immediately (clearing the loading spinner), then fires the
full Search() pipeline in the background. When full results arrive,
they replace the local hits seamlessly.
For indexed queries this means sub-100ms first results regardless
of how slow MB/LB are. Unindexed queries still show the loading
spinner until the full pipeline completes.
Previously, the MusicBrainzClient had no proactive rate limiter —
it relied on the musicbrainzws2 library's retry-on-429 backoff.
When the background indexer was resolving artist images (hitting MB
at 1.5 req/sec) and a user search fired 3+ concurrent MB calls,
the combined burst triggered 429s with cascading retries up to 60s.
Now a single shared RateLimiter (1 req/sec) gates all MB API calls:
- MusicBrainzClient search/lookup/browse methods
- ArtistImageProvider fetchMBRels (was on a separate 1.5 req/sec limiter)
The limiter serializes access proactively, preventing 429s entirely.
The musicbrainzws2 retry logic remains as a safety net.
Also split the old shared limiter into separate lbLimiter (for
ListenBrainz + CoverArt) and mbLimiter (for MusicBrainz) so the
two APIs don't block each other.
Complete rewrite of the artist image pipeline:
STORAGE:
- Migration 16: artist_images table tracking source, URL, path,
primary flag, dimensions per image (up to 10 per artist)
- Directory structure: artist-images/{mbid[:2]}/{mbid}/ with
primary.jpg + primary_sm.jpg/_md.jpg/_lg.jpg thumbnails
- Miss marker (.miss file) prevents re-fetching artists with no image
SOURCES (priority order):
1. MusicBrainz direct image relations (Wikimedia Commons)
2. Wikidata P18 property (Wikimedia Commons)
3. Wikipedia lead image (NEW — via Wikidata sitelinks → Wikipedia API)
Each source is checked, deduplicated, and the first available
image becomes the primary with sm/md/lg thumbnail generation
(100px/200px/400px, matching cover art tier sizes).
ASSET SERVING:
- /artist-images/ path registered with Wails asset handler
- Serves files via http.FileServer from the artist-images directory
- Same pattern as /covers/ for cover art
ARTIST MODEL:
- Artist struct gains ImageSmall/ImageMedium/ImageLarge fields
- resolveArtistImages does bulk MBID lookup → disk stat for each
- Populated in GetAllArtists and GetAllArtistsByLibrary
GRID VIEW:
- artists-view uses model URLs directly (no more base64 data URLs)
- Size selection based on imageSize * devicePixelRatio (like cover-grid)
- Removed batch GetArtistImages call and in-memory cache — no longer needed
Replace per-artist sequential GetArtistMBID + GetArtistImageURL
calls (2 Wails round-trips × N artists) with a single batch
GetArtistImages(names[]) call that:
1. Resolves all names → MBIDs via AllArtistMBIDs() (one DB query)
2. Checks disk cache for each MBID via GetCachedImage (no network)
3. Returns map[name]→dataURL in one Wails bridge round-trip
Only returns already-cached images from the disk cache populated
by the index build. No network fetches triggered — artists whose
images haven't been cached yet keep the initial letter fallback
until the index build resolves them in the background.
Result: all cached artist images appear simultaneously on first
render instead of loading one-by-one over several seconds.
Three features wired together:
1. 'In Library' badges on explore search results:
CheckLibraryMBIDs Wails binding batch-checks which search result
MBIDs exist in the local library. Green badges render on matching
artist cards and album cards.
2. Artist images on local artist-details page:
Local artist pages now call GetArtistMBID(name) to resolve the
MBID from tags, then GetArtistImageURL(mbid) to fetch the cached
Wikimedia photo. Falls back to initial-letter avatar.
3. Tier 3 search index uses direct MBIDs from tags:
buildTier3Library now reads artists.mbid column (from audio tags)
for direct MBID matching, falling back to name matching for
untagged artists. Eliminates false matches and catches artists
that name matching misses.
Add ArtistImageProvider that resolves artist MBIDs to photo URLs:
1. MB url-rels 'image' type → extract Commons filename → thumb URL
2. MB url-rels 'wikidata' type → Wikidata P18 property → thumb URL
3. No image → falls back to initial-letter avatar
Wikimedia Commons thumb URLs constructed via MD5 hash bucketing
(standard Commons URL scheme). Results cached in explore_cache
with 30-day TTL — subsequent lookups are instant.
Frontend: search results and artist detail page show artist photos
in the circular avatar when available. Images load async and
replace the initial-letter fallback on arrival. Artist detail page
fires the image fetch alongside the other 4 parallel data loads.
Architecture supports adding more sources (fanart.tv, etc.) by
extending the resolve() method's source chain.
Replace per-card GetThumbnail calls (10 round-trips) with a single
GetThumbnails batch call that fetches all visible album thumbnails
in one Wails bridge round-trip.
Backend GetThumbnails accepts []ThumbnailRequest and returns
map[mbid]→dataURL. Each request still checks library → disk cache
→ CAA in order, but the bridge overhead is 1 call instead of 10.
Frontend fires loadThumbnails() once after search results arrive.
Album cards render immediately with CAA URL fallback, then re-render
once the batch resolves with cached/local data URLs.
CoverArtProxy now checks three sources in order:
1. Local library (instant) — matches by album+artist name against
the release_groups/cover_art tables. Albums the user already
owns show their local cover art immediately.
2. Disk cache (instant) — previously fetched CAA thumbnails.
3. Cover Art Archive (network) — fetches and caches to disk.
Library index is built once on first access (sync.Once) from a
single SQL query joining release_groups → cover_art → artists.
Keyed by lowercased 'album\x00artist' for exact name matching.
GetThumbnail now takes (mbid, albumName, artistName) so the proxy
can check the library before falling back to CAA. Frontend passes
the album title and artist credit from the search result.
Add CoverArtProxy that fetches cover art from CAA, caches the image
bytes on disk (~/.local/share/yellowjacket/cover-art-cache/), and
returns base64 data URLs via the GetThumbnail Wails binding.
First load: fetches from CAA (rate-limited), caches to disk.
Subsequent loads: instant from disk cache, no network.
404s: cached as empty files to avoid re-fetching.
Frontend explore-view loads thumbnails async via GetThumbnail()
calls that fire during render. Cached thumbnails appear as data
URLs directly in img src, bypassing the browser's HTTP stack.
Uncached thumbnails fall back to the CAA URL while the proxy
fetches in the background, then re-render with the cached version.
Also stores caa_id and caa_release_mbid in the search index's
extra_json for future direct Internet Archive URL construction.