Commit Graph
250 Commits
Author SHA1 Message Date
yonlu 33d42febf1 fix: add panic recovery to search-local handler, remove unused event emission
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.
2026-03-29 17:52:33 -04:00
yonlu c3e16a9fd2 fix: bypass Wails RPC entirely for local search via HTTP endpoint
Both SearchLocal RPC and Wails events were blocked by Wails v2's
Go call serialization. When the indexer or other Go calls were
in-flight, even a 1ms Go function couldn't return to JS.

New approach: registered /api/search-local as an HTTP handler on
the Wails asset server. The frontend fetches it directly via
fetch() — this runs on Go's HTTP server goroutine pool, completely
independent of Wails RPC serialization.

The fetch completes in milliseconds regardless of what other Go
calls are queued. The full Search() pipeline still runs via Wails
RPC and replaces the local results when done.
2026-03-29 17:48:01 -04:00
yonlu c22066e5d5 fix: use Wails event for instant local search results
The SearchLocal RPC approach couldn't render results instantly
because Wails v2 serializes Go method calls — SearchLocal would
queue behind other in-flight calls.

Now Search() emits a 'search:local-results' Wails event at the
start of Phase 0 (before the slow MB/LB pipeline begins). The
frontend listens for this event in connectedCallback and renders
the local hits immediately. The event bypasses the RPC queue
since it's pushed from Go, not pulled by JS.

Removed the SearchLocal RPC call from the frontend entirely.
2026-03-29 17:42:10 -04:00
yonlu 0ce53fb87a fix: local search results now render before full pipeline completes
The full Search() call was blocking the render even after SearchLocal
returned results — both awaits ran in the same async function, and
Wails may serialize Go calls preventing the microtask yield from
triggering a render.

Split executeFullSearch into a separate async method invoked with
void (fire-and-forget). executeSearch now returns after SearchLocal
completes, letting Lit render the local results immediately. The
full pipeline results replace them when ready.
2026-03-29 17:34:40 -04:00
yonlu 50df4676f8 fix: artist flash on search + independent top section columns
Three fixes:

1. SearchLocal no longer applies minBlendedScore filter — index hits
   use scalePopularity scores that aren't comparable to blended
   MB+LB scores. This prevents artists from appearing in local
   results then disappearing when the full pipeline replaces them
   with score-filtered results.

2. Top section columns now render independently — tracks show as
   soon as they load, top releases show their own loading state or
   appear when ready. Previously the entire section was blocked
   until both finished loading.

3. Top releases column shows a loading spinner while its data is
   still fetching, rather than being invisible.
2026-03-29 17:23:13 -04:00
yonlu cef6709d9a feat: top releases sorted by popularity with library-style album cards
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.
2026-03-29 17:12:30 -04:00
yonlu 4138fe593d feat: instant local search results while full pipeline runs
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.
2026-03-29 16:45:25 -04:00
yonlu a93a83c4d2 fix: shared MB rate limiter prevents search/indexer 429 collisions
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.
2026-03-29 16:37:51 -04:00
yonlu fb3a340fa2 fix: compact top-releases cards to match track list height
Replace full album cards (square art + text below) with compact
horizontal cards: 44px art thumbnail on left, title + year on right.
Cards fit ~56px tall each, so a 2×2 grid of 4 releases aligns with
the height of 5 track rows in the left column.
2026-03-29 16:26:02 -04:00
yonlu a312c3d2cf feat: two-column top section — tracks + releases side-by-side
Replace full-width top tracks with a split layout: top tracks on the
left (5 default), top releases grid on the right (2×2 = 4 default).
A 'Show more' toggle below both columns expands to 10 tracks and 8
releases. Top releases are sorted newest-first from the existing
discography data.

The full discography section remains below for browsing by type.
2026-03-29 16:21:01 -04:00
yonlu 4e9df89f93 feat: add artist images to similar artists section
After similar artists load, fetch images for each via GetArtistImageURL
in parallel. Images pop in as they resolve — letter avatars remain as
fallback for artists without images. Each image is cached on disk after
first resolution, so subsequent views are instant.
2026-03-29 15:53:45 -04:00
yonlu 19a803d1fc feat: multi-source artist images with thumbnails + grid integration
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
2026-03-29 08:33:32 -04:00
yonlu 7ec9785463 perf: batch artist image loading for library grid view
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.
2026-03-28 15:22:22 -04:00
yonlu df1d1785e9 feat: artist images in library artists grid view
The local artists grid now shows Wikimedia artist photos in the
circular avatars. Each visible artist card triggers an async load:
GetArtistMBID(name) → GetArtistImageURL(mbid) → cached data URL.

Images load progressively — the initial letter placeholder shows
immediately, replaced by the photo when it resolves. Results are
cached in-memory per session. Artists without MBIDs or without
Wikimedia photos keep the initial letter fallback.

Uses the same disk-cached artist image pipeline as the explore
views — no extra network requests for previously resolved artists.
2026-03-28 13:50:24 -04:00
yonlu 48a8713091 refactor: remove Top Results section from explore search
Remove the mixed Top Results section that showed a blend of artists
and recordings. Search results now show three clean categories:
Artists, Albums, Tracks — each sorted by their own scoring.

Removed: ScoredItem interface, TOP_RESULTS_COUNT, getTopResults,
renderTopResults, renderTopCard, and .top-card CSS.
2026-03-28 12:34:40 -04:00
yonlu 9a841cd173 feat: MusicBrainz verification badge + MBID links in track details
Track details dialog now shows:

1. Green checkmark badge next to the track title when the recording
   has a MusicBrainz ID (hover: 'Metadata verified by MusicBrainz')

2. MusicBrainz section at the bottom with clickable MBID links for:
   - Recording (track) → musicbrainz.org/recording/{mbid}
   - Release Group (album) → musicbrainz.org/release-group/{mbid}
   - Artist → musicbrainz.org/artist/{mbid}

   Links open in the system browser. Only shown for entities that
   have MBIDs from audio file tags.

Backend: GetTrackMBIDs(filePath) Wails binding queries recording,
release_group, and artist mbid columns via a single JOIN query.
Frontend: loaded async when the dialog opens, non-blocking.
2026-03-28 10:11:52 -04:00
yonlu 1797eef844 feat: 'In Library' badges on explore artist detail page
Add CheckLibraryMBIDs call after artist data loads. Checks all
release group MBIDs from the discography against the local library.
Matching albums show a green 'In Library' badge in the album meta
section alongside the year.

Fires after Promise.allSettled completes (same timing as artist
image fetch). Non-blocking — badge appears on re-render when the
check completes.
2026-03-26 12:10:26 -04:00
yonlu 8f6a4c6a8e feat: 'In Library' badges, artist images on local pages, MBID-based Tier 3
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.
2026-03-26 09:34:57 -04:00
yonlu 963269b753 feat: artist image disk cache + fix top results artist photos
Two changes:

1. Artist image disk cache: ArtistImageProvider now fetches the
   actual image bytes from Wikimedia Commons and caches them on
   disk (~/.local/share/yellowjacket/artist-image-cache/{mbid}.jpg).
   Returns base64 data URLs, same pattern as CoverArtProxy.
   First lookup: resolve URL via MB/Wikidata + fetch image (~2s).
   Subsequent: instant from disk cache.
   404s cached as empty files to avoid re-fetching.

2. Top results artist photos: the Top Results section now shows
   artist images from the artistImageCache, same as the Artists
   section. Also shows englishName in the top card display name.
2026-03-25 22:28:52 -04:00
yonlu 9b88b88523 fix: rate-limit MB url-rels fetches, serialize frontend image loads
Artist image resolution was hitting musicbrainz.org with 10
concurrent unthrottled requests per search — enough to trigger
MB's rate limit rejection. Two fixes:

Backend: add dedicated 1 req/s RateLimiter for MB url-rels fetches
in ArtistImageProvider. Each fetch waits on the limiter before
the HTTP call. Results are cached 30 days so repeat lookups are
instant.

Frontend: switch loadArtistImages from concurrent fire-all to
sequential await loop. Each artist image loads one at a time,
images appear progressively as they resolve instead of all
failing from rate limit rejection.
2026-03-25 22:19:18 -04:00
yonlu 55473dd52a feat: artist images from MusicBrainz/Wikidata/Wikimedia Commons
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.
2026-03-25 19:55:10 -04:00
yonlu 0e376abfbb perf: batch thumbnail loading — one Wails call for all album art
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.
2026-03-25 19:37:46 -04:00
yonlu 0761cff408 feat: use local library cover art for search results
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.
2026-03-25 19:00:29 -04:00
yonlu 49a26c6163 feat: cover art proxy with disk cache for instant thumbnail loading
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.
2026-03-25 14:09:30 -04:00
yonlu f2a703ed52 feat: show English alias for non-Latin script artists
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.
2026-03-24 22:47:39 -04:00
yonlu 5f8f6d6a26 feat: min 2-char query gate, result caps at 10 per section
Don't fire search for single-character queries — show 'Keep typing…'
instead. Cap rendered results at 10 per section (artists, albums,
tracks) to reduce noise. Top results already capped at 3.
2026-03-24 22:41:02 -04:00
yonlu 4ddd252e1e fix: don't double-pluralize Albums/Other Albums section headers 2026-03-24 21:43:35 -04:00
yonlu 450f6002a2 feat: split artist discography into Albums vs Other Albums
Use MusicBrainz secondaryTypes to distinguish studio albums from
compilations, soundtracks, live albums, remixes, etc. Albums with
no non-studio secondary types show under 'Albums'; everything else
under 'Other Albums'. EPs and Singles remain their own sections.

Section order: Albums → EP → Single → Other Albums → ...rest.
2026-03-24 21:40:25 -04:00
yonlu a62b1da474 feat(S04/T02): Built the explore-album-details Lit component with relea…
- frontend/src/components/explore-album-details/explore-album-details.ts
2026-03-24 15:02:15 -04:00
yonlu 67e1917f5a feat(S04/T01): Added DiscNumber field to MBTrack (populated from Medium…
- backend/explore/types.go
- backend/explore/musicbrainz.go
- frontend/wailsjs/go/explore/Service.d.ts
- frontend/index.ts
2026-03-24 14:29:01 -04:00
yonlu bb271e86f5 feat(S03/T02): Add similar artists horizontal scroll section with click…
- frontend/src/components/explore-artist-details/explore-artist-details.ts
2026-03-24 13:43:22 -04:00
yonlu 6ee16c7a87 feat(S03/T01): Add explore-artist-details Lit component with artist hea…
- frontend/src/components/explore-artist-details/explore-artist-details.ts
- frontend/index.ts
2026-03-23 23:33:27 -04:00
yonlu 5451e70a0c chore(M004/S02): auto-commit after complete-slice 2026-03-23 17:45:08 -04:00
yonlu 677ed01d89 feat(S02/T01): Added CoverArtGroupURL for release-group cover art, conc…
- backend/explore/coverart.go
- backend/explore/explore.go
- backend/explore/coverart_test.go
- frontend/wailsjs/go/explore/Service.js
- frontend/wailsjs/go/explore/Service.d.ts
2026-03-23 17:31:01 -04:00
yonlu 1ff53d8084 chore(M004/S01): auto-commit after complete-slice 2026-03-23 16:55:22 -04:00
yonlu 3173b78a87 chore(Q1): auto-commit after quick-task 2026-03-22 11:07:17 -04:00
yonlu c7bf5271e6 fix: simplify play count accessor, remove right-align to debug rendering 2026-03-22 10:52:58 -04:00
yonlu 7cde7cc7b8 fix: play count display and live update after playback
- Column accessor shows '0' instead of empty string for unplayed tracks
- recordPlay emits TrackMetadataChanged event after updating DB
- Frontend library store invalidates on that event, refreshing play counts
2026-03-22 09:42:18 -04:00
yonlu e1bfe12903 fix: register playCount column in backend AllColumnIDs + rename to 'Play Count'
Backend validation rejected 'playCount' as unknown column ID, silently
preventing it from being enabled or persisted. Added ColPlayCount to
the tracklist package's constant list and AllColumnIDs slice.
2026-03-21 22:53:52 -04:00
yonlu 2ee157a376 feat(M003/S03): play count column + data pipeline
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.
2026-03-21 15:33:08 -04:00
yonlu 9c85cfcc7b feat(M003/S02): smart playlist integration — play_count and days_since_played fields
Backend:
- Added play_count and days_since_played to rule engine field whitelist
- days_since_played uses julianday() expression with COALESCE for NULL handling
- Never-played tracks (NULL last_played) match 'greater_than' but not 'less_than'
- Added PlayCount + LastPlayed to library.Track struct
- Evaluate query selects play_count and last_played from track_metadata
- Added play_count to sort field options

Frontend:
- Added play_count and days_since_played to field and numeric field lists
- Added play_count to sort options

All 49 rule engine + 15 service tests pass unchanged.
2026-03-21 15:26:43 -04:00
yonlu ebaa856e0c fix: align combobox height to operator select via grid stretch
- Grid rows use align-items: stretch so all cells share the select's height
- Combobox host, wrapper, and input all set height: 100% to fill the cell
- Remove button uses align-self: center to stay centered instead of stretching
2026-03-21 13:46:26 -04:00
yonlu 288215a97c fix: match combobox input height to select dropdowns 2026-03-21 13:40:07 -04:00
yonlu 6972953649 fix: smart playlist UX polish — layout, defaults, free-form input, case-insensitive matching
Details view:
- Skip evaluation on new playlists (was returning all 25k tracks)
- Go straight to editor on auto-edit instead of awaiting loadTracks

Editor:
- Default sort to Random instead of None, remove None option
- Hide sort direction button when sort is Random
- Fixed-width field (160px) and operator (140px) columns, value fills remaining space
- Preview fills remaining vertical space (flex layout) instead of fixed 200px max-height
- Preview scrolls independently while rules/options stay pinned

Combobox:
- Accept free-form text on blur and Enter — no longer requires selection from dropdown
- Enables typing values like 'indie' that may not be exact DB entries

Backend:
- Case-insensitive text matching: COLLATE NOCASE on is/is_not/is_any_of operators
- Applies to both regular fields and genre subqueries
- LIKE operators were already case-insensitive (SQLite default)
2026-03-21 13:25:21 -04:00
yonlu 477b7ff6a2 feat(M002): smart playlists — rule engine, editor UI, sidebar integration
Recovered from orphaned worktree commits (complete-milestone failed to merge).

Backend:
- Migration 9: is_smart + smart_rules columns on playlists table
- smartplaylist package: parameterized WHERE clause builder, field whitelist, genre subquery
- playlist.Service: Create/Update/Evaluate/Preview/GetRules smart playlist methods
- 65 tests (49 rule engine + 15 service + 1 migration)

Frontend:
- yj-combobox: reusable typeable dropdown with keyboard nav, ARIA, blur-race fix
- smart-playlist-editor: row-based rule builder with live preview
- smart-playlist-details: evaluate, refresh, play, shuffle, edit rules
- Sidebar: filter icon, Smart badge, create button, routing
- Queue snapshot on play/shuffle
2026-03-21 12:53:00 -04:00
yonlu d430ad884b fix(18-02): add field labels to all track-details states (single/batch, read/edit) 2026-03-18 13:40:11 -04:00
yonlu 9df2d6764a fix(18-02): add field labels above title/artist/album inputs in batch edit mode 2026-03-18 13:32:08 -04:00
yonlu 656985add9 feat(18-02): wire batch track-details to all 4 view context menus
- track-list: branch on selection count, resolve tracks from this.tracks
- cover-grid: branch on selection count, resolve from expandedTracks
- queue-panel: branch on indices count, resolve via libraryStore.getCachedTracks
- playlist-details: branch on selection count, resolve via libraryStore
- Each view determines coverArt/coverArtMixed state and calls showBatch()
2026-03-18 13:14:02 -04:00
yonlu 6dab32b36b feat(18-02): add batch edit mode to track-details component
- showBatch() API for multi-track entry with merged field values
- Three-state field model: keep/set/clear via dirty tracking in editValues
- Confirmation dialog showing field changes before batch save
- Progress bar with live counter wired to BatchWriteProgress events
- Cancel button calling CancelBatchWrite during batch write
- Results view with success/failure counts and expandable failure details
- Batch cover art: pick, preview, or clear for all selected tracks
- Post-save data refresh returning to updated summary view
- Single-track show() path unchanged (batchMode = false)
2026-03-18 13:11:36 -04:00
yonlu f557ffd652 feat(18-01): add BatchWriteTrackTags with progress, cancellation, and partial failure
- 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
2026-03-18 13:02:30 -04:00