Commit Graph
100 Commits
Author SHA1 Message Date
yonlu b941057a46 feat: extract MusicBrainz IDs from audio tags and store in library DB
Migration 13 adds nullable mbid TEXT columns to artists,
release_groups, and recordings with partial indexes.

Metadata extraction (tags.go) now reads MusicBrainz IDs from Raw()
tags — handles both Vorbis (musicbrainz_artistid) and ID3v2
(MusicBrainz Artist Id) key formats.

Scan pipeline (library.go) updates MBIDs after entity upsert via
raw SQL UPDATE. Only sets mbid if currently NULL (preserves existing
values on rescan).

LibraryMBIDIndex (librarymbid.go) provides:
- CheckMBIDs: batch lookup for 'In Library' badges
- GetArtistMBID: single artist name→MBID lookup
- AllArtistMBIDs: full dump for search index Tier 3

MBIDs will be populated on next library rescan. Existing files
need a rescan to backfill.
2026-03-26 09:24:39 -04:00
yonlu 806de8fd45 feat: migration 13 — add MBID columns to artists, release_groups, recordings
Add nullable TEXT mbid column to artists, release_groups, and
recordings tables. Partial indexes on each (WHERE mbid IS NOT NULL)
for fast MBID lookups without bloating the index for rows without
MBIDs.

Enables linking local library entities to MusicBrainz/ListenBrainz
explore data, artist image sharing, and 'In Library' badges.
2026-03-26 09:17:55 -04:00
yonlu c9b3c86f37 perf: unified per-artist indexing — discography + image in parallel
Restructure indexOneArtist to run LB discography fetches and MB
artist image resolution concurrently. They use different rate
limiters (LB: 3 req/s, MB: 1 req/s) so they overlap without
contention.

Per artist, the indexer now runs two parallel pipelines:
  LB pipeline: top-release-groups + top-recordings
  MB pipeline: url-rels → Wikidata P18 → Wikimedia image fetch

All artist images are pre-cached during the index build instead
of being resolved on-demand during search. Total build time
drops from ~105 min (sequential) to ~63 min (parallel, MB-bound).

SearchIndex now takes ArtistImageProvider as a dependency. The
Service constructor creates artistImg before the index so both
can share it.
2026-03-26 08:54:43 -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 104e469774 fix: simplify artist image provider, remove unnecessary LookupArtist call
The previous implementation called LookupArtist (rate-limited MB API)
before fetching url-rels, wasting a rate limiter slot. The fetchURL
for rels also bypassed the MB rate limiter, risking 503 rejections.

Rewrite: fetch MB url-rels once (direct HTTP, cached 30 days), parse
both image and wikidata relations from the same response, resolve
Wikimedia thumb URL. No dependency on MusicBrainzClient — just the
Cache for storage and a plain http.Client.

Also cleared 34 stale cached empty results from previous failed
resolution attempts that were blocking image lookup.
2026-03-25 20:13:23 -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 d5f34f242f fix: don't cache transient cover art failures as permanent misses
The CAA proxy was caching all fetch failures (including 503s and
timeouts) as empty files, treating them as permanent 'no art' misses.
During Internet Archive outages, this meant every album got cached
as having no cover art, and the cache persisted after IA recovered.

Now only 404 responses (no cover art exists) are cached as permanent
misses. 503, timeouts, and other transient errors are not cached,
so the next request retries the fetch.

Also cleared 33 incorrectly cached 0-byte miss files from a
concurrent IA outage.
2026-03-25 14:14:30 -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 1141febf29 perf: skip live LB popularity + cross-reference when index is ready
Phases 2 (3 LB popularity POST calls) and 3 (3 MB discography
browse calls) were adding ~3-6 seconds to every search through
rate-limited API calls. Now they only run as a fallback during
first launch before the search index is built.

Once the index is ready (after Tier 1, <5 seconds from startup):
  Phase 0: local FTS5 index query (instant)
  Phase 1: MB search (3 concurrent calls, ~1s)
  Phase 4: merge index hits (instant)
  Phase 5: filter and cap (instant)

Search drops from ~4-7s to ~1s. The index already carries
popularity data and covers discography cross-referencing,
making the live API calls redundant.
2026-03-25 13:55:26 -04:00
yonlu e4f4639ab7 feat: per-tier refresh intervals with incremental discography builds
Replace the single 7-day full rebuild with per-tier scheduling:

- Tier 1 (sitewide top lists): weekly refresh, 12 API calls
- Tiers 2-4 (discographies): monthly refresh, incremental —
  only fetches discographies for artists not already indexed

On subsequent runs:
- If T1 is fresh, load cached artists from the index (~0 calls)
- If discographies are fresh, skip Tiers 2-4 entirely (~0 calls)
- If discographies are stale, diff against indexed set and only
  fetch new artists that appeared in the sitewide lists

Add helpers: isMetaFresh (per-key freshness check),
loadCachedSitewideArtists (read artists from existing index),
filterUnindexed (diff artist list against indexed set).

After first build: typical startup is <5s (T1 cache load).
Monthly incremental: ~50-100 calls for newly appeared artists.
2026-03-25 13:43:00 -04:00
yonlu 59d5b7272e feat: popularity-scaled per-artist index budgets
Instead of fixed 20 RGs + 100 recordings for every artist, scale
the budget by popularity using a power curve (exponent 0.3):

  Radiohead (2.5M listens): 20 RGs, 100 recordings
  Hans Zimmer (715K):       15 RGs, 71 recordings
  Clutch (178K):            11 RGs, 50 recordings
  Similar (~10K):            7 RGs, 27 recordings
  Organic (unknown):         5 RGs, 10 recordings

Saves ~53% index size (~29 MB vs ~62 MB) with identical API calls.
The savings come from T4 similar artists (long tail) where full
discographies were wasteful. Top artists still get full coverage.
2026-03-25 13:11:15 -04:00
yonlu 48acede1bc feat: 5-tier search index with library + similar artist expansion
Rewrite SearchIndex with tiered background build:

Tier 1 — Sitewide instant (<5s, 12 calls): top artists, recordings,
  and release groups across 4 time ranges. Searchable immediately.

Tier 2 — Sitewide full discog (~16min, 2881 calls): top 20 RGs +
  top 100 recordings per sitewide artist (~1440 unique artists from
  all_time/this_year/this_month/this_week union).

Tier 3 — Library artists (~4min, 664 calls): match local library
  artist names against known MBIDs, index their full discographies.
  Catches the user's personal taste that sitewide misses.

Tier 4 — Similar artists (~24min, ~4300 calls): fetch similar
  artists from LB labs for each library artist, index their
  discographies. Fans out into the user's taste neighborhood.

Tier 5 — Organic growth (0 calls): BrowseReleaseGroups now writes
  to the search index in a background goroutine. Every artist page
  view adds that artist's discography to the index for free.

Other changes:
- indexRGsPerArtist bumped 10→20 (96% vs 88% coverage)
- indexRecsPerArtist bumped 10→100 (track-name searchability)
- indexMinPopularity = 50 (cuts noise from long tails)
- Dedicated 3 req/s rate limiter for indexer
- Labs similar-artists endpoint at labs.api.listenbrainz.org
- Each tier marks index as ready on completion so search improves
  progressively during the ~44min total build
2026-03-25 12:49:20 -04:00
yonlu cf40909313 feat: wire SearchIndex into Search() — Phase 0 index query + merge
Create SearchIndex in NewExploreService, start background build in
SetContext (on app startup). Search() now has 6 phases:

  Phase 0: query local FTS5 index (instant, no API calls)
  Phase 1: concurrent MB search
  Phase 2: LB popularity boost
  Phase 3: cross-reference artist discographies
  Phase 4: merge index hits (prepend new entries, dedup by MBID)
  Phase 5: filter and cap

Index hits for release groups/recordings not already in MB results
are prepended so popular albums surface even when MB search can't
find them. scalePopularity() maps raw listen counts to 0-100 scores
via log scaling for compatibility with the blended score system.
2026-03-25 09:41:55 -04:00
yonlu bba4e1f3de feat: SearchIndex — background build + FTS5 query for popularity index
New SearchIndex struct in searchindex.go:
- Background build fetches top 1000 LB artists, then their top 10
  release groups and top 10 recordings (2001 API calls total)
- Dedicated 3 req/s rate limiter for indexer (LB allows 30/10s)
- Bounded concurrency (3 goroutines) with progress logging
- Batch INSERTs in transactions of 100 rows
- FTS5 query with prefix matching ('for you' → 'for* you*')
- Results sorted by popularity descending
- Skips rebuild if index is < 7 days old
- Marks index ready from existing rows if build fails
- Context cancellation for clean shutdown
2026-03-25 09:39:22 -04:00
yonlu 57a07e96cb feat: migration 12 — explore_index + FTS5 search index schema
Add explore_index table (entity_type, mbid, title, artist_name,
artist_mbid, popularity, extra_json) with a unique index on
(entity_type, mbid). FTS5 virtual table explore_index_fts backed
by the content table with auto-sync triggers for insert/update/delete.
explore_index_meta table tracks build timestamps.
2026-03-25 09:35:35 -04:00
yonlu ba6185adc3 feat: cross-reference search — match query against top artists' discographies
After MB search + popularity reranking, browse the discographies of
the top 3 artists and fuzzy-match the full query against album titles.
Matching albums not already in results are injected at the front.

Fuzzy matching uses substring containment with word-level ratio
(handles 'for you tatsuro' → 'FOR YOU' at 0.667) and word overlap
as fallback. Threshold: 0.4 ratio.

Example: 'for you tatsuro' now finds FOR YOU by 山下達郎 even though
MB text search treats 'for' and 'you' as stop words and never
returns it. The album is found via Yamashita's cached discography.
2026-03-24 22:53:47 -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 1e95ddee68 feat: score threshold + server-side result caps
Drop artists and recordings with blended score < 25 after popularity
reranking. Cap each entity slice to 15 server-side. Request 20 from
MB to allow filtering headroom. Reduces payload size and noise.
2026-03-24 22:42:54 -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 176ac26f91 feat: popularity-boosted search reranking via ListenBrainz
After MB search returns text-relevance-scored results, fetch bulk
popularity data from ListenBrainz (POST /1/popularity/{artist,
recording,release-group}) for all result MBIDs. Blend scores:

  final = 0.6 * mb_relevance + 0.4 * log10_popularity

Log-scale normalization ensures massive artists don't drown out
everything, but popular results rise above obscure exact matches.
Release groups (no MB score) sort by raw popularity.

Three LB POST calls run concurrently — each hits a different
endpoint. All are rate-limited and cached (24h TTL).

Example: searching 'tatsuro' now ranks Tatsuro Yamashita (2.5M LB
listens, score 97) above 'tatsuro' vocaloid producer (4 listens,
score 64) despite the latter being an exact name match on MB.
2026-03-24 22:07:16 -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 eabcf8395e fix: unmarshal LB top recordings from snake_case wire format, cap at 10
The ListenBrainz popularity API returns snake_case JSON fields
(recording_name, artist_name, total_listen_count, recording_mbid)
but LBTopRecording used camelCase JSON tags for Wails serialization.
All fields silently deserialized as zero values — empty strings and
zero counts — producing ~8000 blank rows in the top tracks section.

Fix: add lbTopRecordingWire with snake_case tags for API unmarshal,
convert to LBTopRecording (camelCase) for Wails. Cap results at 10
to avoid rendering thousands of rows for prolific artists.
2026-03-24 19:35:34 -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 6cc9acbc99 perf(S01/T02): Add MusicBrainz search/lookup/browse client, ListenBrain…
- backend/explore/types.go
- backend/explore/musicbrainz.go
- backend/explore/listenbrainz.go
- backend/explore/coverart.go
- backend/explore/coverart_test.go
2026-03-23 14:03:51 -04:00
yonlu 8fc075c24a perf(S01/T01): Add token-bucket rate limiter (1 req/sec), SQLite respon…
- backend/explore/ratelimiter.go
- backend/explore/cache.go
- backend/database/sql/schemas/explore_cache.sql
- backend/database/database.go
2026-03-23 07:55:25 -04:00
yonlu 398fd5aaae chore(M004/S01): auto-commit after state-rebuild 2026-03-22 23:25:58 -04:00
yonlu 916b5ef753 fix: add missing playCount and lastPlayed args to mapTrackRow test calls
The mapTrackRow signature was extended with playCount and lastPlayed
fields in the play history feature, but the scan_test.go callers
were not updated, breaking go vet and golangci-lint.
2026-03-22 11:10:59 -04:00
yonlu f7ca138296 fix: flush BufferedStreamer ring buffer on seek to prevent stale audio
When seeking, the underlying decoder position was updated but the
BufferedStreamer's ring buffer still contained up to 2 seconds of
pre-seek audio data. The speaker would drain this stale buffer
before playing audio from the new position, causing an audible
delay where the old position's audio continued playing.

Add a Flush() method to BufferedStreamer that resets the ring buffer
pointers, and call it in seekLocked() immediately after a successful
seek. This ensures the speaker starts playing from the seeked
position without any stale audio artifact.
2026-03-22 11:10:53 -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 c5b293d410 chore: update sqlcgen models with play_count/last_played fields
Auto-generated by sqlc from updated schema. Adds PlayCount/LastPlayed
to AudioFile and TrackMetadatum structs, and PlayHistory model.
2026-03-22 10:48:35 -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 2db6e09aa3 fix: use sql.NullTime for last_played to handle NULL scan
COALESCE(last_played, '') returned empty string which can't scan into
time.Time. Removed COALESCE, use sql.NullTime instead. Format to string
only when Valid.
2026-03-21 15:40:34 -04:00
yonlu b643aee5a3 fix: mapTrackRow accepts time.Time for LastPlayed (matches sqlcgen Row type) 2026-03-21 15:36:59 -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 9bf2bbab2e feat(M003/S01): play history tracking — schema, migration, recording hook
Migration 10:
- play_history table (audio_file_id FK, played_at DATETIME, CASCADE delete)
- play_count + last_played columns on audio_files (denormalized)
- Recreated track_metadata VIEW with play_count and last_played columns

Play recording:
- queue.recordPlay() inserts play_history row + updates denormalized columns
- Called from OnPlaybackFinished after queue advance completes
- Mutex released before DB write to avoid MaxOpenConns(1) deadlock
- Natural finish only — skip/stop does not count

Tests:
- TestMigration10PlayHistory: schema, columns, VIEW, round-trip verification
- All 49 smart playlist + 15 service + existing DB tests still pass
2026-03-21 15:23:18 -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 e974bd2a22 Merge remote-tracking branch 'origin/main' into wip 2026-03-20 14:58:19 -04:00
yonlu f16157a213 fix(S21/T01): fix all lint warnings and upgrade wsl to wsl_v5
Files:
- .golangci.yml
- backend/events/cmd/genevents/main.go
- backend/fileutil/atomicwrite_test.go
- backend/library/library.go
- backend/player/buffered_streamer_test.go
- backend/player/player.go
- backend/tagwriter/dbsync.go
- backend/tagwriter/mp3_test.go
- backend/tagwriter/ogg.go
- backend/tagwriter/ogg_test.go
- backend/tagwriter/ogg_vorbis.go
- backend/tagwriter/pipeline.go
- backend/tagwriter/tagwriter.go
- backend/tagwriter/wav_test.go
2026-03-20 14:36:18 -04:00
yonlu 2409b3435a docs(phase-20): complete phase execution 2026-03-19 14:07:56 -04:00
yonlu 712fa260dd docs(20-02): complete OGG Vorbis tag writer tests plan 2026-03-19 14:04:52 -04:00
yonlu f13aa7086b test(20-02): add round-trip tests for all OGG requirements
- TestWriteOggTags_TextFields: all 9 text fields round-trip via dhowden/tag (OGG-01)
- TestWriteOggTags_CoverArt: METADATA_BLOCK_PICTURE embed verified (OGG-04)
- TestWriteOggTags_ClearCoverArt: cover art clear via nil (OGG-04)
- TestWriteOggTags_PartialUpdate: non-edited fields preserved (OGG-02)
- TestWriteOggTags_AudioPreservation: audio page data byte-identical (OGG-03)
- TestWriteOggTags_AtomicSafety: corrupt file untouched on failure (OGG-05)
- TestWriteOggTags_RejectNonVorbis: Theora OGG rejected (OGG-03 error path)
- TestWriteOggTags_RejectMultiStream: multi-serial rejected
2026-03-19 14:02:02 -04:00
yonlu 246a991c75 test(20-02): add OGG test fixture builder and CRC32 validation
- createTestOGG builds minimal valid OGG Vorbis file programmatically
- TestOggCRC_KnownVectors validates CRC32 against known vectors and fixture pages
- TestCreateTestOGG_Valid verifies fixture structure and metadata.ExtractTags compatibility
2026-03-19 14:00:47 -04:00
yonlu d289390396 docs(20-01): complete OGG Vorbis tag writer implementation plan
- SUMMARY.md with implementation details and decisions
- STATE.md updated with position, metrics, and key decisions
- ROADMAP.md updated with Phase 20 progress (1/2 plans)
- REQUIREMENTS.md: OGG-01 through OGG-05 marked complete
2026-03-19 13:55:39 -04:00
yonlu 5e98c03634 feat(20-01): implement OGG Vorbis tag writer with custom page parser and CRC32
- Custom OGG page parser/writer with MSB-first CRC32 lookup table (ogg.go)
- Vorbis Comment packet parse/serialize with raw byte preservation (ogg_vorbis.go)
- METADATA_BLOCK_PICTURE base64 encoding for cover art with legacy field stripping
- Multi-stream and non-Vorbis OGG rejection with clear error messages
- Pipeline integration: FormatOGG constant, .ogg in DetectFormat, writeOggTags dispatch
- Lenient-read/strict-write: warn on CRC mismatch, always write correct CRCs
- Page sequence renumbering and crash-safe writes via AtomicWrite
2026-03-19 13:53:36 -04:00
yonlu 3face33a57 docs(20): create phase plan for OGG Vorbis tag writer 2026-03-19 13:20:16 -04:00
yonlu 676eede51e docs(20): research phase domain 2026-03-19 13:15:12 -04:00
yonlu 707d759ced docs(20): capture phase context 2026-03-19 13:06:07 -04:00
yonlu 1f0fe2cc2e docs(phase-19): complete phase execution 2026-03-19 09:03:54 -04:00
yonlu f7077b15df docs(19-02): complete WAV tag writer tests plan
- SUMMARY.md with 7 test results and 2 deviations documented
- STATE.md: Phase 19 complete, all WAV requirements verified
- ROADMAP.md: Phase 19 marked complete
- REQUIREMENTS.md: WAV-06 marked complete
2026-03-19 09:00:29 -04:00
yonlu f11b523afb style(19-02): fix wsl lint warnings in WAV test and writer
- Add blank lines before cuddled expressions in wav_test.go
- Fix cuddled copy expression in wav.go writeRIFF
- All WAV lint issues resolved (remaining are pre-existing in other files)
2026-03-19 08:57:57 -04:00
yonlu 1b28882af4 test(19-02): add round-trip tests for all WAV tag writer requirements
- TestWriteWavTags_TextFields: 9 field round-trip (WAV-01)
- TestWriteWavTags_CoverArt: JPEG embed/read-back (WAV-04)
- TestWriteWavTags_ClearCoverArt: embed then clear (WAV-04)
- TestWriteWavTags_PartialUpdate: 2-of-9 change, 7 preserved (WAV-01)
- TestWriteWavTags_ChunkPreservation: fmt/data/LIST/bext preserved (WAV-02, WAV-03)
- TestWriteWavTags_AtomicSafety: failed write leaves file untouched (WAV-05)
- TestWriteWavTags_RejectsRF64: RF64 rejected with clear error (WAV-02)
- readWavID3Tags uses bogem/id3v2 ParseReader for reliable read-back
2026-03-19 08:53:36 -04:00
yonlu 21fe17212d test(19-02): add WAV test fixture builder and read-back helper
- createTestWAV: builds minimal valid WAV with optional ID3v2 tag
- readWavID3Tags: extracts ID3v2 from RIFF via parseRIFF + dhowden/tag
- createTestWAVWithExtraChunks: adds LIST INFO and bext chunks for preservation tests
- makePCMFmtData: generates 16-byte PCM format data
2026-03-19 08:50:20 -04:00
yonlu 8794584b97 docs(19-01): complete WAV tag writer implementation plan
- SUMMARY.md with task commits, deviations, decisions
- STATE.md updated with position, metrics, decisions
- ROADMAP.md updated with phase 19 progress (1/2 plans)
- REQUIREMENTS.md: WAV-01 through WAV-05 marked complete
2026-03-19 08:47:04 -04:00
yonlu e6610ff15e feat(19-01): implement WAV RIFF parser/writer and writeWavTags
- Add FormatWAV constant and .wav DetectFormat case
- Add FormatWAV dispatch in pipeline WriteTrackTags switch
- Create wav.go with custom RIFF chunk parser/writer
- parseRIFF: lenient read, RF64 rejection, case-insensitive ID3 chunk detection
- writeRIFF: strict write with correct padding and 4GB size check
- writeWavTags: merge existing ID3v2, reuse applyTextChanges/applyCoverArtChanges
- Atomic write via fileutil.AtomicWrite for crash safety
2026-03-19 08:44:38 -04:00
yonlu 8f4c4a0c2b fix(19-01): add album_artist TPE2 mapping to applyTextChanges
- Map FieldAlbumArtist to TPE2 frame via CommonID
- Follows same pattern as FieldComposer → TCOM
- Fixes latent gap in MP3 writing, enables WAV reuse
2026-03-19 08:42:53 -04:00
yonlu d789c0b68e docs(19): create phase plan for WAV tag writer 2026-03-18 20:37:24 -04:00
yonlu c5184fed79 docs(19): research phase domain 2026-03-18 20:33:37 -04:00
yonlu 75e49a839e docs(19): capture phase context 2026-03-18 19:56:59 -04:00
yonlu 5d37eb3a2a docs: create milestone v1.2.1 roadmap (3 phases) 2026-03-18 16:30:15 -04:00
yonlu 6b4adf8690 docs: define milestone v1.2.1 requirements 2026-03-18 15:01:12 -04:00
yonlu 279501f972 docs: complete project research for v1.2.1 Format Parity 2026-03-18 14:33:42 -04:00
yonlu 664de001ed docs: start milestone v1.2.1 Format Parity 2026-03-18 14:20:55 -04:00
yonlu 2256f8f329 chore: complete v1.2 Tag Editing milestone
Archive v1.2 milestone: ROADMAP + REQUIREMENTS + phases to milestones/.
Evolve PROJECT.md with v1.2 validated requirements and key decisions.
Update RETROSPECTIVE.md with v1.2 lessons and cross-milestone trends.
Clean STATE.md for next milestone.
2026-03-18 14:07:28 -04:00
yonlu e37535b115 docs(phase-18): complete phase execution 2026-03-18 13:57:00 -04:00
yonlu 77dec0bceb docs(18-02): complete batch edit UI plan
- SUMMARY.md with task commits, deviations, and self-check
- STATE.md updated: Phase 18 complete, all BATCH requirements fulfilled
- ROADMAP.md updated: 2/2 plans complete for Phase 18
- REQUIREMENTS.md updated: BATCH-02 and BATCH-04 marked complete
2026-03-18 13:53:53 -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 5f21804b20 docs(18-02): update STATE.md for checkpoint pause at task 3 2026-03-18 13:14:53 -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 92f6353fa6 docs(18-01): complete batch write backend plan
- SUMMARY.md with BatchWriteTrackTags implementation details
- STATE.md updated with Phase 18 position and decisions
- ROADMAP.md progress updated (1/2 plans)
- REQUIREMENTS.md: BATCH-01, BATCH-03 marked complete
2026-03-18 13:04:54 -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
yonlu 3dba0e143c feat(18-01): add BatchWriteProgress event constant
- Add BatchWriteProgress to tag writing events const block in events.go
- Regenerate frontend/src/events.ts via genevents codegen tool
2026-03-18 13:00:05 -04:00
yonlu 78f0d89331 docs(18): create phase plan — 2 plans in 2 waves for batch edit 2026-03-18 12:33:27 -04:00
yonlu 33ee843d77 docs(18): create phase plan 2026-03-18 12:30:36 -04:00
yonlu 255b8db375 docs(18): capture phase context 2026-03-18 11:44:31 -04:00
yonlu 4e79e1bb16 docs(phase-17): complete phase execution 2026-03-18 11:25:32 -04:00
yonlu b0976c9217 docs(17-02): complete track details save flow & cover art editing plan
- SUMMARY.md with 5 commits (1 feat + 4 fixes during verification)
- STATE.md updated: Phase 17 complete, decisions added
- ROADMAP.md updated: Phase 17 2/2 Complete
- REQUIREMENTS.md updated: EDIT-02, EDIT-03, EDIT-04 all complete
2026-03-18 11:21:46 -04:00
yonlu 8cd4914842 fix(17-02): refresh cover art URLs after save
After a successful save, re-fetch albums alongside tracks and re-resolve
cover art URLs from the updated album data. Previously the dialog only
refreshed this.track but kept stale this.coverArt URLs pointing to the
old content-hash files, causing the image to revert until reopen.
2026-03-18 10:54:17 -04:00
yonlu d7c2965752 fix(17-02): fix cover art replace and remove
Three issues fixed:

1. asBytes() helper for []interface{} → []byte conversion — same
   float64 deserialization issue as numeric fields. Cover art data
   from the frontend arrives as []interface{} of float64, not []byte.

2. DB sync for cover art — was a placeholder no-op. Now saves image
   to covers cache dir (content-hash dedup + thumbnail generation),
   upserts cover_art row, and updates release_groups.cover_art_id.
   Clear sets cover_art_id to NULL on linked release groups.

3. Frontend ReadFile returns base64 string (Go []byte JSON encoding),
   not number[]. Decode with atob() before creating Uint8Array for
   preview blob URL.
2026-03-18 10:36:17 -04:00
yonlu 900db2e56c fix(17-02): handle float64 numeric values from Wails JSON deserialization
Wails deserializes JSON numbers from JavaScript as float64, not int.
All .(int) type assertions on year, track_number, and disc_number
silently failed. Add asInt() helper, replace all assertions.
2026-03-18 07:46:29 -04:00
yonlu ffcdc41b0d fix(17-02): refresh track-details dialog data after successful save
After saveEdit() succeeds, re-fetch tracks from library store and update
this.track with the fresh data so the dialog shows updated values instead
of the stale snapshot passed via show().
2026-03-18 01:23:34 -04:00
yonlu 265a9ea8ce feat(17-02): implement save flow, cover art editing, and error handling
- 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
2026-03-17 21:20:01 -04:00
yonlu b776f3acb9 docs(17-01): complete backend bridge & frontend plumbing plan
- Created 17-01-SUMMARY.md
- Updated STATE.md with position, decisions, metrics
- Updated ROADMAP.md with plan progress
- Marked EDIT-01 and EDIT-04 complete in REQUIREMENTS.md
2026-03-17 21:07:19 -04:00