Commit Graph
118 Commits
Author SHA1 Message Date
yonlu 75b2a349eb fix(10-01): move library_id index to migration 6 to fix existing DB startup
On existing databases, CREATE TABLE IF NOT EXISTS audio_files is a no-op
but the standalone CREATE INDEX on library_id would fail because the
column doesn't exist until migration 6 runs. The migration already
creates this index, so removing it from the schema file is correct.
2026-03-09 10:48:11 -04:00
yonlu bc151891b5 feat(10-02): add migration 6 integration tests and NewTestDBWithLibrary helper
- Add NewTestDBWithLibrary helper for tests needing a pre-created library
- TestMigration6FreshDB: verify all tables, columns, phantom cols, VIEW, and user_version
- TestMigration6LibraryQueries: verify CRUD operations and unique path constraint
- TestMigration6PhantomPlaylistTracks: verify SET NULL FK preserves phantom metadata
- TestMigration6AudioFilesLibraryFK: verify FK enforcement on library_id
- TestMigration6TrackMetadataViewHasLibraryID: verify VIEW includes library_id
2026-03-09 09:50:14 -04:00
yonlu 02548dd55e feat(10-02): add sqlc queries for libraries and update playlist queries for phantom support
- Create libraries.sql with 7 CRUD queries (create, get, get-by-path, list, update, delete, count)
- Update playlists.sql: AddPlaylistTrack now accepts 9 params including phantom metadata
- Update playlist queries to use LEFT JOIN for nullable audio_file_id
- Add GetTrackPhantomMetadata helper query for eager phantom population
- Add is_phantom computed column to metadata queries
- Add GetAudioFilesByLibrary and CountAudioFilesByLibrary queries
- Regenerate all sqlc code
2026-03-09 09:47:30 -04:00
yonlu 1179f56c36 feat(10-01): implement migration 6 and pre-migration backup
- Add backupDatabase() for timestamped .db file backup before migration
- Add migration6MultiLibrary() with all 14 steps: FK OFF, create libraries table,
  insert default library from TOML config, add library_id to audio_files, rebuild
  playlist_tracks with SET NULL FK and 6 phantom columns, backfill phantom metadata,
  recreate track_metadata VIEW with library_id, FK ON, clean TOML config
- Add readLibraryDirFromTOML() and removeLibraryDirFromTOML() helpers
- Update runMigrations signature to accept dbPath for backup
- Add sentinel library row in NewTestDB for FK constraint satisfaction
2026-03-09 09:41:08 -04:00
yonlu 535855b383 feat(10-01): update SQL schema files for multi-library fresh installs
- Create _libraries.sql with libraries table (name, path, created_at)
- Add library_id FK column and index to audio_files.sql
- Update playlist_tracks.sql with nullable audio_file_id, SET NULL FK, and 6 phantom columns
- Add af.library_id to track_metadata VIEW
- Regenerate sqlc code for updated schemas
- Fix playlist.go to use sql.NullInt64 for nullable audio_file_id
2026-03-09 09:36:47 -04:00
yonlu bb3fd204f0 fix(09-05): emit VolumeChanged event and persist state in ChangeVolume and MuteToggle 2026-03-07 02:06:08 -05:00
yonlu 6285ca9dc4 feat(09-02): add shortcuts config package with default bindings and Wails persistence
- 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)
2026-03-06 21:46:09 -05:00
yonlu cf22e52a64 feat(09-01): add scan control fields and per-scan cancellable context
- Add scanActive, scanCancel, scanPaused, scanPauseCh fields to Library struct
- Create scan_control.go with CancelScan, PauseScan, ResumeScan, IsScanActive, IsScanPaused
- Thread per-scan scanCtx through walk and worker pipeline
- Add waitIfPaused checkpoint before each worker extraction
- Skip orphan cleanup and variant generation on cancelled scan
- Emit LibraryScanCancelled instead of LibraryScanComplete when cancelled
2026-03-06 21:30:49 -05:00
yonlu c695024241 feat(09-01): add scan control events and cancelled metrics field
- Add LibraryScanCancelled, LibraryScanPaused, LibraryScanResumed events
- Regenerate frontend/src/events.ts via go generate
- Add Cancelled bool field to ScanMetrics struct
2026-03-06 21:28:37 -05:00
logan 2f9d9f8508 fix: recover from go-mp3 seek panic on startup (#86)
The go-mp3 library (v0.3.4) has a bug where Seek panics with a slice
bounds error for certain byte positions. This crashes the app when
restoring a saved playback position on startup.

Add bounds clamping and a recover wrapper around the seek call in
seekLocked to convert the panic into a graceful error. When the seek
fails, playback starts from the beginning instead of crashing.
2026-03-06 00:08:11 -06:00
yonlu b725b801c1 fix CI: skip metadata tests when test_data is absent, use fmt.Fprintf in genevents 2026-03-05 19:11:56 -05:00
yonlu 8a0b16a4ec feat(quick-15): insert BufferedStreamer into player pipeline and increase speaker buffer
- BufferedStreamer wraps resampled streamer with 2s read-ahead buffer
- Speaker buffer increased from 100ms to 200ms for secondary protection
- Close old BufferedStreamer on track change and unload to prevent goroutine leaks
- Streamer chain: decode -> resample -> BufferedStreamer -> ctrl -> volume -> speaker
2026-03-05 15:28:28 -05:00
yonlu 85b23acb24 feat(quick-15): add BufferedStreamer with goroutine read-ahead
- Ring-buffer streamer decouples source I/O from speaker callback
- Read-ahead goroutine pre-fills buffer in 512-sample chunks
- Returns silence when buffer temporarily empty (prevents glitches)
- Close() signals goroutine shutdown via channel
- 5 unit tests: basic stream, small reads, drain, silence, close
2026-03-05 15:27:24 -05:00
yonlu 2820de2510 fix(quick-14): add roll-back-on-failure to queue index advancement
- Next() rolls back currentIndex and skips emitIndexChanged on load failure
- Previous() applies same pattern to all three branches (RepeatOne, restart, navigate)
- OnPlaybackFinished() rolls back currentIndex on playCurrentTrack failure
- PlayIndex() and playFromStart() also guard against load failures
- RepeatOne paths guard emitIndexChanged with the bool return value
2026-03-05 15:03:29 -05:00
yonlu 6eeddda976 refactor(quick-14): make playOrLoadCurrentTrack and playCurrentTrack return bool
- playCurrentTrack returns false on load failure or play error
- playOrLoadCurrentTrack propagates bool from load/play helpers
- loadCurrentTrack already returned bool — no change needed
2026-03-05 15:02:11 -05:00
yonlu e1a95e65a9 fix(quick-13): resolve lint issues in main source files
- Fix errcheck for db.Close() in testhelper.go
- Fix errcheck, nlreturn, wsl, gofumpt issues in genevents/main.go
- Fix gofumpt and wsl issues in library.go
2026-03-05 14:07:50 -05:00
yonlu 97f256d67f fix: include full track metadata in GetAudioFilesByReleaseGroup query
The GetAudioFilesByReleaseGroup SQL query only selected 6 columns,
missing audio properties (sample_rate, bit_depth, channels, bitrate,
file_size) and metadata (album, genre, year, composer, file_type).
This caused track details opened from the album view to show dashes
instead of actual values. Expanded the query to match
GetAllTracksWithFullMetadata and updated GetAlbumTracks to use the
shared mapTrackRow helper.
2026-03-05 13:34:03 -05:00
yonlu a28b4d1e06 feat: add scan progress bar with phase indicator
Add live progress reporting during library scans:

- Pre-walk count: fast WalkDir to count audio files upfront for
  percentage calculation (~1-2s overhead)
- Progress ticker: emits ScanProgress events every 300ms with
  phase, file counts (added/skipped/updated), and total
- Phase labels: counting → scanning → thumbnails → orphans
- Frontend: progress bar with percentage, file counts breakdown,
  and phase indicator in both config-page and library-manager

Replaces the static 'Scanning...' text with a live progress bar
showing e.g. '62% — Scanning... 1,247 / 2,013 files (891 new,
356 skipped)'
2026-03-05 11:21:53 -05:00
yonlu 8e9a616037 fix: drop+recreate contentless FTS5 index instead of DELETE
The search_index is a contentless FTS5 table (content=''), which
SQLite does not support DELETE on. ClearSearchIndex now drops and
recreates the virtual table. Single-row DeleteSearchIndex becomes
a no-op since contentless FTS5 also cannot delete individual rows;
stale entries are harmless (search JOINs filter them out) and the
index is fully rebuilt during FullRescan.
2026-03-05 10:41:55 -05:00
yonlu d43ba7bd0c fix(quick-10): add migration 5 and fix entity cache for composite album key
- Migration 5 rebuilds release_groups with UNIQUE(name, album_artist_credit_id)
- Drops and recreates track_metadata VIEW during table rebuild
- Temporarily disables FK checks for safe table rebuild
- Entity cache now keys by album name + artist credit ID
- Update tests to use composite cache keys
2026-03-05 10:25:53 -05:00
yonlu 999ab967be fix(quick-10): update release_groups schema and queries for composite uniqueness
- Change UNIQUE(name) to UNIQUE(name, album_artist_credit_id) in schema
- Update UpsertReleaseGroup ON CONFLICT to match composite key
- Rename GetReleaseGroupByName to GetReleaseGroupByNameAndArtist with two params
- Regenerate sqlc code
2026-03-05 10:23:25 -05:00
yonlu ced58fe6a9 perf(07-01): eliminate redundant lookups in SetQueue Phase 2
- Add phase1Meta parameter to resolveRemainingTracks
- Filter out already-resolved paths before lookupTrackMetaBatch call
- Merge Phase 1 results into Phase 2 lookup map
- Pass batchMeta from SetQueue call site to resolveRemainingTracks
- For 1000-track queue with Phase 1 resolving 50, Phase 2 now queries 950 instead of 1000
2026-03-04 20:58:22 -05:00
yonlu cdd17db275 perf(07-01): add incremental persistence helpers for queue mutations
- Add persistAddTrack/persistAddTracks for O(1) append operations
- Add persistInsertTracks with variable-N position shift for insert-at operations
- Add persistRemoveTrack with single DELETE + position shift
- Wire AddTrack/AddTracks to use incremental INSERT (no full table rewrite)
- Wire InsertNext/InsertNextTracks/InsertTracksAt to use position shift + INSERT
- Wire RemoveTrack to use single DELETE + shift (no full table rewrite)
- RemoveTracks keeps full persistTracks rewrite (bulk operation per design)
- Preserve shuffle order regeneration in all mutation paths
2026-03-04 20:57:47 -05:00
yonlu 7dfe003e63 docs(06-03): add SAFETY comments to all 12 hand-crafted SQL statements
- 7 SAFETY comments in search.go (FTS5 MATCH/INSERT/DELETE operations)
- 3 SAFETY comments in library.go (FTS5 INSERT/DELETE in commitNewAudioFile, updateAudioFileMetadata)
- 1 SAFETY comment in rescan.go (FTS5 DELETE in clearAllLibraryData)
- 1 SAFETY comment in persistence.go (variable-count multi-row INSERT)
- Cross-references link library.go/rescan.go back to search.go
- Two-part format: why sqlc can't handle it + what makes it safe
2026-03-04 19:33:55 -05:00
yonlu 2221a68459 feat(06-03): migrate lookupChunk to sqlc-generated LookupTrackMetaByPaths query
- Add LookupTrackMetaByPaths sqlc query using track_metadata VIEW with sqlc.slice()
- Replace hand-crafted fmt.Sprintf IN clause in lookupChunk with sqlc-generated call
- Preserve lookupTrackMetaBatch chunking at maxSQLiteVars (900)
- All queue tests pass with -race
2026-03-04 19:31:52 -05:00
yonlu 9159b409dc refactor(06-01): consolidate search queries to use track_metadata VIEW
- SearchFTS uses JOIN track_metadata instead of 5-table inline JOIN
- SearchFTSByFilename uses JOIN track_metadata instead of 5-table inline JOIN
- SearchFTSTracks uses JOIN track_metadata instead of 6-table inline JOIN
- RebuildSearchIndex selects from track_metadata instead of inline JOIN
- All 15 database tests pass with -race
2026-03-04 19:23:11 -05:00
yonlu 3e9edd05e8 feat(06-02): create Go→TypeScript event constant codegen tool
- Add backend/events/cmd/genevents/main.go using go/ast for deterministic output
- Add //go:generate directive to backend/events/events.go
- Generate frontend/src/events.ts with all 21 constants including LibraryConfigChanged
- Atomic file writes via temp file + rename
- Comment groups preserved with trailing period stripping
2026-03-04 19:22:11 -05:00
yonlu 9c7e5a9634 feat(06-01): create track_metadata VIEW schema and migration 4
- Add track_metadata_view.sql for sqlc VIEW awareness
- Add migration4TrackMetadataView for existing databases
- sqlc generate produces TrackMetadatum model from VIEW
- migration2 inline JOIN preserved (runs before VIEW exists)
2026-03-04 19:21:58 -05:00
yonlu dd34569ac0 test(05-01): add FTS5 search tests for database package
- seedSearchData helper creates full entity graph (7 tracks, 4 artists, 7 albums)
- Pure helper tests: tokeniseForFTS, buildFTSQuery, stripExtForSearch
- FTS5 search tests: basic term, empty query, special characters (AC/DC),
  multi-word, diacritics (Beyonce→Beyoncé), ranking, filename search
- SearchFTSTracks verifies all 16 columns populated
- Index ops: insert, delete (documents contentless FTS5 limitation),
  rebuild, clear (documents contentless limitation)
- Migration test: user_version >= 3, UNIQUE index enforcement
- 15 tests, all passing with -race
2026-03-04 16:43:06 -05:00
yonlu fa6c378e25 test(05-02): add entity cache and orphan cleanup tests with DB backing
- TestCachedUpsertArtistCredit: cache hit returns same ID on second call
- TestCachedLinkArtist: skips duplicate INSERT via linkedCredits cache
- TestCachedLinkArtist_MultiCredit: same artist linked to different credits
- TestCachedUpsertGenre: genre cache returns same ID on repeated calls
- TestResolveReleaseGroup: creates release group, updates cover art on cache hit
- TestResolveReleaseGroup_CacheHit: pre-populated cache returns cached ID
- TestOrphanDeletion: DeleteAudioFile removes row, documents contentless FTS5 limitation
- TestEntityCache_EmptyFields: empty artist name, empty album, AlbumArtist reuse
2026-03-04 16:37:48 -05:00
yonlu 6f96a9411f test(05-02): add pure helper tests for scan utility functions
- TestGetRecordingName: title present, fallback to filename sans extension
- TestToNullInt64: zero as null, positive/negative as valid
- TestToNullString: empty as null, non-empty as valid
- TestSplitGenres: empty/single/multiple genre splitting on || delimiter
- TestMapTrackRow: all 16 columns including string TrackLength, NullInt64 fields
2026-03-04 16:35:00 -05:00
yonlu 294b629877 test(04-02): add player volume conversion and state mapping tests
- ToVolume/ToUserVolume at all boundary values (0, 25, 50, 75, 100)
- Out-of-range behavior for both conversion directions
- Full roundtrip characterization (0-100) with ±1 tolerance
- clampVolume at and beyond both boundaries
- stateToMediaControls for all states including unknown fallback
2026-03-03 17:01:40 -05:00
yonlu 77cc993fec test(04-01): add queue persistence round-trip tests
- Full roundtrip: SaveState/RestoreState preserves all fields (tracks, index, shuffle, repeat, shuffleOrder)
- Edge cases: empty queue, single track, no prior save state
- Track order: 10-track order preservation verification
- Overwrite: second SaveState replaces first
- 6 tests all passing with -race
2026-03-03 17:00:38 -05:00
yonlu f9b2ad95b7 test(04-02): add config and sub-config validation tests
- Theme: hex color regex + background shade enum validation
- Tracklist: column ID recognition + duplicate detection
- Favorites: icon style enum validation
- Library: directory existence + scan concurrency mode validation
- Config: load/save roundtrip, missing file handling, composed errors, defaults
2026-03-03 17:00:12 -05:00
yonlu 8d60dc05ce test(04-01): add queue core operations and navigation tests
- Queue operations: SetQueue, AddTrack, InsertTracksAt, MoveQueueTracks, RemoveTrack, Clear, ToggleShuffle, CycleRepeat
- Navigation: nextIndex/previousIndex in all repeat modes (off/all/one)
- Shuffle: generateShuffleOrder properties (no duplicates, current at [0]), shuffle navigation
- Mock TrackLoader and seedAudioFiles helper for DB-backed tests
- 23 tests total (14 core + 9 navigation) all passing with -race
2026-03-03 16:59:45 -05:00
yonlu bae9d70d23 feat(03-01): create NewTestDB helper for in-memory SQLite test databases
- Add testhelper.go with exported NewTestDB(t *testing.T) *DB
- Opens :memory: SQLite with same connection params as production
- Shares applyPRAGMAs, schema application loop, and runMigrations
- Registers t.Cleanup for automatic DB close
- No orphan cleanup, no functional options, no error return
2026-03-02 22:05:39 -05:00
yonlu d34881530a feat(03-01): extract shared applyPRAGMAs and add production PRAGMAs to NewDB
- Extract inline foreign_keys PRAGMA into shared applyPRAGMAs function
- Add synchronous=NORMAL, cache_size=-8000, mmap_size=67108864 PRAGMAs
- NewDB now calls applyPRAGMAs instead of inline PRAGMA exec
- applyPRAGMAs will be reused by NewTestDB for production-mirroring tests
2026-03-02 22:04:49 -05:00
yonlu e6866ded9d feat(02-02): add ScanWarning type and reclassify scan errors as warnings
- Add ScanWarning struct with FilePath/Phase/Err to ScanMetrics
- Add mutex-protected addWarning method for concurrent use
- Reclassify walk, extraction, commit, orphan, variant, FTS failures as warnings
- Update commitBatch to return only fatal tx.Commit errors
- Update cachedLinkArtist to check errors via database.IsUniqueViolation
- Update handleConfigUpdate to capture and log scan warning count
2026-03-02 19:18:11 -05:00
yonlu 0860b2fd4b fix(02-01): log MPRIS callback errors instead of discarding them
- OnPause, OnPlayPause, OnStop, OnSeek now check errors and log at Warn level
- All four MPRIS closures use yj.logger.Warn for non-fatal error reporting
- No more silently discarded player.Pause() / player.Seek() errors
2026-03-02 18:39:54 -05:00
yonlu 2a86408201 fix(02-01): eliminate package-level startupErr and fix config file permissions
- Move startupErr from package-level var to YellowJacketApp struct field
- Update OnStartup and OnDomReady to reference yj.startupErr
- Change config.Save() file permissions from 0o666 to 0o644
- Fix nlreturn lint in database/errors.go (pre-existing, blocking commit hook)
2026-03-02 18:39:15 -05:00
yonlu 83de934c39 feat(quick-8): add FindDuplicateTracksInPlaylist backend method
- Add DuplicateTrackInfo and DuplicateCheckResult types
- Implement FindDuplicateTracksInPlaylist on playlist Service
- Regenerate Wails TypeScript bindings
2026-03-01 11:12:04 -05:00
yonlu 6e123bd47f feat(quick-7): add PinDefault config field with backend getter/setter
- Add PinDefault bool to favorites.Config struct with TOML tag
- Add GetPinDefaultPlaylist() and SetPinDefaultPlaylist() methods
- Include PinDefault in emitFavoritesChanged event payload
- Default to true (pinned) for new config installations
2026-03-01 10:05:22 -05:00
yonlu bdaff478e8 feat(quick-5): add CreatedAt/UpdatedAt to playlist Summary struct
- 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
2026-03-01 08:51:00 -05:00
yonlu 8ba8bbe7be feat(quick-002): add uniquePlaylistName helper and wire into ImportPlaylist
- Add uniquePlaylistName method to auto-rename duplicate names with (1), (2), etc.
- Call uniquePlaylistName only from ImportPlaylist, not CreatePlaylist
2026-02-28 13:53:21 -05:00
yonlu 04b2088b28 feat(quick-002): add CountPlaylistsByName SQL query and regenerate sqlc
- Add CountPlaylistsByName :one query to playlists.sql
- Regenerate sqlc to produce Go function
2026-02-28 13:52:21 -05:00
yonlu c34e4ad029 feat(quick-001): add multi-file picker and batch import support
- Update PlaylistFilePicker to return []string via OpenMultipleFilesDialog
- Add ImportPlaylists method for sequential batch import with partial success
2026-02-28 13:30:36 -05:00
yonlu 3abaeba3af fix(01-01): collapse Player.SetContext double-lock into single acquisition
- Replaced two separate Lock/Unlock pairs with single Lock/defer Unlock
- Both p.ctx assignment and p.restoreStateLocked() now run under same lock hold
- Prevents observing partially-initialized state between the two operations
2026-02-28 12:09:34 -05:00
yonlu daaa6b7f97 fix(01-01): add mutex protection to Queue, Library, and Playlist SetContext methods
- Queue.SetContext acquires existing q.mu before writing q.ctx
- Library struct gets new mu sync.Mutex; SetContext and SetRescanHooks acquire it
- Playlist Service struct gets new mu sync.Mutex; SetContext and SetFavoritesConfig acquire it
- Library and Playlist release lock before calling post-init methods (registerEventHandlers, migrateExistingPlaylists)
2026-02-28 12:09:14 -05:00
yonlu d01f5e5d14 added "default playlist"/ favorites system 2026-02-25 22:53:57 -05:00
yonlu 51da5cfb07 no longer using forked beep 2026-02-25 21:16:29 -05:00