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.
Extract primary English alias from MusicBrainz artist data when the
canonical name uses non-Latin script (CJK, Cyrillic, etc.). Display
it as the primary name in search results and artist detail header,
with the native script name as a subtitle beneath.
Example: 山下達郎 now shows 'Tatsuro Yamashita' prominently with
'山下達郎' as a subtitle. Artists with Latin names are unchanged.
Backend:
- GetAllTracksWithFullMetadata queries now select play_count and last_played
- mapTrackRow accepts and passes through PlayCount/LastPlayed
- PlayCount + LastPlayed added to library.Track struct
- sqlcgen Row types updated with new fields
Frontend:
- PlayCount + LastPlayed added to library.Track TypeScript model
- 'Plays' column added to track-list column definitions (60px, right-aligned, sortable)
Queries without play data (search, genre, album) pass 0/empty defaults.
- Add BatchFailure and BatchResult types for structured batch outcomes
- Add cancelBatch channel and suppressEvents flag to TagWriter struct
- Add CancelBatchWrite method for mid-batch cancellation from frontend
- Add BatchWriteTrackTags method processing tracks sequentially
- Emit BatchWriteProgress event per-track with current/total/succeeded/failed
- Suppress per-track TrackMetadataChanged; emit single event after batch
- Wails bindings auto-generated for BatchWriteTrackTags and CancelBatchWrite
- tagwriter namespace with BatchResult/BatchFailure in models.ts
- Wire saveEdit() to WriteTrackTagsByPath with diff-only TagChanges map
- Add cover art selection via ImageFilePicker with instant blob preview
- Add cover art removal (× button) with clearCoverArt state
- Show saving indicator and disable buttons during save
- Display errors inline in action bar; edit mode stays active on error
- Add ReadFile Go method on FrontendUtil to read cover art bytes
- Add Wails binding for ReadFile
- Clean up all edit state (editValues, pendingCoverArt, errorMessage) on close/cancel
- WriteTrackTagsByPath resolves filePath to trackID via GetAudioFileByPath
- ImageFilePicker opens native file dialog filtered to JPEG/PNG
- Added Wails TypeScript bindings for both new methods
- Add ScanHooks callback struct to library package (follows RemovalHooks pattern)
- Move phantom resolution from library to playlist service via hook
- New ResolvePhantomTracksAfterScan reads M3U8 files and resolves paths
against current audio_files using multi-root resolution
- Handles pre-existing phantoms (match by M3U8 position) and new ones
(match by phantom_file_path)
- Delete old resolvePhantomTracks method that required phantom_file_path
- Wire ScanHooks in app.go OnStartup
Root causes addressed:
- track-list had no _itemSize hint for flow layout — virtualizer
defaulted to 100px, measured actual ~33px rows, then called
_correctScrollError/scrollTo on every scroll causing visible jumps
- will-change:transform on virtualizer elements caused nested GPU
layers (virtualizer positions children with transforms internally)
adding compositor overhead instead of helping
- content-visibility:auto on album cards conflicted with virtualizer's
own DOM recycling, causing redundant layout recalculation
- track-list visibilityChanged handler wrote to store synchronously
on every event (per-item during scroll) without any throttling
- IIFE closure in renderTrackRow created a new function per row per render
Fixes applied:
- Add _itemSize:{height:33} + fixed height:33px on .track-row (matches
queue-panel pattern that already worked smoothly)
- Add overflow-anchor:none on track-list virtualizer
- Remove will-change:transform from all 6 scroll containers
- Remove content-visibility:auto from album cards
- RAF-throttle visibilityChanged scroll position saves
- Replace IIFE with direct cols.map() in template
recording_genres and release_group_recordings reference recordings.id,
so they must be deleted BEFORE the recordings table is cleaned.
Also extends toast duration to 8s for readability.
Task 1: Schema, events, and progress types
- Add library_id to CreateAudioFile SQL INSERT and regenerate sqlc code
- Add LibraryScanQueued and LibraryScanQueueDrained event constants
- Regenerate TypeScript events via genevents
- Add LibraryID, LibraryName, QueuedCount to ScanProgress
- Add LibraryID, LibraryName to ScanMetrics
- Add libraryID field to importResult for threading through pipeline
Task 2: Scan queue coordinator and per-library scanning
- Create scan_queue.go with ScanLibrary(id), ScanAllLibraries()
- Add CancelCurrentScan(), CancelAllScans() for queue-aware cancellation
- FIFO scan queue with silent dedup (same library already scanning or queued)
- Refactor Scan() -> scanInternal(libraryID, libraryName, libraryPath)
- Replace GetAllAudioFiles with GetAudioFilesByLibrary for per-library loading
- Thread libraryID through DB writer to set CreateAudioFileParams.LibraryID
- drainQueue auto-starts next queued library or emits LibraryScanQueueDrained
- Pause freezes current scan AND queue
- Add GetScanQueueLength() and QueuedLibraryNames() for UI
- Mark CancelScan() and Scan() as deprecated
- Pause/Resume buttons replace Soft Scan/Full Rescan during active scan
- Cancel button shows confirmation dialog with Keep/Discard/Continue options
- Register LibraryScanPaused/Resumed/Cancelled event handlers
- Status bar shows 'Scan paused.' with accent color when paused
- Add CancelScan/PauseScan/ResumeScan Wails binding stubs
- Cancel dialog overlay dismissible by clicking outside or Continue Scanning
- Add cancelled field to ScanMetrics TypeScript interface
- Create backend/shortcuts/config.go with DefaultBindings(), ApplyDefaults(), Validate()
- Wire Shortcuts field into main Config struct with TOML persistence
- Add GetShortcuts, SetShortcuts, SetShortcut, ResetShortcuts Wails binding methods
- Regenerate Wails TypeScript bindings for new config methods
- Fix wsl lint in library.go (blank line before logger call)
Move search indicator from absolute-positioned overlay into sort toolbar
(or dedicated search-bar-row for artists/genres views). Shows indicator
on empty-state screens. Fix cover art not displaying for expanded album
tracks by checking expandedAlbumId before the albumName guard. Add
ScanWarning model bindings.
- Add pinDefault state, getter, setter to favorites-store
- Add pinDefault getter and setPinDefault to favorites-controller
- Update sortedEntries in playlist-view to pin default playlist to top
- Add Pin to Top toggle in config page Favorites section
- Add Wails bindings for GetPinDefaultPlaylist/SetPinDefaultPlaylist
- React to PinDefault in FavoritesConfigChanged event payload
- Add CreatedAt and UpdatedAt string fields to Summary struct
- Format as RFC3339 at all construction sites in playlist.go and favorites.go
- Update TypeScript bindings with new fields in models.ts
- Wails bindings reflect new PlaylistFilePicker and ImportPlaylists signatures
- Frontend handleImportPlaylist now passes file array to ImportPlaylists