diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md index 81e4fb9..868801e 100644 --- a/.planning/PROJECT.md +++ b/.planning/PROJECT.md @@ -44,10 +44,15 @@ The music player works reliably and feels solid. Every interaction is correct, r ### Active +- [ ] Multi-library support — manage multiple library directories with per-library scanning and unified presentation +- [ ] Library filtering — view tracks from all libraries or filter to a specific library +- [ ] Cross-library playlists — playlists can reference tracks from any library +- [ ] Phantom tracks — playlist entries preserved with metadata when a library is removed + +### Deferred (Future Milestones) + - [ ] Tag editing — edit track metadata (title, artist, album, etc.) from within the app -- [ ] Scan cancellation — cancel in-progress library scans - [ ] Smart playlists — auto-generated playlists with simple filter rules (genre, year, play count, etc.) -- [ ] Customizable keyboard shortcuts — configurable key bindings for common player actions - [ ] Gapless playback + crossfade — seamless track transitions with optional crossfade setting - [ ] MusicBrainz browser — read-only catalog browsing (artists, discographies, album editions, track listings) - [ ] Layout customization system — section-based UI customization, components declare size constraints, users configure per-section @@ -55,29 +60,29 @@ The music player works reliably and feels solid. Every interaction is correct, r ### Out of Scope -- Tag writing (track metadata editing) — feature work, not consolidation -- Scan cancellation — feature work, deferred to future milestone +- Separate databases per library — overly complex, defeats unified presentation +- Auto-dedup across libraries — complex matching logic, not table stakes +- User access control per library — desktop app, single user +- Parallel library scanning — SQLite single-writer makes it pointless - Cross-platform media controls (macOS/Windows) — feature work - Database health checking / reconnection — low priority, desktop app context - File decomposition for its own sake — only extract when it enables reuse or fixes problems - ORM or query builder — would fight existing sqlc architecture - Connection pooling for SQLite — meaningless with SetMaxOpenConns(1) -## Current Milestone: v1.1 Features & Extensibility +## Current Milestone: v1.1 Multi-Library Support -**Goal:** Add core missing features and build the foundations for a customizable, extensible music player. +**Goal:** Transform YellowJacket from a single-directory player into a multi-library music manager with unified presentation, per-library scanning, and graceful library lifecycle management. **Target features:** -- Tag editing (track metadata editing from within the app) -- Scan cancellation (cancel in-progress library scans) -- Smart playlists (simple filter rules — genre, year, play count, etc.) -- Customizable keyboard shortcuts (configurable key bindings) -- Gapless playback + crossfade (seamless transitions, optional crossfade) -- MusicBrainz browser (read-only catalog: artists, discographies, album editions) -- Layout customization system (MusicBee-style section-based UI configuration) -- Plugin system (full-access API for UI + backend extensibility) +- Library CRUD (add/rename/remove library directories via UI) +- Per-library scanning (scan individual libraries, sequential coordination) +- Unified presentation (merged "All Libraries" view, optional per-library filtering) +- Cross-library playlists (playlists reference tracks from any library) +- Phantom tracks (playlist entries preserved when library removed) +- Config migration (TOML DirectoryPath -> DB libraries table) -**"Done" criteria:** Core features complete and working. Big features (layout, plugins, MusicBrainz) have working foundations — functional but not necessarily feature-complete. +**"Done" criteria:** Users can manage multiple library directories, scan them independently, view tracks from all or one library, and playlists survive library removal with phantom entries. ## Context @@ -124,4 +129,4 @@ The music player works reliably and feels solid. Every interaction is correct, r | Design tokens via :host scope | Component-level token scope matches Lit's shadow DOM encapsulation | ✓ Good — consistent visual language achieved | --- -*Last updated: 2026-03-06 after v1.1 milestone start* +*Last updated: 2026-03-08 after v1.1 restructure for multi-library support* diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 02610b4..eb330c2 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -5,15 +5,15 @@ ## v1.1 Requirements -Requirements for v1.1 Features & Extensibility milestone. Each maps to roadmap phases. +Requirements for v1.1 Multi-Library Support milestone. Each maps to roadmap phases. -### Scan Cancellation +### Scan Cancellation (Phase 9 — Complete) - [x] **SCAN-01**: User can cancel an in-progress library scan via a cancel button - [x] **SCAN-02**: Cancelled scan stops gracefully without corrupting the database - [x] **SCAN-03**: User can pause a library scan and resume it without re-scanning processed files -### Keyboard Shortcuts +### Keyboard Shortcuts (Phase 9 — Complete) - [x] **KEY-01**: Default keybindings work out of box (play/pause, next/prev, volume, search focus, queue toggle, shuffle, repeat) - [x] **KEY-02**: User can customize all keyboard shortcuts via a visual settings UI @@ -21,63 +21,58 @@ Requirements for v1.1 Features & Extensibility milestone. Each maps to roadmap p - [x] **KEY-04**: Shortcuts are scoped — different bindings apply based on focused component (track list vs player vs global) - [x] **KEY-05**: Shortcuts are disabled when text input has focus (except Escape to blur) -### Tag Editing +### Library Management -- [ ] **TAG-01**: User can edit a single track's metadata (title, artist, album, genre, year, track number) -- [ ] **TAG-02**: User can batch edit multiple selected tracks' shared fields -- [ ] **TAG-03**: Tag changes are written to actual audio files (MP3 via ID3v2, FLAC via Vorbis Comments) -- [ ] **TAG-04**: Database and FTS5 search index update after tag writes without requiring a full rescan -- [ ] **TAG-05**: User can set or replace embedded cover art from an image file -- [ ] **TAG-06**: Tag writes use write-to-temp-then-rename to prevent file corruption -- [ ] **TAG-07**: Tag editing is blocked for currently-playing files (queued for after playback stops) +- [ ] **LIB-01**: User can add a new library directory via a folder picker dialog +- [ ] **LIB-02**: User can rename a library (display name) +- [ ] **LIB-03**: User can remove a library — tracks are deleted from DB, shared entities (artists, albums, genres) are cleaned up only if no other library references them +- [ ] **LIB-04**: Libraries are stored in SQLite (not TOML config) with CRUD through the UI +- [ ] **LIB-05**: Existing single-directory config is migrated seamlessly to the libraries table on first run after upgrade +- [ ] **LIB-06**: Library list is displayed in a management UI (settings or sidebar section) -### Smart Playlists +### Library Scanning -- [ ] **SMRT-01**: User can create a smart playlist with filter rules (genre, year, artist, album, title) -- [ ] **SMRT-02**: Multiple rules combine with AND logic -- [ ] **SMRT-03**: User can set random ordering and result limit ("Random 50 Jazz tracks") -- [ ] **SMRT-04**: Smart playlists appear in the sidebar alongside regular playlists -- [ ] **SMRT-05**: Smart playlist rules are persisted and survive app restart +- [ ] **LSCAN-01**: User can trigger a scan for a specific library (not all-or-nothing) +- [ ] **LSCAN-02**: Scanning is sequential — only one library scans at a time (SQLite single-writer) +- [ ] **LSCAN-03**: Scan progress UI shows which library is being scanned +- [ ] **LSCAN-04**: Existing scan cancellation and pause/resume work per-library +- [ ] **LSCAN-05**: Audio files are associated with their library via `library_id` foreign key -### Gapless Playback +### Unified Presentation -- [ ] **GAP-01**: Tracks transition seamlessly with no audible silence gap (gapless playback) -- [ ] **GAP-02**: Next track is pre-decoded before current track ends -- [ ] **GAP-03**: User can enable/disable crossfade with configurable duration (1-10 seconds) -- [ ] **GAP-04**: Crossfade only applies on auto-advance, not manual skip +- [ ] **VIEW-01**: Default view shows tracks from all libraries merged (unified presentation) +- [ ] **VIEW-02**: User can filter the track list to show only tracks from a specific library +- [ ] **VIEW-03**: Browse views (albums, artists, genres) work across all libraries or filtered to one +- [ ] **VIEW-04**: Search (FTS5) searches across all libraries or respects the active library filter -### MusicBrainz Browser +### Playlists & Queue -- [ ] **MB-01**: User can search for artists by name and view results -- [ ] **MB-02**: User can browse an artist's discography (release groups — albums, EPs, singles) -- [ ] **MB-03**: User can view tracks on a specific release -- [ ] **MB-04**: User can view different editions of a release group (pressings, reissues) -- [ ] **MB-05**: API responses are cached in SQLite (24hr for searches, 7 days for entities) -- [ ] **MB-06**: Album cover art is displayed from the Cover Art Archive -- [ ] **MB-07**: Rate limiting (1 req/sec) is enforced with proper User-Agent header +- [ ] **PLAY-01**: Playlists can contain tracks from multiple libraries (cross-library playlists) +- [ ] **PLAY-02**: When a library is removed, playlist entries for that library's tracks become phantom tracks (preserved with cached metadata, not cascade-deleted) +- [ ] **PLAY-03**: Phantom tracks are visually distinguished in playlist views (e.g., greyed out, icon indicator) +- [ ] **PLAY-04**: Queue tracks from a removed library are cascade-deleted (queue is ephemeral) -### Layout Customization +### Data Integrity -- [ ] **LAYOUT-01**: User can resize sidebar and queue panels via drag handles -- [ ] **LAYOUT-02**: Panel sizes persist across app restarts -- [ ] **LAYOUT-03**: User can show/hide sidebar sections and queue panel -- [ ] **LAYOUT-04**: User can choose which component is displayed in each layout section (MusicBee-style) -- [ ] **LAYOUT-05**: Components declare size constraints (min/max dimensions, aspect ratio compatibility) -- [ ] **LAYOUT-06**: Layout presets available (Compact, Full, Mini player) with quick switch - -### Plugin System - -- [ ] **PLUG-01**: Plugin API is defined — plugins can access events, player state, queue, library data -- [ ] **PLUG-02**: JS/TS plugin bundles are loaded from user plugin directory at runtime -- [ ] **PLUG-03**: Plugins can register UI components into the layout system -- [ ] **PLUG-04**: Plugin manifest file defines name, version, permissions, hooks, and UI components -- [ ] **PLUG-05**: Plugins can have their own persistent configuration -- [ ] **PLUG-06**: One example plugin ships demonstrating the API +- [ ] **DATA-01**: Schema migration adds `libraries` table and `library_id` FK on `audio_files` +- [ ] **DATA-02**: Orphan cleanup after library removal: reference-counting bottom-up deletes for artists, albums, genres only referenced by removed library's tracks +- [ ] **DATA-03**: FTS5 index entries for removed tracks are cleaned up (handling contentless table limitations) +- [ ] **DATA-04**: All library operations are transactional — no partial state on failure ## Future Requirements Deferred to future milestones. Tracked but not in current roadmap. +### Tag Editing (Deferred from v1.1) + +- **TAG-01**: User can edit a single track's metadata (title, artist, album, genre, year, track number) +- **TAG-02**: User can batch edit multiple selected tracks' shared fields +- **TAG-03**: Tag changes are written to actual audio files (MP3 via ID3v2, FLAC via Vorbis Comments) +- **TAG-04**: Database and FTS5 search index update after tag writes without requiring a full rescan +- **TAG-05**: User can set or replace embedded cover art from an image file +- **TAG-06**: Tag writes use write-to-temp-then-rename to prevent file corruption +- **TAG-07**: Tag editing is blocked for currently-playing files (queued for after playback stops) + ### Tag Editing (v2+) - **TAG-F01**: Undo/redo for tag edits @@ -85,6 +80,14 @@ Deferred to future milestones. Tracked but not in current roadmap. - **TAG-F03**: Filename-to-tag inference (parse "Artist - Title.mp3" patterns) - **TAG-F04**: Tag-to-filename rename based on template +### Smart Playlists (Deferred from v1.1) + +- **SMRT-01**: User can create a smart playlist with filter rules (genre, year, artist, album, title) +- **SMRT-02**: Multiple rules combine with AND logic +- **SMRT-03**: User can set random ordering and result limit ("Random 50 Jazz tracks") +- **SMRT-04**: Smart playlists appear in the sidebar alongside regular playlists +- **SMRT-05**: Smart playlist rules are persisted and survive app restart + ### Smart Playlists (v2+) - **SMRT-F01**: Play count tracking for smart playlist rules @@ -93,21 +96,51 @@ Deferred to future milestones. Tracked but not in current roadmap. - **SMRT-F04**: Sort order control in rule definition - **SMRT-F05**: Auto-update smart playlists on library changes +### Gapless Playback (Deferred from v1.1) + +- **GAP-01**: Tracks transition seamlessly with no audible silence gap (gapless playback) +- **GAP-02**: Next track is pre-decoded before current track ends +- **GAP-03**: User can enable/disable crossfade with configurable duration (1-10 seconds) +- **GAP-04**: Crossfade only applies on auto-advance, not manual skip + ### Gapless Playback (v2+) - **GAP-F01**: Per-album gapless (disable crossfade within albums) - **GAP-F02**: ReplayGain normalization - **GAP-F03**: Fade-in on play, fade-out on pause -### MusicBrainz Browser (v2+) +### MusicBrainz Browser (Deferred from v1.1) -- **MB-F01**: Link local tracks to MusicBrainz recordings (MBID association) -- **MB-F02**: Search recordings (find specific songs across releases) +- **MB-01**: User can search for artists by name and view results +- **MB-02**: User can browse an artist's discography (release groups — albums, EPs, singles) +- **MB-03**: User can view tracks on a specific release +- **MB-04**: User can view different editions of a release group (pressings, reissues) +- **MB-05**: API responses are cached in SQLite (24hr for searches, 7 days for entities) +- **MB-06**: Album cover art is displayed from the Cover Art Archive +- **MB-07**: Rate limiting (1 req/sec) is enforced with proper User-Agent header + +### Layout Customization (Deferred from v1.1) + +- **LAYOUT-01**: User can resize sidebar and queue panels via drag handles +- **LAYOUT-02**: Panel sizes persist across app restarts +- **LAYOUT-03**: User can show/hide sidebar sections and queue panel +- **LAYOUT-04**: User can choose which component is displayed in each layout section (MusicBee-style) +- **LAYOUT-05**: Components declare size constraints (min/max dimensions, aspect ratio compatibility) +- **LAYOUT-06**: Layout presets available (Compact, Full, Mini player) with quick switch ### Layout Customization (v2+) - **LAYOUT-F01**: Detachable panels (pop out to separate window) +### Plugin System (Deferred from v1.1) + +- **PLUG-01**: Plugin API is defined — plugins can access events, player state, queue, library data +- **PLUG-02**: JS/TS plugin bundles are loaded from user plugin directory at runtime +- **PLUG-03**: Plugins can register UI components into the layout system +- **PLUG-04**: Plugin manifest file defines name, version, permissions, hooks, and UI components +- **PLUG-05**: Plugins can have their own persistent configuration +- **PLUG-06**: One example plugin ships demonstrating the API + ### Plugin System (v2+) - **PLUG-F01**: Plugin marketplace/registry for discovery and installation @@ -120,16 +153,18 @@ Explicitly excluded. Documented to prevent scope creep. | Feature | Reason | |---------|--------| +| Separate databases per library | Defeats unified presentation, overly complex | +| Auto-dedup across libraries | Complex matching logic, not table stakes | +| User access control per library | Desktop app, single user | +| Parallel library scanning | SQLite single-writer makes it pointless | | OGG Vorbis tag writing | No mature pure-Go write library exists | | WAV metadata editing | Rarely needed, low priority | -| Auto-tag from MusicBrainz | Complex matching logic — Picard's domain, not a browser feature | -| Write data to MusicBrainz | Requires OAuth and community guidelines compliance | +| Auto-tag from MusicBrainz | Complex matching logic — Picard's domain | | DSP effects chain (equalizer, reverb) | Scope explosion — separate feature area | | Go `plugin` package for backend plugins | Linux-only, version-fragile, widely considered broken | | Free-form drag-and-drop layout | Overwhelming complexity; section-based approach is better | | Global OS-level hotkeys | Platform-specific, conflicts with OS shortcuts; MPRIS2 handles media keys | | Mobile-responsive layout | Desktop app with fixed minimum size | -| Plugin binary distribution | Source-based (JS bundles) is safer and more portable | ## Traceability @@ -145,47 +180,35 @@ Which phases cover which requirements. Updated during roadmap creation. | KEY-03 | Phase 9 | Complete | | KEY-04 | Phase 9 | Complete | | KEY-05 | Phase 9 | Complete | -| TAG-01 | Phase 10 | Pending | -| TAG-02 | Phase 10 | Pending | -| TAG-03 | Phase 10 | Pending | -| TAG-04 | Phase 10 | Pending | -| TAG-05 | Phase 10 | Pending | -| TAG-06 | Phase 10 | Pending | -| TAG-07 | Phase 10 | Pending | -| SMRT-01 | Phase 11 | Pending | -| SMRT-02 | Phase 11 | Pending | -| SMRT-03 | Phase 11 | Pending | -| SMRT-04 | Phase 11 | Pending | -| SMRT-05 | Phase 11 | Pending | -| GAP-01 | Phase 12 | Pending | -| GAP-02 | Phase 12 | Pending | -| GAP-03 | Phase 12 | Pending | -| GAP-04 | Phase 12 | Pending | -| MB-01 | Phase 13 | Pending | -| MB-02 | Phase 13 | Pending | -| MB-03 | Phase 13 | Pending | -| MB-04 | Phase 13 | Pending | -| MB-05 | Phase 13 | Pending | -| MB-06 | Phase 13 | Pending | -| MB-07 | Phase 13 | Pending | -| LAYOUT-01 | Phase 14 | Pending | -| LAYOUT-02 | Phase 14 | Pending | -| LAYOUT-03 | Phase 14 | Pending | -| LAYOUT-04 | Phase 14 | Pending | -| LAYOUT-05 | Phase 14 | Pending | -| LAYOUT-06 | Phase 14 | Pending | -| PLUG-01 | Phase 14 | Pending | -| PLUG-02 | Phase 14 | Pending | -| PLUG-03 | Phase 14 | Pending | -| PLUG-04 | Phase 14 | Pending | -| PLUG-05 | Phase 14 | Pending | -| PLUG-06 | Phase 14 | Pending | +| LIB-01 | TBD | Pending | +| LIB-02 | TBD | Pending | +| LIB-03 | TBD | Pending | +| LIB-04 | TBD | Pending | +| LIB-05 | TBD | Pending | +| LIB-06 | TBD | Pending | +| LSCAN-01 | TBD | Pending | +| LSCAN-02 | TBD | Pending | +| LSCAN-03 | TBD | Pending | +| LSCAN-04 | TBD | Pending | +| LSCAN-05 | TBD | Pending | +| VIEW-01 | TBD | Pending | +| VIEW-02 | TBD | Pending | +| VIEW-03 | TBD | Pending | +| VIEW-04 | TBD | Pending | +| PLAY-01 | TBD | Pending | +| PLAY-02 | TBD | Pending | +| PLAY-03 | TBD | Pending | +| PLAY-04 | TBD | Pending | +| DATA-01 | TBD | Pending | +| DATA-02 | TBD | Pending | +| DATA-03 | TBD | Pending | +| DATA-04 | TBD | Pending | **Coverage:** -- v1.1 requirements: 43 total -- Mapped to phases: 43 -- Unmapped: 0 ✓ +- v1.1 requirements: 28 total (8 complete + 20 pending) +- Mapped to phases: 8 (Phase 9 complete) +- Awaiting roadmap: 20 --- *Requirements defined: 2026-03-06* -*Last updated: 2026-03-06 — traceability updated with phase mappings* +*Last updated: 2026-03-08 — restructured for multi-library support, deferred TAG/SMRT/GAP/MB/LAYOUT/PLUG* diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index f5ab458..5b934a8 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -1,13 +1,13 @@ # Roadmap: YellowJacket **Created:** 2026-02-27 -**Last updated:** 2026-03-06 -**Current milestone:** v1.1 Features & Extensibility +**Last updated:** 2026-03-08 +**Current milestone:** v1.1 Multi-Library Support ## Milestones - ✅ **v1.0 Consolidation** — Phases 1-8 (shipped 2026-03-05) — [archive](milestones/v1.0-ROADMAP.md) -- 🔄 **v1.1 Features & Extensibility** — Phases 9-14 (in progress) +- 🔄 **v1.1 Multi-Library Support** — Phase 9 complete, multi-library phases TBD (in progress) ## Phases @@ -25,14 +25,10 @@ -### v1.1 Features & Extensibility (Phases 9-14) +### v1.1 Multi-Library Support (Phase 9 + Multi-Library Phases) - [x] **Phase 9: Scan Cancellation & Keyboard Shortcuts** — Cancellable library scans and configurable keyboard shortcuts -- [ ] **Phase 10: Tag Editing** — Edit track metadata and write changes to audio files -- [ ] **Phase 11: Smart Playlists** — Auto-generated playlists with filter rules -- [ ] **Phase 12: Gapless Playback & Crossfade** — Seamless track transitions with optional crossfade -- [ ] **Phase 13: MusicBrainz Browser** — Browse the MusicBrainz catalog from within the app -- [ ] **Phase 14: Layout Customization & Plugin Foundation** — Section-based UI customization and JS plugin system +- [ ] **Phases 10+: Multi-Library Support** — TBD (awaiting roadmap creation from gsd-roadmapper) ## Phase Details @@ -54,63 +50,23 @@ Plans: - [x] 09-04-PLAN.md — Frontend shortcut settings UI (record-style capture, conflict detection) - [x] 09-05-PLAN.md — Integration verification checkpoint -### Phase 10: Tag Editing -**Goal:** Users can edit track metadata from within the app and changes are written to the actual audio files -**Depends on:** Phase 9 (scan cancellation validates context patterns; tag edits must be blocked during active scans) -**Requirements:** TAG-01, TAG-02, TAG-03, TAG-04, TAG-05, TAG-06, TAG-07 -**Success Criteria** (what must be TRUE): - 1. User can select a track, edit its title/artist/album/genre/year/track number in a UI form, and save — the changes appear immediately in the library without requiring a rescan - 2. User can select multiple tracks, edit shared fields (e.g., album name, genre), and the batch edit applies to all selected tracks - 3. Tag changes are persisted to the actual MP3 (ID3v2) and FLAC (Vorbis Comments) files on disk — verified by re-reading the file's metadata - 4. User can assign or replace embedded cover art from an image file, and the new art displays immediately - 5. A file that is currently playing cannot have its tags edited — the UI shows a clear indication that the edit is blocked until playback moves on -**Plans:** TBD +### Multi-Library Phases (10+) -### Phase 11: Smart Playlists -**Goal:** Users can create rule-based playlists that automatically populate based on their music library metadata -**Depends on:** Phase 10 (tag editing validates DB update + event pipeline; edited metadata affects smart playlist membership) -**Requirements:** SMRT-01, SMRT-02, SMRT-03, SMRT-04, SMRT-05 -**Success Criteria** (what must be TRUE): - 1. User can create a smart playlist with one or more filter rules (genre equals "Jazz", year > 2000, artist contains "Miles") and see matching tracks - 2. Multiple rules combine with AND logic — adding a second rule narrows the results - 3. User can set random ordering and a result limit (e.g., "Random 50 Jazz tracks") and the playlist respects both - 4. Smart playlists appear in the sidebar alongside regular playlists with a distinct icon, and their rules persist across app restarts -**Plans:** TBD +**Awaiting roadmap creation.** The gsd-roadmapper will create phased breakdown covering 20 requirements: +- LIB-01..06 (Library Management) +- LSCAN-01..05 (Library Scanning) +- VIEW-01..04 (Unified Presentation) +- PLAY-01..04 (Playlists & Queue) +- DATA-01..04 (Data Integrity) -### Phase 12: Gapless Playback & Crossfade -**Goal:** Tracks transition seamlessly with no audible gap, and users can optionally enable crossfade between tracks -**Depends on:** Phase 9 (keyboard shortcuts needed for testing audio transitions; no direct code dependency but risk isolation — this is the highest-risk phase) -**Requirements:** GAP-01, GAP-02, GAP-03, GAP-04 -**Success Criteria** (what must be TRUE): - 1. When playing an album, tracks transition with no audible silence gap — the audio stream is continuous - 2. The next track is pre-decoded before the current track ends so the transition is instantaneous - 3. User can enable crossfade in settings with a configurable duration (1-10 seconds), and tracks blend smoothly during auto-advance - 4. Crossfade only applies on auto-advance (track finishes naturally) — manual skip/next produces an immediate clean switch -**Plans:** TBD +**Architecture decisions:** +- Hybrid model: `library_id` FK on `audio_files` only; artists/albums/genres stay global +- Libraries stored in SQLite, not TOML config +- Sequential scanning (one library at a time) +- Phantom tracks for playlist preservation on library removal +- Backend filtering for library views -### Phase 13: MusicBrainz Browser -**Goal:** Users can browse the MusicBrainz music catalog (artists, albums, tracks) directly from within the app -**Depends on:** Phase 11 (smart playlists validate dynamic DB query patterns reused by MB cache; no hard dependency but ordering isolates network feature) -**Requirements:** MB-01, MB-02, MB-03, MB-04, MB-05, MB-06, MB-07 -**Success Criteria** (what must be TRUE): - 1. User can search for an artist by name and see a list of matching results from MusicBrainz - 2. User can select an artist and browse their discography — albums, EPs, and singles displayed as release groups - 3. User can view the track listing for a specific release and see different editions (original, reissue, deluxe) of a release group - 4. Album cover art from the Cover Art Archive is displayed alongside release information - 5. The app respects MusicBrainz rate limits (1 req/sec), caches responses in SQLite (24hr searches, 7 days entities), and works gracefully when offline or rate-limited -**Plans:** TBD - -### Phase 14: Layout Customization & Plugin Foundation -**Goal:** Users can customize the app's layout (resize, show/hide, rearrange panels) and developers can extend the app with JavaScript plugins -**Depends on:** Phase 13 (layout and plugin systems wrap all existing features — needs stable component set and API surface) -**Requirements:** LAYOUT-01, LAYOUT-02, LAYOUT-03, LAYOUT-04, LAYOUT-05, LAYOUT-06, PLUG-01, PLUG-02, PLUG-03, PLUG-04, PLUG-05, PLUG-06 -**Success Criteria** (what must be TRUE): - 1. User can drag to resize the sidebar and queue panels, and the sizes persist across app restarts - 2. User can show/hide sidebar sections and the queue panel, and choose which component is displayed in each layout section - 3. Layout presets (Compact, Full, Mini player) are available and the user can quick-switch between them - 4. A JS/TS plugin loaded from the user's plugin directory can access player state, queue data, and library data via a defined API, and can register a custom UI component into the layout - 5. An example plugin ships with the app demonstrating the plugin API (manifest, configuration, event hooks, UI registration) -**Plans:** TBD +See `.planning/research/SUMMARY.md` for full research. ## Progress @@ -124,13 +80,9 @@ Plans: | 6. SQL Consolidation & Code Quality | v1.0 | 3/3 | Complete | 2026-03-04 | | 7. Backend Performance | v1.0 | 2/2 | Complete | 2026-03-05 | | 8. Frontend Performance & UX | v1.0 | 4/4 | Complete | 2026-03-05 | -| 9. Scan Cancellation & Keyboard Shortcuts | v1.1 | 0/5 | Planning complete | - | -| 10. Tag Editing | v1.1 | 0/? | Not started | - | -| 11. Smart Playlists | v1.1 | 0/? | Not started | - | -| 12. Gapless Playback & Crossfade | v1.1 | 0/? | Not started | - | -| 13. MusicBrainz Browser | v1.1 | 0/? | Not started | - | -| 14. Layout Customization & Plugin Foundation | v1.1 | 0/? | Not started | - | +| 9. Scan Cancellation & Keyboard Shortcuts | v1.1 | 5/5 | Complete | 2026-03-07 | +| 10+ Multi-Library phases | v1.1 | 0/? | Awaiting roadmap | - | --- *Roadmap created: 2026-02-27* -*Last updated: 2026-03-06 — v1.1 phases 9-14 added* +*Last updated: 2026-03-08 — v1.1 restructured for multi-library support, old phases 10-14 deferred* diff --git a/.planning/STATE.md b/.planning/STATE.md index 7d42af7..f3c6bad 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -1,9 +1,9 @@ --- gsd_state_version: 1.0 milestone: v1.1 -milestone_name: Features & Extensibility -status: unknown -last_updated: "2026-03-07T15:12:59.202Z" +milestone_name: Multi-Library Support +status: planning +last_updated: "2026-03-08" progress: total_phases: 1 completed_phases: 1 @@ -15,34 +15,30 @@ progress: ## Project Reference -See: .planning/PROJECT.md (updated 2026-03-06) +See: .planning/PROJECT.md (updated 2026-03-08) **Core value:** The music player works reliably and feels solid — every interaction is correct, responsive, and trustworthy. -**Current focus:** v1.1 Features & Extensibility — Phase 9 complete +**Current focus:** v1.1 Multi-Library Support — roadmap planning ## Current Position -Phase: 9 — Scan Cancellation & Keyboard Shortcuts +Phase: 9 — Scan Cancellation & Keyboard Shortcuts (last completed) Plan: 5 of 5 -Status: Complete -Progress: ████████████████████ 5/5 plans (100%) -Last activity: 2026-03-07 — Completed 09-05 (integration testing & verification) +Status: Complete — awaiting multi-library roadmap +Progress: Roadmap creation in progress +Last activity: 2026-03-08 — Restructured v1.1 milestone for multi-library support ### Phase Overview | Phase | Status | |-------|--------| | 9. Scan Cancellation & Keyboard Shortcuts | Complete (5/5 plans) ✅ | -| 10. Tag Editing | Not started | -| 11. Smart Playlists | Not started | -| 12. Gapless Playback & Crossfade | Not started | -| 13. MusicBrainz Browser | Not started | -| 14. Layout Customization & Plugin Foundation | Not started | +| 10+ Multi-Library phases | Awaiting roadmap creation | ## Performance Metrics **v1.0 baseline:** 8 phases, 17 plans, 34 tasks in 6 days (107 commits) -**v1.1 scope:** 6 phases, 43 requirements, 5 plans (Phase 9) +**v1.1 scope:** Phase 9 complete (5 plans), multi-library phases TBD (20 requirements) | Phase | Plan | Duration | Tasks | Files | |-------|------|----------|-------|-------| @@ -70,38 +66,39 @@ Decisions from v1.0 are archived in PROJECT.md Key Decisions table. Key patterns | Decision | Rationale | |----------|-----------| | Phase 9 = Scan Cancel + Shortcuts | Quick wins, validate context cancellation and config extension patterns | -| Phase 10 = Tag Editing after shortcuts | Introduces 3 new deps, file-write-DB-event pipeline; benefits from validated patterns | -| Phase 11 = Smart Playlists after tags | DB patterns, benefits from validated DB update pipeline | -| Phase 12 = Gapless mid-sequence | Highest risk isolated after foundations proven, before meta-features | -| Phase 13 = MusicBrainz after gapless | First network feature, orthogonal to audio work | -| Phase 14 = Layout + Plugins last | Meta-features that wrap all others, need stable API surface | -| Layout + Plugins combined into one phase | Both are extensibility foundations; layout provides component registry that plugins register into | +| v1.1 restructured for multi-library | Tag editing, smart playlists, gapless, MusicBrainz, layout, plugins deferred to future milestones | +| Hybrid model (library_id on audio_files only) | Physical files belong to libraries; logical entities (artists, albums, genres) are global/shared | +| Libraries in DB, not TOML | CRUD through UI shouldn't require TOML manipulation; DB is source of truth | +| SET NULL for playlist_tracks FK | Phantom tracks preserve playlist structure when library removed | +| CASCADE for queue_tracks FK | Queue is ephemeral, not user-curated like playlists | +| Sequential scanning | SQLite single-writer makes parallel scans pointless | +| Backend filtering, not frontend | Don't load 150K tracks when viewing one library | ### Warnings (carry forward) -- Player lock ordering (`p.mu` before `speaker.Lock()`, goroutine dispatch in beep callback) — CRITICAL for Phase 12 gapless work +- Player lock ordering (`p.mu` before `speaker.Lock()`, goroutine dispatch in beep callback) — carry forward - modernc.org/libc version must match exactly when updating modernc.org/sqlite - `@lit-labs/signals` is experimental (v0.2.0) — not blocking but noted -- Tag writing: block edits on currently-playing files (beep holds `*os.File` handle) — Phase 10 - Scan cancellation: skip orphan cleanup on cancelled scans — Phase 9 ✅ (implemented in 09-01) - Volume mutations (ChangeVolume/MuteToggle) must emit events + persist state — Phase 9 ✅ (fixed in 09-05) -- MusicBrainz: strict 1 req/s rate limit, proper User-Agent, SQLite cache — Phase 13 -- Plugin system: JS-only for v1.1, recover() wrappers, read-only DB access — Phase 14 -- FLAC tag writes load entire file into memory (go-flac) — acceptable for v1.1 — Phase 10 +- ALTER TABLE ADD COLUMN requires DEFAULT for NOT NULL — create libraries table first +- Table rebuild must audit ALL CASCADE FKs (playlist_tracks AND queue_tracks) +- FTS5 contentless can't DELETE rows — stale entries accumulate after library removal; consider contentless_delete migration +- Orphan cleanup must not delete shared entities across libraries (reference-counting bottom-up) +- Existing user migration must be seamless (TOML DirectoryPath to DB libraries table) ### Research Flags -- **Phase 12 (Gapless + Crossfade):** Needs deeper research — beep Mixer/Seq composition for real-time crossfade not well-documented. Prototype persistent-mixer architecture before committing to implementation. -- **Phase 14 (Plugin System):** Needs deeper research — plugin API surface design, error containment, security boundaries. Consider spike/prototype. +- **Multi-library research complete** — see `.planning/research/` (STACK.md, FEATURES.md, ARCHITECTURE.md, PITFALLS.md, SUMMARY.md) ## Session Continuity ### Last Session -**Date:** 2026-03-07 -**What happened:** Completed 09-05-PLAN.md — integration testing & verification. All automated checks passed. Human verification approved all 23 test scenarios. Fixed volume data flow bug (ChangeVolume/MuteToggle missing event emission and state persistence). -**Where we stopped:** Completed 09-05-PLAN.md — Phase 9 complete -**Next action:** `/gsd-plan-phase 10` — Plan Phase 10 (Tag Editing) +**Date:** 2026-03-08 +**What happened:** Restructured v1.1 milestone from "Features & Extensibility" to "Multi-Library Support". Ran 4 parallel researchers (Stack, Features, Architecture, Pitfalls). Defined 20 multi-library requirements (LIB, LSCAN, VIEW, PLAY, DATA). Deferred TAG/SMRT/GAP/MB/LAYOUT/PLUG to future milestones. Updated PROJECT.md, REQUIREMENTS.md, STATE.md, ROADMAP.md. +**Where we stopped:** Planning documents updated, ready for roadmap creation +**Next action:** Run gsd-roadmapper to create phased multi-library roadmap (phases 10+) --- *State initialized: 2026-02-27* @@ -114,4 +111,4 @@ Decisions from v1.0 are archived in PROJECT.md Key Decisions table. Key patterns | 18 | add multi-column metadata display to playlist-details | 2026-03-08 | ce23177 | [18-add-multi-column-metadata-display-to-pla](./quick/18-add-multi-column-metadata-display-to-pla/) | Last activity: 2026-03-08 - Completed quick task 18: add multi-column metadata display to playlist-details -*Last updated: 2026-03-08* +*Last updated: 2026-03-08 — v1.1 restructured for multi-library support* diff --git a/.planning/research/ARCHITECTURE.md b/.planning/research/ARCHITECTURE.md index 0d09e10..9549116 100644 --- a/.planning/research/ARCHITECTURE.md +++ b/.planning/research/ARCHITECTURE.md @@ -1,970 +1,118 @@ -# Architecture Patterns: v1.1 Feature Integration +# Architecture Research: Multi-Library Integration -**Domain:** Desktop music player — new feature integration with existing Wails/Lit/beep/SQLite architecture -**Researched:** 2026-03-06 -**Confidence:** HIGH (derived from complete codebase read + official docs for beep, MusicBrainz API, dhowden/tag) +**Researched:** 2026-03-08 +**Confidence:** HIGH -## Recommended Architecture +## Design Decision: Hybrid Model -YellowJacket v1.1 adds 8 features to an existing, well-structured codebase. The architecture approach is **integration-first**: each feature slots into established patterns (two-phase init, event-driven sync, mutex-protected state, sqlc codegen) rather than introducing new architectural paradigms. The one exception is the plugin system, which necessarily introduces a new extension mechanism. +- **`library_id` on `audio_files` only** — physical file binding +- **Artists, albums, recordings, genres stay global** — shared reference data +- **Unified presentation by default** — optional library filter +- **Cross-library playlists** — playlists reference audio_file_id +- **Phantom tracks on removal** — playlist entries preserved with metadata -### High-Level Integration Map +## Database Changes -``` - ┌─────────────────────────────────────────┐ - │ app.go (wiring) │ - │ New: TagEditor, SmartPlaylist, │ - │ MusicBrainz, Shortcuts, Layout │ - └────────────────┬────────────────────────┘ - │ - ┌────────────────────────────┼────────────────────────────┐ - │ │ │ - ┌────▼────┐ ┌─────▼─────┐ ┌─────▼─────┐ - │ player │ │ database │ │ events │ - │ │ │ │ │ │ - │ +gapless│ │ +smart_pl │ │ +TagsEdit │ - │ +xfade │ │ +shortcuts│ │ +ScanCanc │ - │ │ │ +layout │ │ +SmartPL │ - └─────────┘ └───────────┘ │ +Shortcut │ - │ +Layout │ - │ +MBrainz │ - └───────────┘ -``` - -### Component Boundaries - -| Component | Responsibility | New vs Modified | Communicates With | -|-----------|---------------|-----------------|-------------------| -| `backend/tageditor/` | Read/write audio file tags, coordinate DB updates | **NEW** package | metadata, database, library, events | -| `backend/library/` | Scan cancellation via context | **MODIFIED** | database, events, coverart | -| `backend/smartplaylist/` | Rule-based dynamic playlists | **NEW** package | database, events | -| `backend/shortcuts/` | Keyboard shortcut registry + dispatch | **NEW** package | config, events, player, queue | -| `backend/player/` | Gapless playback + crossfade | **MODIFIED** | beep, events, queue | -| `backend/musicbrainz/` | MusicBrainz API client + caching | **NEW** package | database (cache tables), events | -| `backend/layout/` | Layout section configuration | **NEW** package | config, events | -| `backend/plugin/` | Plugin loading, lifecycle, API surface | **NEW** package | all packages (via API) | -| `frontend/src/store/tageditor-store.ts` | Tag edit state | **NEW** store | backend tageditor bindings | -| `frontend/src/store/smartplaylist-store.ts` | Smart playlist state | **NEW** store | backend smartplaylist bindings | -| `frontend/src/store/shortcut-store.ts` | Shortcut config state | **NEW** store | backend shortcuts bindings | -| `frontend/src/store/musicbrainz-store.ts` | MB browsing state | **NEW** store | backend musicbrainz bindings | -| `frontend/src/store/layout-store.ts` | Layout config state | **NEW** store | backend layout bindings | - -### Data Flow - -**Existing pattern preserved**: Backend is source of truth. Frontend stores are reactive mirrors. Events flow backend-to-frontend. Actions flow frontend-to-backend via Wails bindings. - -**New data flows:** - -1. **Tag Edit Flow**: Frontend collects edits → Wails binding → `tageditor.SaveTags()` → write file tags → update DB records → emit `TagsEdited` event → frontend refreshes affected views -2. **Scan Cancel Flow**: Frontend sends cancel request → Wails binding → `library.CancelScan()` → cancel context propagation → scan goroutines check `ctx.Done()` → emit `LibraryScanCancelled` event -3. **Smart Playlist Flow**: User defines rules via frontend → Wails binding → `smartplaylist.Create()` → persist rules to DB → evaluate rules → emit `SmartPlaylistChanged` event → frontend refreshes -4. **Gapless Flow**: Player pre-decodes next track in background → when current track ends, swap streamer chains without speaker interruption → seamless transition -5. **MusicBrainz Flow**: Frontend search query → Wails binding → `musicbrainz.Search()` → HTTP GET to MB API (rate-limited) → cache results in SQLite → return to frontend → display - ---- - -## Feature 1: Tag Editing - -### Architecture - -**New package: `backend/tageditor/`** - -The existing `dhowden/tag` library is **read-only**. Tag writing requires a separate library. Use `bogem/id3v2` for MP3 files and `go-flac/go-flac` (or equivalent) for FLAC Vorbis comments. OGG and WAV tag writing can be deferred (LOW priority formats). - -**Confidence:** HIGH — `dhowden/tag` has no write support (confirmed from source). `bogem/id3v2` is the standard Go ID3v2 writer. - -```go -// backend/tageditor/tageditor.go -type TagEditor struct { - mu sync.Mutex - ctx context.Context - logger *slog.Logger - db *database.DB - lib *library.Library // for FTS re-indexing -} - -type TagUpdate struct { - FilePath string - Title string - Artist string - Album string - // ... all editable fields -} - -func (te *TagEditor) SaveTags(update TagUpdate) error { - // 1. Write tags to audio file (format-specific writer) - // 2. Update recording/artist_credit/release_group in DB - // 3. Re-index in FTS5 search_index - // 4. Emit TagsEdited event with affected file paths -} -``` - -### Integration Points - -| Touchpoint | Change | Risk | -|------------|--------|------| -| `backend/app.go` | Add TagEditor to `FEBindings`, wire in `OnStartup` | LOW — follows existing pattern | -| `backend/events/events.go` | Add `TagsEdited`, `TagEditFailed` events | LOW — codegen handles sync | -| `backend/database/` | New queries: `UpdateRecordingMetadata`, `GetRecordingByAudioFileID` | LOW — sqlc pattern | -| `backend/metadata/tags.go` | Add `WriteTags()` function alongside existing `ExtractTags()` | MEDIUM — new dependency | -| `frontend/src/components/` | New `` component (modal/panel) | LOW | -| Library/Queue/Player stores | Must react to `TagsEdited` to refresh displayed metadata | MEDIUM — cross-store coordination | - -### Critical Design Decision - -**Write tags to file first, then update DB.** If the file write fails, the DB stays consistent. If the DB update fails after file write, the next scan will reconcile. This matches the existing pattern where the filesystem is the primary source and the DB is derived. - -### DB Update Strategy - -Tag edits must cascade through the normalized schema: -1. Update `recordings.name` (title) -2. Upsert `artist_credit` + `artists` + link table (if artist changed) -3. Upsert `release_groups` (if album changed) -4. Re-link `release_group_recordings` -5. Re-index FTS5 `search_index` - -All within a single transaction. The existing `entityCache` pattern from library scanning can be reused for lookups. - ---- - -## Feature 2: Scan Cancellation - -### Architecture - -**Modified: `backend/library/library.go`** - -The scan pipeline already uses `l.ctx` for context propagation. Cancellation requires: -1. A dedicated `context.CancelFunc` stored on the Library struct -2. All scan phases checking `ctx.Done()` (most already do via `select` in the walk and worker phases) - -```go -type Library struct { - mu sync.Mutex - ctx context.Context - // ... existing fields ... - scanCancel context.CancelFunc // NEW: cancel function for active scan - scanning bool // NEW: flag for active scan -} - -func (l *Library) CancelScan() { - l.mu.Lock() - defer l.mu.Unlock() - if l.scanCancel != nil { - l.scanCancel() - } -} - -func (l *Library) Scan() (*ScanMetrics, error) { - scanCtx, cancel := context.WithCancel(l.ctx) - l.mu.Lock() - l.scanCancel = cancel - l.scanning = true - l.mu.Unlock() - defer func() { - l.mu.Lock() - l.scanCancel = nil - l.scanning = false - l.mu.Unlock() - }() - // ... existing scan code, but use scanCtx instead of l.ctx ... -} -``` - -### Integration Points - -| Touchpoint | Change | Risk | -|------------|--------|------| -| `backend/library/library.go` | Add `scanCancel` field, `CancelScan()` method, wrap scan in child context | LOW — surgical change | -| `backend/events/events.go` | Add `LibraryScanCancelled` event | LOW | -| `frontend/src/store/library-store.ts` | Listen for cancelled event, update scan state | LOW | -| Frontend scan progress UI | Add cancel button | LOW | - -### Key Constraint - -The scan's DB writer goroutine commits in batches of 50. Cancellation should allow the current batch to complete (don't leave a half-committed transaction). Check `scanCtx.Done()` between batches, not mid-batch. - ---- - -## Feature 3: Smart Playlists - -### Architecture - -**New package: `backend/smartplaylist/`** - -Smart playlists are rule-based queries that dynamically produce track lists. They are **not** persisted as `playlist_tracks` — they're evaluated on demand from the rule definition. - -```go -// backend/smartplaylist/smartplaylist.go -type Service struct { - mu sync.Mutex - ctx context.Context - logger *slog.Logger - db *database.DB -} - -type Rule struct { - Field string // "genre", "year", "artist", "album", "play_count", "date_added" - Operator string // "is", "is_not", "contains", "greater_than", "less_than", "between" - Value string - Value2 string // for "between" operator -} - -type SmartPlaylist struct { - ID int64 - Name string - Rules []Rule - MatchAll bool // AND vs OR - OrderBy string - Limit int -} - -func (s *Service) Evaluate(id int64) ([]Track, error) { - // 1. Load smart playlist rules from DB - // 2. Build SQL WHERE clause from rules - // 3. Query track_metadata VIEW with dynamic conditions - // 4. Return results -} -``` - -### Database Schema +### New Table: libraries ```sql --- New table -CREATE TABLE smart_playlists ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - rules_json TEXT NOT NULL, -- JSON-encoded []Rule - match_all BOOLEAN NOT NULL DEFAULT 1, - order_by TEXT NOT NULL DEFAULT 'title', - max_tracks INTEGER NOT NULL DEFAULT 0, -- 0 = unlimited - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +CREATE TABLE IF NOT EXISTS libraries ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + path TEXT NOT NULL UNIQUE, + scan_concurrency TEXT NOT NULL DEFAULT 'auto', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_scanned_at DATETIME ); ``` -### Query Generation Strategy +### Migration 6: Multi-Library Support -Rules map to the existing `track_metadata` VIEW columns. Build parameterized WHERE clauses: +Order of operations: +1. Create `libraries` table +2. Read TOML `DirectoryPath`, insert as default library +3. `ALTER TABLE audio_files ADD COLUMN library_id INTEGER NOT NULL DEFAULT {defaultLibID}` +4. Create index on `audio_files(library_id)` +5. Rebuild `playlist_tracks` with SET NULL FK + phantom columns +6. Drop and recreate `track_metadata` VIEW with `library_id` +7. Set `PRAGMA user_version = 6` -```go -func buildWhereClause(rules []Rule, matchAll bool) (string, []any) { - // Each rule becomes: "column OPERATOR ?" - // Combined with AND (matchAll) or OR (!matchAll) - // All values are parameterized — no SQL injection risk -} -``` - -**Use raw `db.QueryContext()` for dynamic queries** — sqlc cannot generate dynamic WHERE clauses. Document with `// SAFETY:` comments per existing convention. - -### Integration Points - -| Touchpoint | Change | Risk | -|------------|--------|------| -| `backend/app.go` | Add SmartPlaylist service to `FEBindings` | LOW | -| `backend/database/` | New schema for `smart_playlists` table, migration 6 | LOW | -| `backend/events/events.go` | Add `SmartPlaylistChanged`, `SmartPlaylistDeleted` events | LOW | -| Frontend | New `` component, rules builder UI | MEDIUM — most complex frontend work | - ---- - -## Feature 4: Customizable Keyboard Shortcuts - -### Architecture - -**New package: `backend/shortcuts/`** - -Shortcuts are stored in the TOML config and dispatched via Wails events. The backend holds the definitive shortcut map; the frontend registers a global `keydown` listener that sends key combos to the backend for resolution. - -```go -// backend/shortcuts/shortcuts.go -type Service struct { - mu sync.Mutex - ctx context.Context - logger *slog.Logger - bindings map[string]string // key combo → action name - actions map[string]func() // action name → handler -} - -type Shortcut struct { - Action string `toml:"Action" json:"action"` - Key string `toml:"Key" json:"key"` // e.g., "Ctrl+Space", "MediaPlayPause" -} -``` - -### Config Integration - -Add a `[Shortcuts]` section to `config.toml`: - -```toml -[Shortcuts] -PlayPause = "Space" -NextTrack = "Ctrl+Right" -PrevTrack = "Ctrl+Left" -VolumeUp = "Ctrl+Up" -VolumeDown = "Ctrl+Down" -# ... -``` - -### Frontend Dispatch Pattern - -```typescript -// Frontend: global keydown handler -document.addEventListener('keydown', (e) => { - const combo = buildComboString(e); // e.g., "Ctrl+Space" - Shortcuts.Execute(combo); // Wails binding → backend resolves + executes -}); -``` - -**Why backend dispatch?** The backend already owns all action handlers (player.Play, queue.Next, etc.). Having the backend resolve shortcuts avoids duplicating action dispatch logic in the frontend. The frontend's only job is translating DOM KeyboardEvents into combo strings. - -### Integration Points - -| Touchpoint | Change | Risk | -|------------|--------|------| -| `backend/config/config.go` | Add `Shortcuts` config section | LOW | -| `backend/app.go` | Wire Shortcuts service, register action handlers | LOW | -| `frontend/index.ts` | Add global keydown listener | LOW | -| MPRIS callbacks | Already wired in `app.go OnStartup` — shortcut actions reuse same handler functions | LOW | - ---- - -## Feature 5: Gapless Playback + Crossfade - -### Architecture - -**Modified: `backend/player/player.go`** - -This is the most architecturally complex feature because it fundamentally changes how track transitions work. - -### Gapless Playback - -**Current behavior:** `beep.Callback` fires → `onPlaybackFinished()` goroutine → queue calls `player.LoadFile()` → decode + resample + register with speaker. This gap (file open + decode) causes audible silence. - -**Gapless approach:** Pre-decode the next track while the current one is still playing. When the current track's streamer is near exhaustion, seamlessly swap to the pre-decoded next track. - -```go -type Player struct { - // ... existing fields ... - - // Gapless pre-loading - nextFile *os.File - nextStreamer beep.StreamSeekCloser - nextFormat beep.Format - nextBuffered *BufferedStreamer - nextFilePath string - gaplessEnabled bool -} - -// PreloadNext is called by the queue when it knows what track comes next. -func (p *Player) PreloadNext(filePath string) error { - p.mu.Lock() - defer p.mu.Unlock() - // Open file, decode, build resampled chain, store in next* fields - // Do NOT register with speaker yet -} -``` - -**Speaker integration:** Use `beep.Seq()` to chain current + next streamer, or use a custom `GaplessStreamer` that automatically drains the current streamer and transitions to the next one without the `beep.Callback` → goroutine → LoadFile delay. - -The key insight: beep's `Seq(a, b)` already provides gapless transition between two streamers. The challenge is having `b` ready before `a` ends. - -### Crossfade - -Crossfade uses beep's `Mixer` to overlap two tracks: - -```go -// When crossfade is enabled and we're N seconds from track end: -// 1. Start fading out current track's volume -// 2. Start next track at low volume, fade in -// 3. Mix both through beep.Mixer -``` - -This requires: -1. A `crossfadeDuration` config setting (default 0 = disabled, range 1-12 seconds) -2. A crossfade mixer that handles the volume ramping -3. Knowing the remaining duration of the current track to trigger crossfade at the right time - -### Queue Integration - -The queue must tell the player what's next: - -```go -// In queue.go, after track advance logic: -func (q *Queue) notifyNextTrack() { - nextIdx := q.peekNextIndex() // look ahead without advancing - if nextIdx >= 0 && nextIdx < len(q.tracks) { - q.player.PreloadNext(q.tracks[nextIdx].FilePath) - } -} -``` - -This notification happens: -- After `SetQueue` (next track is known) -- After `Next`/`Previous` (new next track) -- After `OnPlaybackFinished` auto-advance (next-next track) - -### TrackLoader Interface Change - -```go -type TrackLoader interface { - LoadFile(filePath string) error - Play() error - IsPlaying() bool - CurrentPositionSeconds() (int, error) - UnloadTrack() - PreloadNext(filePath string) error // NEW -} -``` - -### Integration Points - -| Touchpoint | Change | Risk | -|------------|--------|------| -| `backend/player/player.go` | Pre-loading, gapless streamer chain, crossfade mixer | **HIGH** — core audio pipeline | -| `backend/queue/queue.go` | `TrackLoader` interface extension, next-track notification | MEDIUM | -| `backend/config/config.go` | Crossfade duration setting | LOW | -| `backend/events/events.go` | Potentially `CrossfadeStarted` event | LOW | -| Speaker initialization | May need larger speaker buffer for crossfade overlap | MEDIUM | - -### Risk Mitigation - -- **Start with gapless only**, defer crossfade. Gapless is the higher-value feature. -- Gapless can be implemented by pre-decoding and using `beep.Seq()` to chain streamers — this is well-supported by beep. -- Crossfade is additive — build it on top of working gapless. -- The existing `BufferedStreamer` read-ahead pattern provides a foundation for pre-loading. - ---- - -## Feature 6: MusicBrainz Browser - -### Architecture - -**New package: `backend/musicbrainz/`** - -Read-only catalog browsing. The MusicBrainz API is rate-limited to **1 request per second** and requires a meaningful User-Agent header. - -**Confidence:** HIGH — MusicBrainz API docs confirmed. JSON format via `fmt=json` or `Accept: application/json`. - -```go -// backend/musicbrainz/client.go -type Client struct { - mu sync.Mutex - ctx context.Context - logger *slog.Logger - db *database.DB // for response caching - httpClient *http.Client - rateLimiter *time.Ticker // 1 req/sec - userAgent string -} - -const apiBaseURL = "https://musicbrainz.org/ws/2/" - -func (c *Client) SearchArtist(query string, limit, offset int) (*ArtistSearchResult, error) -func (c *Client) GetArtist(mbid string) (*Artist, error) -func (c *Client) GetArtistReleaseGroups(mbid string, limit, offset int) (*ReleaseGroupBrowse, error) -func (c *Client) GetReleaseGroup(mbid string) (*ReleaseGroup, error) -func (c *Client) GetRelease(mbid string) (*Release, error) -``` - -### Rate Limiting - -```go -// Enforce 1 request per second globally -func (c *Client) doRequest(url string) ([]byte, error) { - <-c.rateLimiter.C // Block until rate limit allows - // ... execute HTTP GET with User-Agent header ... -} -``` - -### Response Caching - -Cache MB API responses in SQLite to avoid redundant requests: +### playlist_tracks Rebuild (for phantom support) ```sql -CREATE TABLE musicbrainz_cache ( - url TEXT PRIMARY KEY, - response_json TEXT NOT NULL, - fetched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +CREATE TABLE playlist_tracks_new ( + id INTEGER PRIMARY KEY, + playlist_id INTEGER NOT NULL, + audio_file_id INTEGER, -- NOW NULLABLE + position INTEGER NOT NULL, + phantom_file_path TEXT, + phantom_title TEXT, + phantom_artist TEXT, + phantom_album TEXT, + FOREIGN KEY(playlist_id) REFERENCES playlists(id) ON DELETE CASCADE, + FOREIGN KEY(audio_file_id) REFERENCES audio_files(id) ON DELETE SET NULL ); ``` -Cache TTL: 24 hours for search results, 7 days for entity lookups (data changes infrequently). +Two-phase library removal: +1. Populate phantom metadata BEFORE deleting audio_files +2. Delete audio_files -> SET NULL triggers -> phantom columns preserve display info -### Integration Points +### track_metadata VIEW (updated) -| Touchpoint | Change | Risk | -|------------|--------|------| -| `backend/app.go` | Add MusicBrainz client to `FEBindings` | LOW | -| `backend/database/` | New `musicbrainz_cache` table, migration | LOW | -| `go.mod` | No new dependencies — use `net/http` from stdlib | LOW | -| Frontend | New `` component with search, artist, album views | MEDIUM | +Add `af.library_id` to SELECT list. Same JOIN structure. Consumers get library_id for filtering. -### Key Constraint +## Scan Pipeline Changes -The app is currently fully offline. MusicBrainz browsing introduces the first network dependency. Handle network errors gracefully — cache-first with fallback, clear "offline/error" states in the UI, timeouts on HTTP requests. +- `Scan()` -> `ScanLibrary(libraryID int64)` — accepts library ID, loads path from DB +- `ScanAllLibraries()` — sequential iteration, one at a time +- Orphan cleanup scoped to library being scanned +- Entity cache (artists, albums) remains per-scan and works correctly (shared entities) +- Progress events include library_id and library_name ---- +## Orphan Cleanup (Library Removal) -## Feature 7: Layout Customization System - -### Architecture - -**New package: `backend/layout/`** - -MusicBee-style section-based UI customization. The layout defines which component renders in each section of the UI. - -### Section Model - -The current `index.html` defines a fixed layout: +Reference-counting bottom-up deletes in single transaction: ``` -┌─────────────────────────────────────────┐ -│ header (title + search-bar) │ -├─────────┬───────────────────┬───────────┤ -│ sidebar │ main-panel │ queue- │ -│ │ (track-list) │ panel │ -│ │ │ │ -├─────────┴───────────────────┴───────────┤ -│ footer (now-playing + audio-player) │ -└─────────────────────────────────────────┘ +audio_files (library_id = X) -> DELETE +recordings (no remaining audio_files) -> DELETE +release_group_recordings (orphaned) -> DELETE +recording_genres (orphaned) -> DELETE +release_groups (no remaining recordings) -> DELETE +artist_credit (no remaining references) -> DELETE +artist_credit_artist (orphaned) -> DELETE +artists (no remaining credits) -> DELETE +genres (no remaining recording links) -> DELETE +cover_art (no remaining release_groups) -> DELETE ``` -Make sections configurable: +## FTS5 Search Index -```go -// backend/layout/layout.go -type Section struct { - ID string `toml:"ID" json:"id"` - Component string `toml:"Component" json:"component"` // component tag name - Visible bool `toml:"Visible" json:"visible"` -} +Contentless FTS5 (`content=''`) works naturally: +- Search queries JOIN `search_index` on `track_metadata` (which now has `library_id`) +- Library-filtered search: add `AND tm.library_id = ?` to WHERE clause +- Stale entries after library removal filtered out by JOIN (same as current orphan behavior) +- Consider migrating to `contentless_delete=1` (SQLite 3.43.0+) for per-row DELETE support -type Layout struct { - Sections []Section `toml:"Sections" json:"sections"` -} -``` +## Frontend Architecture -### Config Integration +- `libraryStore` gains: library list, active filter (null = all), persistence in localStorage +- Backend filtering (not frontend) — pass libraryID to backend queries +- `library-manager` component redesigned: library list view, add/remove/rename, per-library scan +- All browse views check active filter when fetching data +- New events: LibraryAdded, LibraryRemoved, LibraryRenamed +- Existing scan events gain library_id in payload -```toml -[Layout] -[[Layout.Sections]] -ID = "sidebar" -Component = "app-sidebar" -Visible = true +## Config Migration -[[Layout.Sections]] -ID = "main-panel" -Component = "track-list" -Visible = true +- TOML `[Library].DirectoryPath` read once during migration 6, inserted as default library +- Post-migration: library management through DB only +- `ScanConcurrency` moves per-library (DB column) with global default fallback +- `SetLibraryDirectory()` and `GetLibraryDirectory()` deprecated -[[Layout.Sections]] -ID = "right-panel" -Component = "queue-panel" -Visible = true -``` +## Build Order -### Frontend Implementation - -The layout engine lives in the frontend. It reads the layout config and dynamically instantiates components in their designated sections: - -```typescript -// frontend/src/layout/layout-engine.ts -class LayoutEngine { - private sectionMap: Map; - - applyLayout(config: LayoutConfig) { - for (const section of config.sections) { - const container = this.sectionMap.get(section.id); - if (container) { - container.innerHTML = ''; - if (section.visible) { - const el = document.createElement(section.component); - container.appendChild(el); - } - } - } - } -} -``` - -### Component Registry - -Each component declares its size constraints (min width, preferred width, etc.) so the layout engine can validate configurations: - -```typescript -interface LayoutComponent { - tagName: string; - displayName: string; - allowedSections: string[]; // which sections this can go in - minWidth?: number; - minHeight?: number; -} - -const COMPONENT_REGISTRY: LayoutComponent[] = [ - { tagName: 'track-list', displayName: 'Track List', allowedSections: ['main-panel'] }, - { tagName: 'cover-grid', displayName: 'Album Grid', allowedSections: ['main-panel'] }, - { tagName: 'queue-panel', displayName: 'Queue', allowedSections: ['right-panel', 'main-panel'] }, - // ... -]; -``` - -### Integration Points - -| Touchpoint | Change | Risk | -|------------|--------|------| -| `backend/config/config.go` | Add `Layout` config section | LOW | -| `frontend/index.html` | Replace hard-coded components with section containers | MEDIUM | -| `frontend/index.ts` | Initialize layout engine, apply config | MEDIUM | -| All existing components | No changes — they're already self-contained Web Components | LOW | - ---- - -## Feature 8: Plugin System - -### Architecture - -**New package: `backend/plugin/`** - -This is the most complex architectural addition. The plugin system provides extensibility hooks for both backend logic and frontend UI. - -### Plugin Loading - -Plugins are directories in `~/.local/share/yellowjacket/plugins/`, each containing: -- `manifest.json` — name, version, entry points, permissions -- `main.js` — frontend code (Lit component) -- `backend.go` (optional) — Go plugin via `plugin` package or WASM - -**Recommended approach for v1.1: JavaScript-only plugins.** Go's `plugin` package has severe limitations (Linux-only, same Go version required, no unloading). WASM is possible but adds complexity. JS-only plugins can: -- Register new UI components -- Subscribe to events -- Call exposed backend APIs via Wails bindings -- Add sidebar items, context menu entries, toolbar buttons - -### Plugin API Surface - -```typescript -// frontend/src/plugin/api.ts -interface YellowJacketAPI { - // Events - on(event: string, callback: Function): void; - emit(event: string, data: any): void; - - // Player - player: { - play(): void; - pause(): void; - seek(seconds: number): void; - getState(): PlayerState; - }; - - // Queue - queue: { - addTrack(path: string): void; - getState(): QueueState; - }; - - // Library - library: { - search(query: string): Promise; - getTrackMetadata(path: string): Promise; - }; - - // UI - ui: { - registerSidebarItem(item: SidebarItem): void; - registerContextMenuItem(item: ContextMenuItem): void; - registerComponent(tagName: string, component: typeof LitElement): void; - }; -} -``` - -### Plugin Lifecycle - -``` -1. App startup → scan plugins directory -2. Parse manifest.json for each plugin -3. Validate permissions -4. Load JS entry point in sandboxed context -5. Call plugin.init(api) with the API surface -6. Plugin registers its components/handlers -7. App shutdown → call plugin.destroy() for each -``` - -### Sandboxing - -Plugins run in the same webview context (no iframe sandbox — too restrictive for Lit components). Instead, use an API-mediated approach: plugins can only interact with the app through the provided API object, not by reaching into internal stores or DOM directly. - -### Backend Plugin Hooks - -For backend extensibility, expose hooks rather than full plugin loading: - -```go -// backend/plugin/hooks.go -type Hooks struct { - OnTrackChanged []func(trackInfo player.TrackInfo) - OnLibraryScanDone []func(metrics *library.ScanMetrics) - OnConfigChanged []func(key string, value any) -} -``` - -### Integration Points - -| Touchpoint | Change | Risk | -|------------|--------|------| -| `backend/app.go` | Plugin loader initialization | MEDIUM | -| `backend/plugin/` | New package with loader, manifest parser, hook registry | HIGH — significant new code | -| `frontend/src/plugin/` | New directory with API, loader, registry | HIGH | -| `frontend/index.ts` | Plugin initialization after DOM ready | MEDIUM | -| Security | Plugin code is untrusted — API surface must be carefully scoped | HIGH | - -### v1.1 Scope Recommendation - -For v1.1, implement the **foundation**: -1. Plugin directory scanning + manifest parsing -2. JS plugin loading mechanism -3. Core API surface (events, player, queue) -4. One example plugin demonstrating the pattern - -Defer to later: backend Go plugins, WASM plugins, plugin marketplace, permissions system. - ---- - -## Patterns to Follow - -### Pattern 1: Two-Phase Initialization for New Packages - -**What:** All new packages that need Wails runtime follow `New*()` + `SetContext()`. - -**When:** Any new struct that emits events or uses Wails dialogs. - -**Example:** -```go -// backend/tageditor/tageditor.go -func NewTagEditor(logger *slog.Logger, db *database.DB) *TagEditor { - return &TagEditor{ - logger: logger.WithGroup("tageditor"), - db: db, - } -} - -func (te *TagEditor) SetContext(ctx context.Context) { - te.mu.Lock() - defer te.mu.Unlock() - te.ctx = ctx -} -``` - -### Pattern 2: Event-Driven Frontend Sync - -**What:** Backend emits events; frontend stores subscribe. - -**When:** Any state change that the frontend needs to reflect. - -**Example:** -```go -// Backend emits -runtime.EventsEmit(te.ctx, events.TagsEdited, map[string]any{ - "filePaths": affectedPaths, -}) - -// Frontend subscribes -EventsOn(Events.TagsEdited, (data: {filePaths: string[]}) => { - // Refresh affected track displays -}); -``` - -### Pattern 3: Config Section Pattern - -**What:** New config sections follow the `ApplyDefaults()` + `Validate()` pattern. - -**When:** Any new user-configurable setting. - -**Example:** -```go -// backend/shortcuts/config.go -type Config struct { - Bindings []Shortcut `toml:"Bindings"` -} - -func (c *Config) ApplyDefaults() { /* ... */ } -func (c *Config) Validate() error { /* ... */ } -``` - -### Pattern 4: Database Migration for New Tables - -**What:** New tables use `CREATE TABLE IF NOT EXISTS` in schema files + `PRAGMA user_version` migration for any ALTER operations. - -**When:** Adding new persistent data. - -**Example:** Smart playlists table in `backend/database/sql/schemas/smart_playlists.sql` with `CREATE TABLE IF NOT EXISTS`, plus Migration 6 in `database.go` for any column additions. - ---- - -## Anti-Patterns to Avoid - -### Anti-Pattern 1: Frontend-Owned State - -**What:** Storing authoritative state in frontend stores rather than the backend. - -**Why bad:** Violates the single-source-of-truth principle. State gets out of sync on refresh, loses persistence. - -**Instead:** All state changes go through backend. Frontend stores are mirrors. - -### Anti-Pattern 2: Direct DB Access from New Packages - -**What:** New packages opening their own DB connections or using raw `sql.DB` directly. - -**Why bad:** Violates single-writer constraint. Bypasses sqlc type safety. - -**Instead:** All DB access goes through the shared `*database.DB` instance with sqlc-generated queries. Use `db.BeginTx()` for transactions. Only use raw queries for dynamic SQL (smart playlists), with `// SAFETY:` comments. - -### Anti-Pattern 3: Circular Package Dependencies - -**What:** `tageditor` importing `library` which imports `tageditor`. - -**Why bad:** Go doesn't allow circular imports. - -**Instead:** Use interface-based decoupling (like `TrackLoader` interface) or hook patterns (like `RescanHooks`). The tageditor can accept a `LibraryRefresher` interface rather than importing the library package. - -### Anti-Pattern 4: Blocking the Wails Event Loop - -**What:** Long-running operations in Wails binding methods without goroutines. - -**Why bad:** Freezes the UI. - -**Instead:** Long operations (tag writing, MB API calls, scan) run in goroutines and emit progress events. The binding method returns immediately or returns a "started" acknowledgement. - -### Anti-Pattern 5: Uncontrolled HTTP Requests - -**What:** MusicBrainz API calls without rate limiting. - -**Why bad:** IP gets blocked. MusicBrainz enforces 1 req/sec strictly. - -**Instead:** Single rate-limited HTTP client with `time.Ticker`. Cache all responses. Queue requests. - ---- - -## Build Order (Dependency-Aware) - -### Phase 1: Independent Foundations - -These features have no inter-dependencies and can be built in any order: - -1. **Scan Cancellation** — Smallest change. Modifies existing code minimally. Tests scan pipeline resilience. -2. **Customizable Keyboard Shortcuts** — Config + new package + frontend keydown listener. No data model changes. - -### Phase 2: Data Model Extensions - -These features add new database tables/queries: - -3. **Tag Editing** — New dependency (`bogem/id3v2`), new DB queries, new package. Validates that tag write → DB update → event → frontend refresh pipeline works. -4. **Smart Playlists** — New table, new package, dynamic SQL. Independent of tag editing but benefits from validated DB migration patterns. - -### Phase 3: Complex Backend Changes - -5. **Gapless Playback** — Core audio pipeline modification. Start with gapless, add crossfade later. Most technically risky feature. -6. **MusicBrainz Browser** — First network feature. HTTP client, caching, rate limiting. Independent of other features. - -### Phase 4: Extensibility Foundations - -These are the "foundation" features — functional but not necessarily feature-complete: - -7. **Layout Customization** — Requires all existing components to be working well. Modifies `index.html` structure. -8. **Plugin System** — Must be last — it depends on having a stable API surface from all other features. - -### Rationale for This Order - -- **Scan cancellation first** because it's a quick win that validates context cancellation patterns used throughout. -- **Shortcuts early** because they're simple config + dispatch with no data model changes. -- **Tag editing before smart playlists** because smart playlists query against track metadata that tag editing modifies — testing both together reveals integration issues. -- **Gapless after tag editing** because tag editing validates the "modify player behavior → event → frontend update" pipeline at a simpler level. -- **MusicBrainz after gapless** because it introduces network complexity that's orthogonal to audio — building it later keeps the audio work focused. -- **Layout and plugins last** because they're meta-features that wrap existing features. Building them last means the thing they're wrapping is stable. - ---- - -## Wails Bridge Implications - -### New FEBindings - -Every new backend service added to `FEBindings` in `app.go` generates TypeScript stubs in `frontend/wailsjs/go/`. After adding new bindings: - -```bash -make generate # regenerates Wails bindings + sqlc + events codegen -``` - -### New Events (All Features) - -Estimated new events across all features: - -```go -// Tag editing -TagsEdited = "TagsEdited" -TagEditFailed = "TagEditFailed" - -// Scan cancellation -LibraryScanCancelled = "LibraryScanCancelled" - -// Smart playlists -SmartPlaylistCreated = "SmartPlaylistCreated" -SmartPlaylistUpdated = "SmartPlaylistUpdated" -SmartPlaylistDeleted = "SmartPlaylistDeleted" - -// Shortcuts -ShortcutConfigChanged = "ShortcutConfigChanged" - -// MusicBrainz -MusicBrainzSearchComplete = "MusicBrainzSearchComplete" - -// Layout -LayoutConfigChanged = "LayoutConfigChanged" - -// Gapless/Crossfade -CrossfadeConfigChanged = "CrossfadeConfigChanged" -``` - -All go through the existing AST-based codegen pipeline (`go generate` + pre-commit hook). - -### Database Migrations - -New migration sequence (current version = 5): - -| Migration | Feature | What | -|-----------|---------|------| -| 6 | Smart Playlists | `CREATE TABLE smart_playlists` | -| 7 | MusicBrainz | `CREATE TABLE musicbrainz_cache` | -| 8 | Shortcuts | Config-based (no table needed) | -| 9 | Layout | Config-based (no table needed) | -| 10 | Plugins | `CREATE TABLE plugin_state` (optional, for persistent plugin data) | - -Most features use config (TOML) rather than DB for their settings, keeping migrations minimal. - ---- - -## Scalability Considerations - -| Concern | Current (~1K tracks) | At 10K tracks | At 100K tracks | -|---------|---------------------|---------------|----------------| -| Smart playlist eval | <10ms | <100ms | May need indexing | -| Tag edit (single file) | ~50ms | ~50ms | ~50ms (file-level) | -| Tag edit (batch 100) | — | ~5s (serial writes) | Same | -| FTS5 re-index (tag edit) | ~1ms | ~1ms | ~1ms (single row) | -| MB API browse | Network-bound | Same | Same | -| Layout render | ~5ms | Same | Same | - -The main scalability concern is **smart playlist evaluation** at large library sizes. The `track_metadata` VIEW already has a 5-table JOIN. Adding WHERE clauses for smart playlist rules adds no extra JOINs — the VIEW handles the complexity. SQLite's query planner should handle 100K rows with proper indexes. - ---- - -## Sources - -- Codebase analysis: Complete read of all Go packages and TypeScript sources (2026-03-06) -- beep v2.1.1 API: `pkg.go.dev/github.com/gopxl/beep/v2` — Mixer, Seq, Buffer, Ctrl types confirmed -- MusicBrainz API: `musicbrainz.org/doc/MusicBrainz_API` — rate limiting (1 req/sec), JSON format, entity types -- dhowden/tag: Read-only library confirmed from source (`tag.ReadFrom` only, no write methods) -- SQLite WAL mode + single writer: Existing `database.go` configuration confirmed -- Wails v2 binding generation: Existing `app.go` FEBindings pattern confirmed - ---- - -*Architecture research: 2026-03-06* +1. Schema & Migration (foundation) +2. Backend scan pipeline (per-library scanning) +3. Backend API (CRUD, filtered queries, events) +4. Frontend (library manager, filter, store updates) diff --git a/.planning/research/FEATURES.md b/.planning/research/FEATURES.md index d0b2abc..68b5062 100644 --- a/.planning/research/FEATURES.md +++ b/.planning/research/FEATURES.md @@ -1,550 +1,73 @@ -# Feature Landscape: v1.1 Features & Extensibility +# Features Research: Multi-Library Support -**Domain:** Desktop music player — new capabilities milestone -**Researched:** 2026-03-06 -**Confidence:** HIGH (grounded in codebase analysis, official documentation, established desktop music player patterns) +**Researched:** 2026-03-08 +**Confidence:** HIGH ---- +## How Mature Players Handle Multiple Libraries -## Overview +| Player | Model | Libraries Separate? | Cross-Library Playlists? | +|--------|-------|--------------------|-----------------------| +| foobar2000 | Multiple folders, one merged library | No — all folders merge | N/A (one library) | +| MusicBee | Multiple folders per library, separate library databases | Yes (separate DBs) | No | +| Plex | Separate typed libraries, multiple folders each | Yes (fully isolated) | No | +| Jellyfin | Virtual collections with multiple paths | Yes | No | +| Navidrome | Named libraries with user access control | Yes (with multi-select merge) | Yes | +| Roon | Watched folders, one unified library | No — all merge | N/A (one library) | -This research covers 8 feature areas for YellowJacket v1.1: tag editing, scan cancellation, smart playlists, customizable keyboard shortcuts, gapless playback + crossfade, MusicBrainz browser, layout customization, and plugin system. Each is categorized as table stakes, differentiator, or anti-feature relative to the desktop music player domain. +### Desktop Player Pattern (foobar2000, Roon) ---- +All folders contribute to one unified library. No folder-level filtering in default UI. User never thinks about "which folder." -## 1. Tag Editing +### Server Pattern (Plex, Jellyfin, Navidrome) -### Table Stakes +Separate libraries with access control. More suited to multi-user server apps. -| Feature | Why Expected | Complexity | Dependencies | -|---------|--------------|------------|--------------| -| Edit title, artist, album, genre, year, track number | Every music manager (MusicBee, foobar2000, Clementine, Strawberry) supports this. Users expect to correct metadata without leaving the app. | MEDIUM | Existing metadata extraction pipeline, new tag writing libraries | -| Edit single track | Right-click → edit properties is the universal pattern | LOW | Tag writing backend | -| Batch edit multiple tracks | Select multiple → edit shared fields (e.g., set all to same album). This is the primary workflow for fixing album imports. | MEDIUM | Single-track editing must work first | -| Write changes to actual audio files | Tags must persist to the file on disk, not just the DB. Users expect changes to survive re-imports and transfers to other players. | MEDIUM | Tag writing libraries (format-specific) | -| Update DB after tag write | After writing tags to file, the DB must reflect the new metadata without requiring a full rescan. | LOW | Existing DB update queries | -| Cover art assignment | Set/replace embedded cover art from an image file | MEDIUM | Image handling + tag writing | +### YellowJacket Fit -### Differentiators +Desktop player = **merged by default**, with optional filter. Follows foobar2000/Roon pattern but adds Navidrome-style library selector for power users. -| Feature | Value Proposition | Complexity | Dependencies | -|---------|-------------------|------------|--------------| -| Undo/redo for tag edits | Safety net — rare in music players, very valued when present | HIGH | Requires edit history tracking | -| Auto-capitalize/clean tag values | Consistent library appearance with minimal effort | LOW | String utilities | -| Filename-to-tag inference | Parse "Artist - Title.mp3" patterns to pre-fill fields | MEDIUM | Regex/pattern engine | -| Tag-to-filename rename | Rename files based on tag template (e.g., "%artist% - %title%.%ext%") | HIGH | File system operations, template engine | +## Feature Classification -### Anti-Features +### Table Stakes (Must Have) -| Anti-Feature | Why Avoid | What to Do Instead | -|--------------|-----------|-------------------| -| Auto-tag from online DB in tag editor | Conflates two features — tag editing and metadata lookup. MusicBrainz browser is the separate feature for this. | Keep tag editing purely manual; MusicBrainz browser is the lookup tool | -| Destructive batch operations without confirmation | Mass edits can corrupt a library. | Always show preview/confirmation dialog for batch edits | -| Writing tags during playback of that file | File locking conflicts on Windows; potential corruption on any OS | Queue the write for after playback stops, or copy-on-write | +| Feature | Complexity | Dependencies | +|---------|-----------|-------------| +| Add multiple watched folders | Low | Config to DB migration | +| Unified merged view (default) | Medium | All browse views, search aggregate | +| Per-folder independent scan | Medium | Scan pipeline scoping | +| Remove folder without data loss | Low | Phantom tracks for playlists | +| Folder status indicators | Low | Scan event system | +| Graceful offline handling | Medium | Guard orphan cleanup | +| Existing playlists unaffected | Low | File-path references already work | -### Implementation Notes +### Differentiators (Nice to Have) -**Tag writing requires format-specific libraries (the existing `dhowden/tag` is read-only):** +| Feature | Complexity | Dependencies | +|---------|-----------|-------------| +| Filter/narrow by source folder | Medium | UI filter chip, query-level filtering | +| Named libraries | Low | DB stores name + path | +| Per-folder scan concurrency | Low | Extend existing ScanConcurrency per library | +| Folder health dashboard | Medium | Aggregate scan metrics | -- **MP3 (ID3v2):** `github.com/bogem/id3v2/v2` — mature, pure Go, supports ID3v2.3/2.4 read+write, handles text frames, pictures, comments. Confirmed: `tag.Open()` → `tag.SetArtist()` → `tag.Save()` pattern. v2.1.4 is current. -- **FLAC (Vorbis Comments):** `github.com/go-flac/go-flac` + `github.com/go-flac/flacvorbis` — parse FLAC file, modify vorbis comment metadata blocks, save back. Confirmed: `flac.ParseFile()` → modify `Meta` slice → `f.Save()`. v1.0.0/v0.2.0 current (v2 exists). -- **OGG Vorbis:** No mature pure-Go write library exists. Options: (a) skip OGG tag writing initially, (b) use `go-flac/flacvorbis`-style approach with raw vorbis comment manipulation if a library surfaces, or (c) shell out to `vorbiscomment` CLI tool. -- **WAV:** WAV metadata (INFO chunks, ID3 headers) is rarely edited. Skip for v1.1. +### Anti-Features (Do NOT Build) -**Critical constraint:** The existing `dhowden/tag` library is read-only. Tag writing is a completely separate code path requiring new dependencies. Tag reading continues through `dhowden/tag`; writing uses format-specific libraries. +| Anti-Feature | Why Avoid | +|--------------|-----------| +| Separate databases per library | Breaks unified browse, doubles query logic | +| User/access control per library | Desktop app is single-user | +| Auto-merge/deduplicate across folders | Complex, error-prone, unexpected | +| Library-specific settings/themes | Over-engineering | -**DB sync pattern:** After writing tags to file, update the specific DB rows rather than triggering a full rescan. Extract the new metadata from the written file (or trust the values just written), update the `recordings`, `artists`, `release_groups`, and `audio_files` tables, then emit a `TrackMetadataChanged` event to sync the frontend. +## Library Removal Patterns ---- +All players that support library removal: +1. Show confirmation dialog +2. Remove tracks from DB (or mark as missing) +3. Handle playlist references (delete, mark phantom, or leave as-is) +4. Don't delete files from disk -## 2. Scan Cancellation +**YellowJacket approach:** Phantom tracks (preserving metadata) for playlists. Queue tracks cascade-deleted (ephemeral). Orphan cleanup for shared entities via reference counting. -### Table Stakes +## Offline Handling -| Feature | Why Expected | Complexity | Dependencies | -|---------|--------------|------------|--------------| -| Cancel button during scan | Large libraries take minutes to scan. Users expect to be able to stop a scan in progress. Every file manager and media player with scanning provides this. | LOW | Existing scan pipeline with `context.Context` | -| Graceful stop (don't corrupt DB) | Cancellation must not leave the DB in an inconsistent state. Complete in-flight transactions, skip remaining files. | LOW | Existing transaction batching | -| Scan progress reporting | Users need to see what's happening — "Processing 340/2000 files" — to decide whether to wait or cancel. | LOW | Existing `ScanProgress` event (already partially implemented) | - -### Differentiators - -| Feature | Value Proposition | Complexity | Dependencies | -|---------|-------------------|------------|--------------| -| Pause and resume scan | Stop temporarily, resume later without re-scanning already-processed files | HIGH | Would need scan state persistence | -| Background scan with low priority | Scan without impacting playback or UI responsiveness | LOW | Already partially handled by worker pool concurrency tuning | - -### Anti-Features - -| Anti-Feature | Why Avoid | What to Do Instead | -|--------------|-----------|-------------------| -| Immediate hard kill (kill goroutines) | Data corruption risk — partial writes, broken entity caches | Use context cancellation for cooperative shutdown | -| Auto-cancel on any error | Users want the scan to continue past individual file failures | Continue scanning, accumulate warnings (already the pattern) | - -### Implementation Notes - -**The existing scan pipeline already uses `context.Context` — `l.ctx` is available throughout the scan.** The implementation pattern is straightforward: - -1. Create a cancellable context: `scanCtx, cancelScan := context.WithCancel(l.ctx)` -2. Store `cancelScan` so the frontend can trigger it via a Wails binding (e.g., `Library.CancelScan()`) -3. Check `scanCtx.Done()` in the filesystem walk loop, the worker pool dispatch, and the DB writer -4. On cancellation, the `errgroup` returns `context.Canceled`, which is caught and treated as a clean stop -5. Emit `LibraryScanCancelled` event (distinct from `LibraryScanComplete`) - -**Key insight:** The existing scan already uses `errgroup` which respects context cancellation. The DB writer goroutine processes whatever is in its batch channel, so in-flight batches complete cleanly. The only new code needed is: (a) storing/exposing the cancel function, (b) checking context in the walk loop, (c) a new event for cancellation. - -**Complexity is LOW** because the architecture already supports this pattern. The scan pipeline's multi-phase design means cancellation at any phase is naturally bounded. - ---- - -## 3. Smart Playlists - -### Table Stakes - -| Feature | Why Expected | Complexity | Dependencies | -|---------|--------------|------------|--------------| -| Filter by genre | "All Jazz tracks" — the most basic smart playlist rule | LOW | Existing genre data in DB | -| Filter by year/year range | "Tracks from 1990-1999" | LOW | Existing year field in DB | -| Filter by artist | "All tracks by Artist X" | LOW | Existing artist data | -| Combine multiple rules (AND) | "Jazz tracks from the 1990s" — users expect to stack filters | MEDIUM | Rule evaluation engine | -| Auto-update when library changes | Smart playlists should refresh when tracks are added/removed. This is the defining feature vs. manual playlists. | MEDIUM | Event subscription to library changes | -| Name and save smart playlists | Persist rule definitions, show in sidebar alongside regular playlists | LOW | New DB table for rule definitions | - -### Differentiators - -| Feature | Value Proposition | Complexity | Dependencies | -|---------|-------------------|------------|--------------| -| Filter by play count | "Most played" / "Never played" — requires play count tracking (not currently implemented) | MEDIUM | New `play_count` column or table | -| Filter by date added | "Recently added" — very popular smart playlist | LOW | Existing file modification time or new `added_at` column | -| Filter by rating | Requires rating system (not currently implemented) | MEDIUM | New rating feature | -| OR logic and nested groups | "(Genre=Jazz OR Genre=Blues) AND Year>1980" — powerful but complex UI | HIGH | Recursive rule evaluation, complex UI builder | -| Random/limit results | "Random 50 Jazz tracks" — playlist-as-radio | LOW | SQL `ORDER BY RANDOM() LIMIT N` | -| Sort order in rules | "Newest first" / "Alphabetical by artist" | LOW | SQL `ORDER BY` clause | - -### Anti-Features - -| Anti-Feature | Why Avoid | What to Do Instead | -|--------------|-----------|-------------------| -| Full SQL WHERE clause as input | Exposes DB internals, injection risk, terrible UX | Structured rule builder with defined fields and operators | -| Complex nested boolean logic in v1 | Overwhelms users, complex UI, rarely used | Start with flat AND rules; add OR/nesting later if demanded | -| Real-time updating during playback | Unnecessary overhead — smart playlists don't need sub-second freshness | Refresh on library scan completion and on explicit refresh | - -### Implementation Notes - -**Rule model — keep it simple for v1:** - -``` -SmartPlaylistRule { - Field: "genre" | "year" | "artist" | "album" | "title" | "date_added" - Operator: "equals" | "not_equals" | "contains" | "greater_than" | "less_than" | "between" - Value: string (or string pair for "between") -} - -SmartPlaylist { - ID: int64 - Name: string - Rules: []SmartPlaylistRule // all ANDed together - SortField: string (optional) - SortOrder: "asc" | "desc" - Limit: int (0 = unlimited) -} -``` - -**Storage:** New `smart_playlists` table (id, name, rules_json, sort_field, sort_order, limit_count) with rules stored as JSON in a TEXT column. This avoids a complex relational schema for rules and is trivially extensible. - -**Query generation:** Each rule maps to a SQL WHERE clause fragment. Rules are joined with AND. The existing `track_metadata` VIEW provides all the needed columns for filtering. Generated SQL uses parameterized queries (NOT string concatenation) to prevent injection. - -**Refresh strategy:** Smart playlists evaluate lazily — results are computed on access and cached. Cache is invalidated on `LibraryScanComplete` events. This avoids expensive re-evaluation on every library change. - -**Depends on:** Existing `track_metadata` VIEW, playlist sidebar UI, event system. - ---- - -## 4. Customizable Keyboard Shortcuts - -### Table Stakes - -| Feature | Why Expected | Complexity | Dependencies | -|---------|--------------|------------|--------------| -| Play/pause hotkey | Space bar is universal; must work | LOW | Existing player controls | -| Next/previous track | Arrow keys or media key equivalents | LOW | Existing queue navigation | -| Volume up/down | Standard audio app functionality | LOW | Existing volume control | -| Mute toggle | Expected in any audio application | LOW | Existing mute functionality | -| Search focus | Ctrl+F or / to focus search — standard in any list-heavy app | LOW | Existing search bar | -| Default keybindings that work out of box | Users shouldn't have to configure anything to get basic shortcuts | LOW | Hardcoded defaults with override capability | - -### Differentiators - -| Feature | Value Proposition | Complexity | Dependencies | -|---------|-------------------|------------|--------------| -| Full customization UI | Visual keybinding editor with conflict detection | MEDIUM | Settings page extension | -| Import/export keybindings | Share/backup custom configs | LOW | TOML serialization (already used for config) | -| Scoped shortcuts (global vs. component-specific) | Different bindings when focus is in search vs. track list | MEDIUM | Focus tracking | -| "When focused" context awareness | Arrows navigate track list when it's focused, but control volume when player is focused | MEDIUM | Component focus management | - -### Anti-Features - -| Anti-Feature | Why Avoid | What to Do Instead | -|--------------|-----------|-------------------| -| Global OS-level hotkeys (outside app window) | Platform-specific, conflicts with OS shortcuts, security concerns on Wayland | App-scoped shortcuts only; MPRIS2 handles media keys | -| Vim-mode or complex modal keybindings | Niche appeal, confusing for 99% of users | Simple single/modifier key combos (Ctrl+X, Shift+X) | -| Shortcut for every possible action | Overwhelming configuration UI | Cover the 10-15 most common actions; rest accessible via menus | - -### Implementation Notes - -**Architecture — event-driven, backend-aware:** - -The shortcut system has two layers: -1. **Frontend key listener:** Captures keyboard events at the document level, maps keystrokes to action names using a binding table -2. **Action dispatch:** Frontend calls the appropriate Wails binding or emits a frontend event for UI-only actions - -**Binding table structure:** - -```typescript -interface KeyBinding { - action: string; // "play_pause", "next_track", "volume_up", etc. - key: string; // "Space", "ArrowRight", etc. (KeyboardEvent.key) - modifiers: string[]; // ["ctrl"], ["shift"], ["ctrl", "shift"], [] - scope?: string; // "global" | "tracklist" | "queue" (optional, default "global") -} -``` - -**Storage:** Add `[Shortcuts]` section to TOML config. Default bindings are hardcoded; user overrides merge on top. Config change emits `ShortcutConfigChanged` event. - -**Conflict detection:** When user changes a binding, check for conflicts within the same scope. Show warning if two actions share the same keystroke. - -**Default bindings (the 12 essentials):** - -| Action | Default Key | Scope | -|--------|-------------|-------| -| Play/Pause | Space | global | -| Stop | . (period) | global | -| Next Track | Ctrl+Right | global | -| Previous Track | Ctrl+Left | global | -| Volume Up | Ctrl+Up | global | -| Volume Down | Ctrl+Down | global | -| Mute | M | global | -| Search Focus | Ctrl+F | global | -| Toggle Queue | Q | global | -| Toggle Shuffle | S | global | -| Toggle Repeat | R | global | -| Select All (track list) | Ctrl+A | tracklist | - -**Key insight:** Keyboard shortcuts must NOT interfere with text input. When a text input or textarea has focus, the shortcut system must be disabled (except for Escape to blur). This is the #1 pitfall in keyboard shortcut implementations. - ---- - -## 5. Gapless Playback + Crossfade - -### Table Stakes - -| Feature | Why Expected | Complexity | Dependencies | -|---------|--------------|------------|--------------| -| Gapless playback (no silence between tracks) | Expected by any serious music listener. Albums are meant to flow. Strawberry, foobar2000, Deadbeef, Audacious all support this. | HIGH | Fundamental change to audio pipeline | -| Crossfade setting (on/off, duration) | Standard feature in every modern music player. Even basic mobile players have this. | MEDIUM | Gapless infrastructure + mixer | -| Crossfade duration control | Users expect 1-10 second configurable fade | LOW | UI slider + config storage | -| Gapless without crossfade (default) | Pure gapless (no overlap) should be the default. Crossfade is opt-in. | HIGH | Pre-decode/buffer next track | - -### Differentiators - -| Feature | Value Proposition | Complexity | Dependencies | -|---------|-------------------|------------|--------------| -| Per-album gapless (auto-detect live albums) | Disable crossfade within albums, enable between albums | MEDIUM | Album boundary detection in queue | -| ReplayGain normalization | Consistent volume across tracks from different sources | HIGH | ReplayGain tag parsing + volume adjustment | -| Fade-in on play, fade-out on pause | Smoother start/stop experience | LOW | Volume envelope on play/pause | - -### Anti-Features - -| Anti-Feature | Why Avoid | What to Do Instead | -|--------------|-----------|-------------------| -| DSP effects chain (equalizer, reverb, etc.) | Scope explosion — not part of gapless/crossfade | Defer to plugin system if ever needed | -| Crossfade for all transitions (including manual skip) | Crossfade on skip feels sluggish | Only crossfade on auto-advance; manual skip is instant | -| Pre-loading entire tracks into memory | Memory explosion with FLAC files (50-100MB per track) | Buffer only the crossfade overlap region (last/first N seconds) | - -### Implementation Notes - -**This is the highest-complexity feature in v1.1.** The current audio pipeline plays one track at a time with a single streamer chain. Gapless playback requires pre-decoding the next track and seamlessly transitioning. - -**Current pipeline:** `file → decode → resample → BufferedStreamer → Ctrl → Volume → Speaker` - -**Gapless pipeline (conceptual):** -1. When current track is N seconds from ending, pre-load next track's decoder + resampler -2. For pure gapless: use `beep.Seq()` to chain current and next streamer — but Seq doesn't support the pre-decode timing -3. For crossfade: use `beep.Mixer` to overlap the fade-out of current with fade-in of next - -**beep library support:** -- `beep.Mixer` — adds/mixes multiple streamers. This is the foundation for crossfade. -- `beep.Seq()` — sequences streamers end-to-end. Foundation for gapless without crossfade. -- `effects.Volume` — volume control already used; can create fade curves by adjusting volume over time. -- `beep.Take()` — extract N samples from a streamer. Useful for defining crossfade regions. - -**Architecture change required:** -- The `Player` must manage TWO streamer chains simultaneously during crossfade -- A `TransitionManager` or equivalent coordinates pre-loading the next track -- The `playbackFinishedHandler` (callback from beep when track ends) must trigger next-track pre-loading rather than waiting for the callback -- The `Queue` must expose a "peek next" capability (already has `tracks` and `currentIndex`) - -**Crossfade implementation sketch:** -``` -[Track A ~~~~~~~~ fade-out] - [fade-in ~~~~~~~~ Track B] - |-- overlap (N seconds) --| -``` -- Track A's volume ramps from 1.0 → 0.0 over N seconds -- Track B's volume ramps from 0.0 → 1.0 over N seconds -- Both feed into a `beep.Mixer` during the overlap period -- After overlap, Track A is closed, Track B continues alone - -**Config addition:** `[Playback]` section with `GaplessEnabled` (bool, default true), `CrossfadeEnabled` (bool, default false), `CrossfadeDurationMs` (int, default 3000, range 500-10000). - -**Critical constraint:** The beep `speaker.Play()` can only be called once; the speaker's mixer is the root. All track management must happen within the streamer chain that the speaker is already playing. This means using a persistent `beep.Mixer` as the root streamer, adding/removing track streamers from it. - ---- - -## 6. MusicBrainz Browser - -### Table Stakes - -| Feature | Why Expected | Complexity | Dependencies | -|---------|--------------|------------|--------------| -| Search artists by name | The entry point — user types artist name, gets results | MEDIUM | MusicBrainz API integration, HTTP client | -| View artist discography (release groups) | Browse albums/EPs/singles by an artist | MEDIUM | API browse: release-groups by artist | -| View album track listing | See what tracks are on a release | MEDIUM | API lookup: release with recordings | -| View album editions (releases within a release group) | Different pressings, reissues, deluxe editions | MEDIUM | API browse: releases by release-group | -| Rate limiting compliance | MusicBrainz requires max 1 request/second with meaningful User-Agent | LOW | HTTP rate limiter, User-Agent header | -| Offline-safe (read-only, no writes) | Read-only browsing — no MusicBrainz account needed | LOW | No authentication required for reads | - -### Differentiators - -| Feature | Value Proposition | Complexity | Dependencies | -|---------|-------------------|------------|--------------| -| Link local tracks to MusicBrainz recordings | Associate library tracks with MBIDs for definitive identity | HIGH | Matching algorithm, DB schema changes | -| Show cover art from Cover Art Archive | Display album art from MusicBrainz's linked image archive | MEDIUM | coverartarchive.org API | -| Cache API responses locally | Avoid re-fetching on every browse session | MEDIUM | SQLite cache table with TTL | -| Search recordings | Find specific songs across all releases | LOW | MusicBrainz recording search API | - -### Anti-Features - -| Anti-Feature | Why Avoid | What to Do Instead | -|--------------|-----------|-------------------| -| Auto-tag from MusicBrainz | This is Picard's domain — extremely complex matching logic | Read-only browsing only. Users can manually apply info from browse to tag editor. | -| Write data to MusicBrainz | Requires OAuth, community guidelines compliance, edit approval | Strictly read-only | -| Download/stream from MusicBrainz | MusicBrainz is a metadata database, not a music source | Display metadata only | -| Background MusicBrainz scanning of entire library | Rate limiting makes this impractical (1 req/sec = 3600 tracks/hour max) | On-demand browsing only | - -### Implementation Notes - -**MusicBrainz API:** REST API at `https://musicbrainz.org/ws/2/`. JSON format via `fmt=json` parameter. No API key required, but must set meaningful User-Agent header: `YellowJacket/ (contact-url-or-email)`. - -**Rate limiting:** Strict 1 request/second. Implement with a `time.Ticker`-based rate limiter in the Go backend. All API calls go through a single rate-limited HTTP client. - -**Go libraries available:** -- `github.com/michiwend/gomusicbrainz` — Go client, but may be outdated -- `go.uploadedlobster.com/musicbrainzws2` — another Go client on SourceHut -- **Recommended: Build a thin HTTP client** — the API is simple REST/JSON. A custom client with rate limiting, User-Agent, and JSON parsing is ~200 lines and avoids third-party dependency risk. - -**API patterns needed for read-only browsing:** -1. **Search artist:** `GET /ws/2/artist?query=&fmt=json&limit=25` -2. **Artist discography:** `GET /ws/2/release-group?artist=&fmt=json&limit=100&inc=artist-credits` -3. **Release group releases:** `GET /ws/2/release?release-group=&fmt=json&inc=media+recordings` -4. **Release track listing:** `GET /ws/2/release/?fmt=json&inc=recordings+media+artist-credits` - -**Frontend architecture:** New view (`musicbrainz-browser` component) accessible from sidebar. Search bar, results list, detail panels for artist/album/release. Navigation is drill-down: search → artist → release group → release → tracks. - -**Caching strategy:** Cache API responses in SQLite (`mb_cache` table: url, response_json, fetched_at). TTL of 24 hours for search results, 7 days for entity lookups (MusicBrainz data changes infrequently). Cache reduces API calls and improves responsiveness. - -**This is YellowJacket's first network feature** — the app is currently fully offline. Need to handle: network errors gracefully, timeout configuration, offline mode (show cached data), user notification of network status. - ---- - -## 7. Layout Customization System - -### Table Stakes - -| Feature | Why Expected | Complexity | Dependencies | -|---------|--------------|------------|--------------| -| Resizable panels (sidebar, queue, main) | Basic expectation in any multi-panel desktop app. Users want wider sidebar or hidden queue. | MEDIUM | CSS grid/flexbox with drag handles | -| Show/hide queue panel | Already partially implemented (queue toggle button exists) | LOW | Existing queue panel toggle | -| Show/hide sidebar sections | Collapse navigation sections user doesn't need | LOW | Sidebar configuration | -| Persist layout across restarts | Layout changes must survive app restart | LOW | TOML config section | - -### Differentiators - -| Feature | Value Proposition | Complexity | Dependencies | -|---------|-------------------|------------|--------------| -| Section-based component placement (MusicBee-style) | Users choose what goes where — put album art in sidebar, now-playing at top, etc. This is MusicBee's signature feature. | HIGH | Component registry, layout engine, size constraints | -| Component size constraints | Components declare min/max sizes; layout engine respects constraints | MEDIUM | Component metadata system | -| Layout presets | "Compact", "Full", "Mini player" — quick switch between configurations | MEDIUM | Preset definitions + switch mechanism | -| Detachable panels | Pop out queue or now-playing to separate window | HIGH | Wails multi-window support (limited in v2) | - -### Anti-Features - -| Anti-Feature | Why Avoid | What to Do Instead | -|--------------|-----------|-------------------| -| Free-form drag-and-drop layout | Overwhelming complexity, hard to make look good | Section-based: defined slots with selectable components | -| CSS theme editor | Users don't want to write CSS | Extend existing theme system (accent color, background shade) | -| Mobile-responsive layout | This is a desktop app with fixed minimum size | Optimize for 1024x768 minimum | - -### Implementation Notes - -**MusicBee-style layout means section-based composition:** - -The UI is divided into named sections (slots): -- `header` (top bar) -- `sidebar` (left panel) -- `main` (center content area) -- `footer` (bottom bar — now playing + player controls) -- `right-panel` (queue panel or other content) - -Each section has a list of components it can host. Components declare their size constraints (min width/height). Users configure which component goes in which section via a settings UI. - -**Implementation approach:** - -1. **Component registry:** Each component registers itself with metadata (name, description, supported sections, min/max size). This is a TypeScript Map, not a plugin system yet. -2. **Layout configuration:** Stored in TOML config under `[Layout]` section. Maps section names to component names. -3. **Layout renderer:** A root `` component reads config and instantiates the right components in the right sections using dynamic imports. -4. **Resize handles:** CSS resize or custom drag handles on section boundaries. Store widths/heights as percentages in config. - -**Start simple for v1.1:** -- Phase 1: Resizable panels (sidebar width, queue width) with drag handles + persistence -- Phase 2: Show/hide sections + layout presets -- Phase 3: Component-in-section customization (the full MusicBee-style system) - -The full section-based system is the v1.1 "foundation" — functional but not complete. - -**Depends on:** Config system (TOML), existing component architecture, CSS grid layout. - ---- - -## 8. Plugin System - -### Table Stakes - -| Feature | Why Expected | Complexity | Dependencies | -|---------|--------------|------------|--------------| -| Defined plugin API (what plugins can do) | Without clear API boundaries, plugins break on every update | HIGH | API design + stability commitment | -| Plugin loading/unloading | Install/remove plugins without rebuilding the app | HIGH | Dynamic loading mechanism | -| Plugin configuration | Plugins need their own settings that persist | MEDIUM | Extend config system | -| Plugin isolation (one plugin crash doesn't kill app) | Critical for stability | HIGH | Error boundaries, sandboxing | - -### Differentiators - -| Feature | Value Proposition | Complexity | Dependencies | -|---------|-------------------|------------|--------------| -| UI component plugins (custom panels, visualizations) | Plugins can add new views to the layout system | HIGH | Layout customization system + component registry | -| Backend hook plugins (custom metadata sources, scrobblers) | Plugins can intercept/extend backend operations | HIGH | Hook system in Go backend | -| Plugin marketplace/registry | Discover and install plugins | HIGH | External infrastructure | -| TypeScript/JavaScript plugin runtime | Lowest barrier to entry for plugin authors | MEDIUM | Webview already runs JS | - -### Anti-Features - -| Anti-Feature | Why Avoid | What to Do Instead | -|--------------|-----------|-------------------| -| Go plugin system (`plugin` package) | Linux-only, version-fragile, build-tag sensitive, widely considered broken | Use process-based or embedded scripting approach | -| Full filesystem access for plugins | Security nightmare | Sandboxed API with explicit permissions | -| Plugin binary distribution | Build reproducibility, platform issues | Source-based distribution (TypeScript/JS bundles) | -| Network access for plugins without user consent | Privacy concern | Require explicit network permission declaration | - -### Implementation Notes - -**Plugin systems in Go desktop apps are notoriously difficult.** The `plugin` package is Linux-only and requires exact build-tag matching. Wails v2 doesn't have a plugin framework. - -**Recommended approach for v1.1 "foundation":** - -1. **Frontend-first plugins (TypeScript):** - - Plugins are JS/TS bundles loaded dynamically into the webview - - They register with the component registry (layout system) to add UI - - They access backend data through the existing Wails binding layer - - Isolation via Shadow DOM for UI, try/catch for errors - -2. **Backend hooks (Go):** - - Define hook points as interfaces: `OnTrackPlay`, `OnLibraryScan`, `OnMetadataChange`, etc. - - Internal Go "plugins" implement these interfaces - - For v1.1, hooks are compile-time (not dynamic) — the plugin system defines the API, but plugins are compiled in - - Dynamic loading deferred to future (hashicorp/go-plugin RPC, or WASM) - -3. **Plugin manifest:** - ```json - { - "name": "my-plugin", - "version": "1.0.0", - "description": "Does a thing", - "entry": "index.js", - "hooks": ["onTrackPlay", "onLibraryScan"], - "ui": [{"component": "my-panel", "sections": ["sidebar", "right-panel"]}], - "permissions": ["network"] - } - ``` - -4. **Plugin directory:** `~/.config/yellowjacket/plugins//` containing manifest + JS bundle - -**v1.1 scope should be the API definition and loading mechanism** — not a full marketplace. "Working foundation" means: plugins can be loaded, they can register UI components, they can subscribe to backend events. The API surface is deliberately small and stable. - -**Depends on:** Layout customization system (for UI plugins), event system (for hook subscriptions), config system (for plugin settings). - ---- - -## Feature Dependencies - -``` -Scan Cancellation ──── (standalone, no dependencies) - │ -Tag Editing ────────── (standalone, needs new libraries) - │ -Smart Playlists ────── depends on: existing DB/track_metadata VIEW - │ -Keyboard Shortcuts ─── (standalone, frontend-primary) - │ -Gapless + Crossfade ── depends on: audio pipeline refactor - │ -MusicBrainz Browser ── depends on: HTTP client (new), network handling (new) - │ -Layout Customization ── depends on: component registry (new) - │ -Plugin System ──────── depends on: Layout Customization, Event system, Config system -``` - -**Dependency ordering (what blocks what):** -1. **Nothing blocks:** Scan cancellation, tag editing, keyboard shortcuts, smart playlists, MusicBrainz browser -2. **Layout blocks plugins:** Plugin UI registration needs the layout component registry -3. **Gapless is self-contained** but is the highest-risk change (audio pipeline) - ---- - -## MVP Recommendation - -### Build First (low risk, high value, unblocked) -1. **Scan cancellation** — lowest complexity, immediate UX win, architecture already supports it -2. **Keyboard shortcuts** — low complexity, massive usability improvement, no backend changes -3. **Smart playlists** — medium complexity, high value, builds on existing DB infrastructure - -### Build Second (medium risk, foundational) -4. **Tag editing** — medium complexity, requires new dependencies, needed before MusicBrainz becomes useful -5. **MusicBrainz browser** — medium complexity, first network feature, independent of others -6. **Layout customization** — medium-high complexity, needed before plugins - -### Build Last (high risk, high complexity) -7. **Gapless playback + crossfade** — highest complexity, fundamental audio pipeline change, can ship independently -8. **Plugin system** — highest complexity, depends on layout system, explicitly a "foundation" for v1.1 - -### Defer (explicitly) -- Tag-to-filename rename -- Undo/redo for tag edits -- Play count tracking (needed for some smart playlist rules) -- Rating system -- Plugin marketplace -- Dynamic Go plugin loading -- Detachable panels (Wails v2 limitation) - ---- - -## Sources - -- MusicBrainz API documentation: https://musicbrainz.org/doc/MusicBrainz_API (HIGH confidence — official docs, verified 2026-03-06) -- MusicBrainz rate limiting: https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting (HIGH confidence — official docs) -- `github.com/bogem/id3v2/v2` v2.1.4: https://pkg.go.dev/github.com/bogem/id3v2/v2 (HIGH confidence — official pkg.go.dev) -- `github.com/go-flac/go-flac` v1.0.0: https://pkg.go.dev/github.com/go-flac/go-flac (HIGH confidence — official pkg.go.dev) -- `github.com/go-flac/flacvorbis` v0.2.0: https://pkg.go.dev/github.com/go-flac/flacvorbis (HIGH confidence — official pkg.go.dev) -- `github.com/gopxl/beep/v2` v2.1.1: https://pkg.go.dev/github.com/gopxl/beep/v2 (HIGH confidence — official pkg.go.dev, confirms Mixer, Seq, Loop2, effects) -- YellowJacket codebase analysis: `.planning/codebase/` (HIGH confidence — direct code inspection) -- Desktop music player patterns: foobar2000, MusicBee, Strawberry, Deadbeef, Audacious (MEDIUM confidence — training data knowledge of established players) +Universal pattern: Don't delete tracks when source goes offline. Mark as unavailable. Auto-recover on next scan when source returns. Never auto-delete on temporary unavailability. diff --git a/.planning/research/PITFALLS.md b/.planning/research/PITFALLS.md index 202b92a..597db06 100644 --- a/.planning/research/PITFALLS.md +++ b/.planning/research/PITFALLS.md @@ -1,414 +1,100 @@ -# Domain Pitfalls +# Pitfalls Research: Multi-Library Support -**Domain:** Adding tag editing, scan cancellation, smart playlists, customizable keyboard shortcuts, gapless playback + crossfade, MusicBrainz browser, layout customization, and plugin system to an existing Go/Wails/Lit/SQLite desktop music player -**Researched:** 2026-03-06 -**Confidence:** HIGH (based on deep codebase analysis, official MusicBrainz API docs, beep library docs, and established Go/Wails/SQLite patterns) - ---- +**Researched:** 2026-03-08 +**Confidence:** HIGH ## Critical Pitfalls -These mistakes cause rewrites, data loss, or architectural dead ends. +### P1: SQLite ALTER TABLE ADD COLUMN with NOT NULL Requires DEFAULT ---- +SQLite requires NOT NULL columns added via ALTER TABLE to have a default. Create `libraries` table and insert default library BEFORE adding `library_id` to `audio_files`. Use `DEFAULT {id}` where id is the auto-created library's ID. -### Pitfall 1: Tag Writing Corrupts Audio Files or Loses Data +**Phase:** Schema & Migration -**Feature area:** Tag editing -**What goes wrong:** Writing ID3/Vorbis tags corrupts the audio file — partial writes leave the file unplayable, or the tag library strips existing frames (cover art, replay gain, MusicBrainz IDs) that it doesn't understand. The user edits "Artist" and loses their embedded lyrics, custom TXXX frames, and cover art. Worse: if the file is currently being played by beep, simultaneous reads and writes corrupt both the playback stream and the tag data. +### P2: Table Rebuild for CASCADE to SET NULL Must Audit ALL Tables -**Why it happens:** -- `github.com/dhowden/tag` (already in deps) is **read-only** — it does not support tag writing. The CONCERNS.md notes tag writing as a known gap (line 22). -- ID3v2 tag writing requires rewriting the file header. If the new tag is larger than the existing padding, the **entire file must be rewritten** — the audio data shifts. A crash or power loss during rewrite produces a corrupted file. -- FLAC uses Vorbis Comments in a METADATA_BLOCK. Rewriting this block similarly requires shifting the audio frame data if the block grows. -- The beep decoder holds an `*os.File` handle for the currently playing track. Writing to that same file while beep's read-ahead goroutine (`BufferedStreamer.readAhead`) is actively streaming from it will cause data corruption — the file offsets shift but the decoder's internal state doesn't update. +Both `playlist_tracks` AND `queue_tracks` have `ON DELETE CASCADE` on `audio_file_id`. Decision: playlist_tracks -> SET NULL (phantom support), queue_tracks -> keep CASCADE (queue is ephemeral). Must explicitly document this choice. -**Prevention:** -1. **Use `github.com/bogem/id3v2/v2` (n10v/id3v2)** for MP3 tag writing — it supports ID3v2.3 and v2.4 read/write with 359 stars and active maintenance. For FLAC, use `github.com/go-flac/flacvorbis` or a similar FLAC-specific writer. Keep `dhowden/tag` for read operations. -2. **Write-to-temp-then-rename pattern:** Write the modified file to a temp file in the same directory, then `os.Rename()` atomically. This ensures the original file is never partially written. On failure, the temp file is deleted and the original is untouched. -3. **Block tag writes on the currently playing file.** Before writing, check if `player.currentFile` points to the same path. If so, either: (a) stop playback, close the file, write, then reload; or (b) queue the write to execute after the track changes. Option (a) is simpler and more predictable. -4. **After writing tags, update the database.** The tag write changes the file on disk but the SQLite database still has the old metadata. You must: update the `recordings` table, update `artist_credit`/`release_groups` if changed, rebuild the FTS5 `search_index` entry for that track, and invalidate any entity cache. -5. **Preserve frames you don't edit.** When using id3v2, open with `Parse: true` to load all existing frames, modify only the ones the user changed, then save. Don't create a new tag from scratch. +**Phase:** Schema & Migration -**Detection:** -- Audio file won't play after tag edit -- Cover art disappears after editing title/artist -- Playback glitches or crashes during a tag write on the currently playing file -- FTS5 search returns stale metadata after edits +### P3: FTS5 Contentless Table Cannot Delete Individual Rows -**Confidence:** HIGH — `dhowden/tag` being read-only is confirmed by its API (no `Save()` or `Write()` methods). File corruption from concurrent read/write is a fundamental OS-level concern. +After removing a library with 10K tracks, 10K stale FTS5 entries remain. Current JOIN filtering handles this, but FTS5 scoring is affected. Consider migrating to `contentless_delete=1` (SQLite 3.43.0+). Alternative: full rebuild after library removal. ---- +**Phase:** Schema & Migration -### Pitfall 2: Gapless Playback Breaks the Existing Lock Ordering and Callback Contract +### P4: Orphan Cleanup Through Entity Graph Is Complex -**Feature area:** Gapless playback + crossfade -**What goes wrong:** The current playback flow uses `beep.Seq(streamer, beep.Callback(func() { go p.onPlaybackFinished() }))` — when the stream ends, the callback fires (with speaker lock held), dispatches to a goroutine, which then tells the queue to advance, which calls `player.LoadFile()`. This produces an audible gap of 100-500ms (file open + decode + resample + buffer fill). Attempting to eliminate this gap by pre-decoding the next track while the current one plays introduces a new concurrent resource: two open decoders, two BufferedStreamers, two file handles, and a crossfade mixer that must be swapped into the speaker chain atomically. +Reference-counting deletes must handle shared entities. Two libraries with same artist — removing one must not delete the artist if the other still references it. Use `NOT IN (SELECT ...)` or `LEFT JOIN ... WHERE ... IS NULL` pattern. Single transaction required. -**Why it happens:** -- **beep's `speaker.Play()` adds streamers to a global mix.** You can call it multiple times — new streamers are mixed with existing ones. But the Player struct assumes a single active streamer chain (`p.speakerStreamer`). Pre-loading a second track means two streamer chains are live simultaneously. -- **The lock ordering `p.mu → speaker.Lock()` assumes one-at-a-time.** With crossfade, you need to: (a) decode the next track under `p.mu`, (b) build its streamer chain, (c) under `speaker.Lock()`, splice the crossfade mixer into the active chain. If the existing track's `onPlaybackFinished` fires during this splice, you have a race between the callback goroutine (acquiring `p.mu`) and the pre-load logic (holding `p.mu` and needing `speaker.Lock()`). -- **The `BufferedStreamer` has its own goroutine.** With two tracks buffering simultaneously, you have two `readAhead()` goroutines competing for disk I/O. The `Close()` method must be called on the old BufferedStreamer at the right time — too early truncates audio, too late leaks goroutines. +**Phase:** Backend API / Library CRUD -**Prevention:** -1. **Don't try to pre-decode inside the existing `LoadFile` flow.** Instead, build a separate pre-loading mechanism: when the current track reaches N seconds from the end (detectable by comparing `seeker.Position()` to `seeker.Len()`), start decoding the next track in a background goroutine. Store the pre-decoded streamer and format in a `nextTrack` field on the Player struct, protected by `p.mu`. -2. **For gapless (no crossfade): use `beep.Seq` with both streamers.** When the pre-decoded next track is ready, replace the current speaker chain with `beep.Seq(remainingCurrentTrack, nextTrackStreamer, beep.Callback(...))`. This lets beep handle the seamless transition without a gap. The key insight: you must resample both tracks to the same sample rate (the speaker rate, 44100) before sequencing them. -3. **For crossfade: build a custom `CrossfadeStreamer`.** This streamer reads from both the ending track and the starting track simultaneously, mixing their samples with a volume ramp. Register this single crossfade streamer with the speaker. It internally manages the two underlying streamers and their lifecycle. -4. **Never close the outgoing `BufferedStreamer` until the crossfade is complete.** The crossfade streamer should call `Close()` on the old track's BufferedStreamer only after it has drained all needed samples from it. -5. **The `onPlaybackFinished` callback must be suppressed during gapless/crossfade transitions.** If beep's `Seq` fires the callback for track A while you've already started track B, the queue will try to advance again. Use a "gapless transition in progress" flag, or change the callback to a no-op during transitions and notify the queue directly from the pre-load logic. +### P5: Existing User Migration Must Be Seamless -**Detection:** -- App deadlocks when tracks transition (lock ordering violation) -- Two tracks play simultaneously (both speaker.Play'd without removing the old one) -- Goroutine leak (BufferedStreamer.readAhead never returns) -- Audio cuts out briefly then resumes (old track closed before crossfade samples drained) -- Queue advances twice (callback fires AND pre-load logic notifies queue) +First launch after update: migration 6 reads TOML DirectoryPath, creates library row, backfills audio_files.library_id. Test on real user database snapshot, not just fresh DB. -**Confidence:** HIGH — lock ordering and callback contract are documented in player.go. The `go p.onPlaybackFinished()` goroutine dispatch pattern is explicitly commented as avoiding deadlock (lines 355-361). - ---- - -### Pitfall 3: Scan Cancellation Leaves Database in Inconsistent State - -**Feature area:** Scan cancellation -**What goes wrong:** User cancels a scan mid-way through Phase 4 (DB writer batching results). The current batch may be partially committed — 30 of 50 files written in a transaction that got rolled back, but the `added` counter was already incremented. Or worse: the orphan cleanup (Phase 5) runs on a partial scan, deleting files from the database that weren't visited because the walk was cancelled early, not because they were actually deleted from disk. - -**Why it happens:** -- The scan pipeline has 6 phases running as communicating goroutines (walk → worker pool → DB writer → orphan cleanup → thumbnail generation). Cancellation must propagate cleanly through all of them. -- The `l.ctx.Done()` checks in the walk phase (lines 297, 324) use the Wails app context, which is only cancelled on shutdown. A user-triggered cancellation needs a separate `context.WithCancel()`. -- The `existingPaths` sync.Map is loaded in Phase 1 and entries are removed as files are found during the walk (Phase 2). Orphan cleanup (Phase 5) iterates remaining entries and deletes them. If the walk was cancelled early, many valid files remain in `existingPaths` and get incorrectly deleted as orphans. -- The DB writer's `flushBatch()` runs inside a transaction. If the context is cancelled between `BEGIN` and `COMMIT`, the transaction rolls back, but the import results have already been dequeued from `resultChan` — they're lost. - -**Prevention:** -1. **Create a scan-specific context:** `scanCtx, scanCancel := context.WithCancel(l.ctx)`. Store `scanCancel` on the Library struct so the frontend can call a `CancelScan()` method. -2. **Skip orphan cleanup on cancelled scans.** Add a `cancelled bool` check before Phase 5. If the scan was cancelled, the `existingPaths` map is incomplete — orphan cleanup would delete valid files. Emit a `LibraryScanCancelled` event instead of `LibraryScanComplete`. -3. **Make the DB writer respect cancellation between batches, not mid-batch.** Check `scanCtx.Done()` in the `for result := range resultChan` loop, but let the current `flushBatch()` complete before stopping. This ensures each committed batch is complete. -4. **Drain channels on cancellation.** When the walk is cancelled, it closes `workChan`. Workers drain and close `resultChan`. The DB writer drains `resultChan` normally. But if workers are blocked sending to `resultChan` (buffer full), they need to select on `scanCtx.Done()` too. Ensure all goroutines can unblock. -5. **Report partial results.** The `ScanMetrics` should include a `Cancelled: true` flag. The frontend should show "Scan cancelled — X files processed" rather than treating it as a failure. - -**Detection:** -- Files disappear from library after cancelling a scan (orphan cleanup ran on partial data) -- `ScanMetrics.Added` doesn't match actual DB row count (counter incremented but batch rolled back) -- App hangs on cancel (goroutines blocked on channel sends/receives) -- Subsequent scan adds files that were already in the library (previous scan's partial results lost) - -**Confidence:** HIGH — confirmed by reading the scan pipeline code (library.go lines 175-540). The orphan cleanup problem is the most dangerous because it's a silent data loss. - ---- - -### Pitfall 4: Plugin System Without Isolation Crashes the Host App - -**Feature area:** Plugin system -**What goes wrong:** A plugin panics in a goroutine, and since Go panics are per-goroutine, the entire application crashes. Or a plugin holds the speaker lock for too long and audio glitches. Or a plugin writes to the SQLite database concurrently and hits `SQLITE_BUSY`. Or a plugin registers a Wails event handler that conflicts with core event names. The "full-access API" promised in the project requirements makes every component a potential victim of plugin misbehavior. - -**Why it happens:** -- Go has no built-in process isolation for plugins. `plugin.Open()` loads shared objects into the same address space. Panics, goroutine leaks, and memory corruption in plugins affect the host. -- The SQLite single-writer constraint (`SetMaxOpenConns(1)`) means any plugin database access serializes with all core operations. A slow plugin query blocks library scans, queue persistence, and player state saves. -- The Wails event system is a global namespace. If a plugin emits `TrackChanged`, it could confuse the frontend. If it subscribes to `PlaybackFinished`, it runs in the same goroutine context as core handlers. - -**Prevention:** -1. **Don't use Go's `plugin` package.** It requires matching Go versions between host and plugin, doesn't work on all platforms, and provides no isolation. Instead, use one of: - - **Embedded scripting (Lua via `github.com/yuin/gopher-lua` or JavaScript via `github.com/nicholasgasior/goja`):** Run plugin code in an interpreter with controlled API exposure. Panics in the interpreter don't crash the host. - - **Process-based plugins with gRPC/stdin-stdout RPC:** Like HashiCorp's `go-plugin` model. Full isolation but higher complexity and latency. - - **WASM plugins (e.g., `github.com/tetratelabs/wazero`):** Good isolation, cross-platform, but limited Go interop. - For a desktop music player, **embedded Lua or JS is the pragmatic choice** — it's fast enough for UI customization and event hooks, and panics are contained. -2. **Wrap all plugin API calls in recover().** If using native Go plugins or any host-side callback, wrap in `defer func() { if r := recover(); r != nil { log.Error(...) } }()`. -3. **Give plugins a read-only database view.** Open a second read-only SQLite connection (since WAL mode supports concurrent readers) for plugins. This doesn't compete with the single writer. -4. **Namespace plugin events.** All plugin-emitted events must be prefixed: `plugin::`. Core events cannot be emitted by plugins. -5. **Rate-limit plugin API calls.** A plugin calling `Player.Seek()` in a tight loop would create a cascade of mutex acquisitions, speaker locks, event emissions, and frontend updates. Apply a rate limiter (e.g., 10 calls/second per plugin per API surface). - -**Detection:** -- App crashes with panic stack trace originating in plugin code -- Audio stutters when a plugin is active (speaker lock contention) -- Library scan takes 10x longer with plugins installed (SQLite writer contention) -- Frontend shows ghost events from plugin event namespace collisions - -**Confidence:** MEDIUM — plugin architecture is a design decision with many valid approaches. The specific pitfalls around Go's `plugin` package and SQLite single-writer are HIGH confidence. The recommendation for embedded scripting is based on the "foundation, not feature-complete" goal stated in PROJECT.md. - ---- +**Phase:** Schema & Migration ## Moderate Pitfalls -These mistakes cause significant rework or user-facing bugs but not architectural collapse. +### P6: Frontend Memory Pressure with Multiple Large Libraries ---- +`libraryStore.eagerFetch()` loads ALL data. 150K tracks x ~500 bytes = 75MB. Use backend filtering (pass library_id to queries). When "All Libraries" is selected, this is unavoidable for now — pagination is a future optimization. -### Pitfall 5: MusicBrainz Rate Limiting Blocks the User or Gets the App Banned +**Phase:** Frontend -**Feature area:** MusicBrainz browser -**What goes wrong:** The app fires burst requests to MusicBrainz when the user browses an artist's discography (artist lookup + release groups + releases + recordings = 4+ API calls per click). MusicBrainz enforces a **1 request per second per IP address** rate limit (confirmed from official docs). Exceeding this returns HTTP 503 for ALL subsequent requests until the rate drops. The user sees blank pages and errors. Worse: if the User-Agent string is missing or generic, the app falls into the "anonymous" throttle bucket with a shared 50 req/s global limit. +### P7: Scan Coordination — No Concurrent Scans -**Why it happens:** -- YellowJacket is currently a fully offline app (INTEGRATIONS.md: "No external API calls, cloud services"). Adding network requests is a new domain with no existing patterns for rate limiting, caching, or error handling. -- MusicBrainz API responses are richly linked — an artist has release groups, each release group has releases, each release has recordings. A naive "fetch everything on click" pattern generates a burst of requests. -- The `inc` parameter in the MusicBrainz API allows requesting related data in a single call (e.g., `?inc=release-groups+recordings`), but many combinations are not allowed together, forcing multiple requests anyway. +Single `scanActive` bool, single entity cache, single writer SQLite. Enforce one-scan-at-a-time globally with scan coordinator. Track which library is scanning for UI display. -**Prevention:** -1. **Set a proper User-Agent:** `YellowJacket/ (https://github.com/your/repo)` — this is REQUIRED by MusicBrainz. Without it, the app is rate-limited as "anonymous" (official docs confirm). -2. **Implement a global HTTP rate limiter:** Use `golang.org/x/time/rate` with `rate.NewLimiter(1, 1)` — one request per second, burst of 1. All MusicBrainz API calls go through this limiter. This is the officially documented limit. -3. **Cache aggressively.** MusicBrainz data changes rarely. Cache responses in SQLite (a new `musicbrainz_cache` table with MBID as key, response JSON as value, and a TTL column). Artist data can be cached for days. This eliminates repeat API calls for the same artist/album. -4. **Use `inc` parameters to reduce request count.** Fetch `artist?inc=release-groups` in one call rather than artist + separate release-groups lookup. Check the MusicBrainz API docs for valid `inc` combinations. -5. **Show loading states, not blank pages.** While waiting for rate-limited responses, show skeleton UI with a "Loading from MusicBrainz..." indicator. Queue requests and process them sequentially. -6. **Handle 503 gracefully.** On 503, back off exponentially (2s, 4s, 8s). Show the user "MusicBrainz is rate limiting us, retrying in Xs..." Don't silently fail. +**Phase:** Backend Scan Pipeline -**Detection:** -- Blank artist/album pages in the MusicBrainz browser -- Console shows repeated 503 errors -- All MusicBrainz browsing stops working for ~10 seconds (IP-level block) -- MusicBrainz community reports your app as misbehaving +### P8: Phantom Track Resolution with Multiple Library Roots -**Confidence:** HIGH — rate limiting rules confirmed from official MusicBrainz documentation at https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting. +`LibraryDirProvider` returns single string. M3U8 path resolution checks one root. With multi-library, try all library roots for phantom resolution. Store phantom_file_path as absolute path to avoid ambiguity. ---- +**Phase:** Frontend / Playlist Integration -### Pitfall 6: Smart Playlists Trigger Expensive Full-Table Scans on Every Library Change +### P9: Queue and Now-Playing During Library Removal -**Feature area:** Smart playlists -**What goes wrong:** A smart playlist with filter rules like "genre = 'Rock' AND year > 2000 AND playCount > 5" must be re-evaluated whenever the library changes (scan complete, tag edit, etc.). If evaluation queries the full `track_metadata` VIEW (which already joins 5 tables) with additional filter conditions, each smart playlist re-evaluation is a full table scan. With 10 smart playlists and a 50k-track library, a library scan completion triggers 10 expensive queries simultaneously, blocking the single SQLite writer for seconds and freezing the UI. +If currently playing track belongs to removed library: stop playback, advance to next non-removed track. Check queue and player state before proceeding with removal. -**Why it happens:** -- The `track_metadata` VIEW (schemas/track_metadata_view.sql) joins `audio_files`, `recordings`, `artist_credit`, `release_group_recordings`, `release_groups`, `genres`, and `file_types`. Adding smart playlist filters on top of this VIEW means SQLite can't use indexes effectively — VIEWs are expanded inline. -- SQLite's `SetMaxOpenConns(1)` means all these queries serialize. Even read queries block behind any pending write. -- Smart playlist re-evaluation is triggered by `LibraryScanComplete` events. If 10 smart playlists each take 200ms to evaluate, that's 2 seconds of blocked database access. +**Phase:** Backend API / Library CRUD -**Prevention:** -1. **Don't re-evaluate all smart playlists on every library change.** Instead, mark smart playlists as "stale" when the library changes, and only re-evaluate when the user views the playlist. -2. **Write dedicated sqlc queries for smart playlist evaluation** that target specific indexed columns directly on `audio_files` and `recordings` tables, rather than going through the `track_metadata` VIEW. For example, a "genre = Rock" filter should query `genre_recordings JOIN genres` directly with an index on `genres.name`. -3. **Add indexes for common smart playlist filter columns:** `recordings.year`, `genres.name`, `recordings.name` if not already indexed. Check existing indexes before adding. -4. **Batch evaluation.** If multiple smart playlists need re-evaluation, evaluate them in a single transaction to amortize the transaction overhead. -5. **Store smart playlist rules as JSON in a new `smart_playlists` table**, separate from the existing `playlists` table. Smart playlists don't have fixed track lists — they have rules. Mixing them into the same table complicates the playlist code. -6. **Consider a play_count column.** Smart playlists often filter by play count, but there's no `play_count` column in the current schema. This needs a schema migration adding it to `audio_files` or `recordings`, with an increment trigger on playback completion. +### P10: Cross-Library Entity Deduplication -**Detection:** -- UI freezes for several seconds after library scan completes -- SQLite `busy_timeout` errors in logs during smart playlist evaluation -- Smart playlist contents don't update until app restart (stale evaluation) +Same artist in two libraries -> one `artists` row (UNIQUE constraint handles this). Removing one library's audio_files must NOT delete shared artist. Reference-counting cleanup handles this correctly. -**Confidence:** HIGH — the VIEW structure and single-writer constraint are confirmed from the codebase. The performance concern is proportional to library size. - ---- - -### Pitfall 7: Keyboard Shortcut System Conflicts with Browser/WebView Defaults and Shadow DOM - -**Feature area:** Customizable keyboard shortcuts -**What goes wrong:** The user configures Ctrl+L as "next track," but WebKitGTK intercepts Ctrl+L as "focus address bar" (or similar browser-internal shortcut). Or the user maps Space to "play/pause," but pressing Space while focused on a button triggers the button's click handler AND the global shortcut. Shadow DOM boundaries in Lit components further complicate event propagation — a keyboard event inside a component's shadow root may not bubble to the document-level shortcut handler. - -**Why it happens:** -- Wails v2 uses a native WebView (WebKitGTK on Linux). The WebView has its own keyboard shortcut handling that runs before JavaScript event handlers. Some key combinations are intercepted before they reach the page. -- The existing Ctrl+F handler in `index.ts` (line 157) uses `document.addEventListener('keydown', ...)`. This works because it's at the document level. But components with shadow DOM (all Lit components in this project) create isolated event boundaries. A `keydown` event on an `` inside a shadow root does bubble to the document, but `event.composedPath()` must be used to determine the actual target. -- Keyboard shortcuts that overlap with form controls (Space, Enter, arrow keys, Tab) interfere with normal text input, button interaction, and accessibility navigation. - -**Prevention:** -1. **Use `document.addEventListener('keydown', ..., { capture: true })` for global shortcuts.** The capture phase fires before any component-level handlers can `stopPropagation()`. This is where the shortcut system should live. -2. **Skip shortcuts when focus is on an input/textarea.** Check `document.activeElement` (and use `event.composedPath()` to see through shadow DOM) — if the focused element is an input, text area, or contenteditable, don't handle the shortcut unless it uses a modifier key (Ctrl, Alt, Meta). -3. **Maintain a conflict list of reserved key combinations.** Some keys cannot be remapped because WebKitGTK intercepts them: Ctrl+C/V/X (copy/paste/cut), Ctrl+A (select all), Tab (focus navigation). Document these as non-configurable. -4. **Store shortcuts in the TOML config** under a `[Shortcuts]` section. Use the existing config event pattern — `ShortcutsConfigChanged` event triggers frontend re-registration. Don't store shortcuts in the frontend — the backend is the source of truth. -5. **Use the `key` property, not `keyCode`.** `keyCode` is deprecated and varies by keyboard layout. `event.key` is layout-aware and returns "a" regardless of whether the user has QWERTY or AZERTY. - -**Detection:** -- Some key combinations "don't work" on Linux but work on macOS (WebView intercepts differently) -- Typing in a search or playlist name triggers shortcut actions -- Shortcut works when focus is on the track list but not when focus is inside a shadow DOM component - -**Confidence:** HIGH — the shadow DOM event boundary behavior is fundamental to Lit/Web Components. WebView keyboard interception is platform-specific and confirmed by Wails community reports. - ---- - -### Pitfall 8: Layout Customization Breaks Component Assumptions About Size and Context - -**Feature area:** Layout customization system -**What goes wrong:** The current layout is hardcoded in `index.html` with a CSS Grid template: `"top-bar top-bar" 4em "sidebar main-panel" 1fr "bottom-bar bottom-bar" 4em`. Components assume their grid area and available space — `track-list` expects to fill the main panel, `now-playing` expects to be in the bottom bar with exactly 4em height. When users can rearrange sections, a component designed for a wide horizontal area (queue panel) gets placed in a narrow sidebar slot, or the `audio-player` component that assumes bottom-bar positioning gets placed in the sidebar where its progress bar layout breaks. - -**Why it happens:** -- Components use CSS that assumes their container context. For example, `now-playing` uses `grid-template-columns: var(--now-playing-width, 200px) 1fr auto` in the bottom bar (index.css line 68). Moving it elsewhere breaks this layout. -- The `@lit-labs/virtualizer` used for large lists requires a fixed-height container to calculate visible items. If the track list is placed in a container without explicit height, virtual scrolling breaks — it either renders all items (defeating the purpose) or renders none. -- Navigation routing in `index.ts` uses `document.getElementById('main-content')` and replaces its `innerHTML`. Layout customization means there might be multiple content areas or the main content area might have a different ID. - -**Prevention:** -1. **Components must declare size constraints.** Define a component metadata interface: `{ minWidth: number, minHeight: number, resizable: boolean, preferredArea: 'main' | 'sidebar' | 'footer' | 'any' }`. The layout system validates placements against constraints. -2. **Use CSS Container Queries for responsive components.** Instead of assuming "I'm in the sidebar" or "I'm in the main panel," components should use `@container` queries to adapt their layout based on available space. This requires adding `container-type: inline-size` to layout section containers. -3. **The layout system should operate at the section level, not the component level.** Sections have fixed roles (navigation, content, playback controls, queue). Users configure which components appear in each section and section sizes, but the section structure itself remains constrained. This is the MusicBee model. -4. **Don't refactor existing components for layout flexibility in the first pass.** Instead, build the layout configuration system that works with the current component set. Mark certain components as "fixed position" (audio-player must be in footer, sidebar must exist). Allow the content area to swap between different content components. Expand flexibility in later iterations. -5. **Virtual scrolling containers need explicit height.** Any section that hosts a virtualized list must provide a concrete CSS height (not `auto`). The layout system must enforce this for sections marked as "supports-virtualization." - -**Detection:** -- Virtual scrolling breaks when components are moved to different sections -- Components render with broken layouts (overlapping, zero height, horizontal overflow) -- Navigation stops working because `main-content` element doesn't exist in the new layout - -**Confidence:** HIGH — the hardcoded grid layout and component CSS assumptions are confirmed from index.html and index.css analysis. - ---- - -### Pitfall 9: Tag Editing and Library Scan Compete for SQLite Writer and File Access - -**Feature area:** Tag editing + library scanning interaction -**What goes wrong:** The user edits a track's tags while a library scan is in progress. The tag write modifies the file on disk, then tries to update the database. Simultaneously, the scan's DB writer goroutine is batching inserts in a transaction. The tag edit's UPDATE waits on `busy_timeout` (5000ms). Meanwhile, the scan discovers the same file during its walk — the file's modification time has changed (because of the tag write), so the scan processes it again, overwriting the just-saved tag edits with the data it reads from the file. But the file now has the NEW tags, so the scan reads the new data... unless the scan started reading the file before the tag write completed, in which case it reads a partially written file and gets corrupted metadata. - -**Why it happens:** -- SQLite single-writer with WAL mode allows concurrent reads, but writes serialize. The scan's batch transaction holds the writer lock for the duration of each batch (50 files). A tag edit UPDATE must wait for the batch to commit. -- The scan pipeline's Phase 2 (walk) checks file existence and mod time against the `sync.Map` of existing files. If a tag write changes the file between the `sync.Map` population (Phase 1) and the walk (Phase 2), the file appears "modified" and gets reprocessed. -- The metadata extraction worker pool (Phase 3) reads the file concurrently with the user's tag write. There's no file-level locking. - -**Prevention:** -1. **Block tag editing during active scans.** The simplest and most robust approach. Check `l.scanning` (add an atomic bool) before allowing tag writes. Return a user-friendly error: "Cannot edit tags while library scan is in progress." -2. **Alternatively, use a per-file advisory lock.** Before writing tags, acquire an in-memory lock for that file path. Before the scan processes a file, check the same lock. This is more complex but allows tag editing during scans for non-conflicting files. -3. **After a tag write, mark the file as "recently edited" with a timestamp.** The scan's walk phase should skip files edited within the last N seconds to avoid re-processing files that were just intentionally modified. -4. **The tag write endpoint should be a single Go method that coordinates all steps atomically:** stop playback if needed → write temp file → rename → update database → update FTS5 → emit events. Don't let the caller orchestrate these steps. - -**Detection:** -- Tag edits "revert" after a library scan completes -- `SQLITE_BUSY` errors in the tag edit path during scans -- Corrupted metadata for files that were edited during a scan - -**Confidence:** HIGH — the single-writer constraint and scan pipeline concurrency model are confirmed from the codebase. - ---- +**Phase:** Backend API / Library CRUD ## Minor Pitfalls -These cause developer frustration or minor user issues but are containable. +### P11: Config Migration — TOML to DB Split Creates Two Sources of Truth ---- +Move ALL library-related config to DB. TOML only for app-level settings (theme, shortcuts, window). TOML `[Library]` section is migration source only. -### Pitfall 10: MusicBrainz Data Model Mismatch with YellowJacket Schema +**Phase:** Schema & Migration -**Feature area:** MusicBrainz browser -**What goes wrong:** MusicBrainz uses a different data model than YellowJacket's schema. MusicBrainz has release groups (albums), releases (specific editions), and recordings (tracks). YellowJacket's schema already mirrors some of this (tables named `release_groups`, `recordings`, `artist_credit`), but the mapping isn't perfect — YellowJacket's `release_groups` are "albums" with a single name, while MusicBrainz release groups have types (Album, Single, EP, Compilation), dates, and disambiguation comments. Trying to merge MusicBrainz browsing data into the existing schema creates confusion about which data is "local library" and which is "MusicBrainz catalog." +### P12: Library Filter State Interacting with Everything -**Prevention:** -1. **Keep MusicBrainz browser data completely separate from the library database.** Use a separate cache table (`musicbrainz_cache`) or even an in-memory map. The MusicBrainz browser is read-only catalog browsing — it shouldn't modify library data. -2. **Map MusicBrainz entities to display-only DTOs**, not to the existing sqlcgen types. Create separate TypeScript interfaces (`MBArtist`, `MBReleaseGroup`, `MBRecording`) that the MusicBrainz browser components consume. -3. **If linking local tracks to MusicBrainz IDs (for future features like automatic tagging), store MBIDs as optional columns** on existing tables (e.g., `recordings.musicbrainz_id TEXT`), not as foreign keys to MusicBrainz tables. This is a one-way link — local data points to MusicBrainz, not the reverse. +Single filter state in libraryStore. All data-fetching functions accept the filter. Trigger invalidate+refetch on filter change. -**Confidence:** MEDIUM — the schema naming overlap is confirmed, but the exact API response structure would need to be verified against the MusicBrainz API at implementation time. +**Phase:** Frontend ---- +### P13: Scan-While-Remove Race Condition -### Pitfall 11: Crossfade Sample-Rate Mismatch Between Outgoing and Incoming Tracks +Before removing a library, cancel any active scan on it and wait for completion. Serialize scan and remove operations. -**Feature area:** Gapless playback + crossfade -**What goes wrong:** Track A is a 44.1kHz MP3 and Track B is a 96kHz FLAC. Both are resampled to the speaker rate (44100Hz), but the resampling happens in `updateStreamers()` which creates a new resample chain for each track. During crossfade, both tracks must produce samples at the same rate for mixing. If the crossfade streamer reads raw samples from pre-resample streamers, the mix produces garbage audio (different sample rates interpreted as the same). +**Phase:** Backend API -**Prevention:** -1. **Always crossfade post-resample.** The crossfade mixer must receive samples that are already resampled to the speaker rate. Since `updateStreamers()` already handles resampling, ensure the crossfade operates on the resampled output, not the raw decoder output. -2. **The crossfade streamer should accept two `beep.Streamer` interfaces** (not `beep.StreamSeeker`), because the resampled streamers don't support seeking. This matches beep's design where resampled streamers lose the StreamSeeker interface. +### P14: Cover Art Files Not Library-Scoped -**Confidence:** HIGH — the resample chain is confirmed in player.go lines 309-313. The speaker rate is hardcoded to 44100. +Cover art stored by content hash (shared). Removing a library: only delete cover_art DB rows that are truly orphaned (no remaining release_groups reference them). Then delete corresponding files. ---- +**Phase:** Backend API -### Pitfall 12: Config TOML Backward Compatibility When Adding New Sections +### P15: Testing Gaps -**Feature area:** Keyboard shortcuts, layout customization -**What goes wrong:** Adding `[Shortcuts]` and `[Layout]` sections to config.toml works for new installations (defaults applied), but existing users have config files without these sections. The TOML decoder fills in zero values for missing sections. If the code checks `config.Shortcuts != nil` but TOML decoding creates an empty struct (not nil), the nil check passes but the struct has zero-value fields. The `applyDefaults()` function runs before decode (see CONCERNS.md line 168: "applyDefaults runs after decode which could overwrite valid zero values"), creating a timing issue. +Create "two-library fixture" test helper. Test: add two libraries with overlapping artists -> remove one -> verify other is intact. Test migration on pre-multi-library DB snapshot. -**Prevention:** -1. **Follow the existing pattern:** `applyDefaults()` sets defaults, then TOML `Decode()` overwrites with user values. New sections get populated defaults even if the user's file doesn't contain them. This already works correctly for existing sections. -2. **Add defaults for ALL new fields in `applyDefaults()`.** For shortcuts, provide a complete default keybinding map. For layout, provide the default layout matching the current hardcoded grid. -3. **Test with an empty config file and an old-format config file.** The `config_test.go` should verify that loading a TOML file without `[Shortcuts]` or `[Layout]` produces valid defaults. -4. **Never use nil checks for TOML-decoded sections.** The TOML decoder creates zero-value structs, not nil pointers. Use a validation method that checks for meaningful content (e.g., "shortcuts map is empty" not "shortcuts is nil"). - -**Confidence:** HIGH — the config loading pattern is confirmed from config.go and CONCERNS.md. - ---- - -### Pitfall 13: Wails Event Bridge Payload Size for MusicBrainz and Layout Data - -**Feature area:** MusicBrainz browser, layout customization -**What goes wrong:** The Wails event system serializes payloads as JSON through the WebView bridge. A MusicBrainz artist response with full discography (release groups, releases with track listings) can be 100KB+ of JSON. Emitting this via `runtime.EventsEmit()` means serializing to JSON in Go, passing through the WebView bridge, and deserializing in JavaScript. For large payloads, this introduces noticeable latency. Similarly, saving/loading a complex layout configuration with per-component state creates large event payloads. - -**Prevention:** -1. **Use Wails function bindings (direct calls) for large data transfers, not events.** Events are for notifications ("data changed"). Bindings are for data retrieval ("give me the data"). The frontend should call a Go binding method that returns the MusicBrainz data directly, not listen for an event with the data embedded. -2. **Paginate MusicBrainz results.** Don't load an artist's entire discography at once. Load release groups first (lightweight), then load releases for a specific release group on click (lazy loading). -3. **For layout config, store in the TOML file and load via the existing config binding pattern.** Don't emit the full layout through events — load it once at startup via `Config.GetLayoutConfig()` binding. - -**Confidence:** MEDIUM — Wails event serialization overhead depends on WebView implementation. The recommendation to use bindings over events for data is based on Wails architecture best practices. - ---- - -### Pitfall 14: Frontend Store Proliferation and Controller Explosion - -**Feature area:** Smart playlists, MusicBrainz browser, layout customization, plugin system -**What goes wrong:** Each new feature area gets its own store and controller: `SmartPlaylistStore + SmartPlaylistController`, `MusicBrainzStore + MusicBrainzController`, `LayoutStore + LayoutController`, `PluginStore + PluginController`, `ShortcutStore + ShortcutController`. The project goes from 8 store/controller pairs to 13+. Each pair requires: a singleton store class, event subscriptions, a controller class with `hostConnected`/`hostDisconnected`, barrel file exports, and event name constants in both Go and TypeScript. The boilerplate adds up and the store/controller pattern becomes a maintenance burden. - -**Prevention:** -1. **Not every feature needs its own store.** MusicBrainz data is view-local (only relevant when the user is browsing MusicBrainz) — it can live as component-local state in the MusicBrainz browser component, not a global store. -2. **Smart playlist rules are part of playlist data** — extend the existing `PlaylistStore` rather than creating a new store. -3. **Keyboard shortcuts and layout config are extensions of the existing config system.** Extend `Config` (backend) and load via the existing config binding. The frontend reads once at startup; changes are rare. -4. **Only create a new store when the data is: (a) shared across multiple components, (b) updated from backend events, AND (c) needed across different views.** If data is view-local or rarely changes, use component state or a simple module-level variable. - -**Confidence:** HIGH — the store/controller pattern is confirmed from the codebase. The frontend already has 8 stores for ~15 components. - ---- - -### Pitfall 15: Event Name Constants Drift with Many New Events - -**Feature area:** All features (cross-cutting) -**What goes wrong:** Adding tag editing, scan cancellation, smart playlists, MusicBrainz, shortcuts, layout, and plugins requires ~15-20 new event names. Each must be added to both `backend/events/events.go` and `frontend/src/events.ts`. The AST-based codegen (`genevents`) generates TypeScript from Go, but only if you run `go generate`. Forgetting to regenerate after adding an event in Go leaves the TypeScript file stale. The pre-commit hook checks for codegen freshness, but a developer working in the frontend first (adding a TypeScript event) has no corresponding Go constant. - -**Prevention:** -1. **Always add events in Go first.** The codegen flows Go → TypeScript. Never add events in TypeScript manually. This is already documented but worth reinforcing with 15+ new events being added. -2. **Run `make generate` as part of the development workflow,** not just before commit. The pre-commit hook is a safety net, not the primary mechanism. -3. **Group new events by feature area** in `events.go` with section comments, matching the existing pattern (Playback, Queue, Config, Playlist, Library). Add new groups: `Tag`, `SmartPlaylist`, `MusicBrainz`, `Layout`, `Plugin`, `Shortcuts`, `Scan`. -4. **Consider adding a build-time check** that counts events in both files and fails if they differ. The current codegen check verifies file freshness but not content correctness if someone manually edited the TypeScript. - -**Confidence:** HIGH — the codegen pattern and its fragility are documented in CONCERNS.md. - ---- - -## Phase-Specific Warnings - -| Phase Topic | Likely Pitfall | Mitigation | -|-------------|---------------|------------| -| Tag editing | File corruption during write (P1) | Write-to-temp-then-rename; block writes on playing file | -| Tag editing | SQLite contention with scan (P9) | Block tag edits during active scans | -| Scan cancellation | Orphan cleanup on partial scan (P3) | Skip orphan cleanup when cancelled | -| Scan cancellation | Goroutine leaks on cancel (P3) | Drain all channels; use scan-specific context | -| Gapless playback | Lock ordering deadlock (P2) | Pre-decode in separate goroutine; suppress callback during transition | -| Crossfade | Sample rate mismatch (P11) | Always crossfade post-resample streamers | -| Smart playlists | Full-table scans (P6) | Lazy evaluation; dedicated indexed queries | -| Smart playlists | Missing play_count column (P6) | Schema migration with increment on playback | -| MusicBrainz browser | Rate limiting (P5) | 1 req/s rate limiter; aggressive caching; proper User-Agent | -| MusicBrainz browser | Schema confusion (P10) | Separate cache table; display-only DTOs | -| Keyboard shortcuts | Shadow DOM event boundaries (P7) | Capture phase listener; composedPath() for target detection | -| Keyboard shortcuts | WebView key interception (P7) | Document reserved keys; skip shortcuts on input focus | -| Layout customization | Component size assumptions (P8) | Container queries; component size constraints metadata | -| Layout customization | Virtual scrolling breakage (P8) | Explicit height enforcement for virtualized sections | -| Plugin system | Host crash from plugin panic (P4) | Embedded scripting runtime (not native Go plugins) | -| Plugin system | SQLite contention (P4) | Read-only connection for plugins; namespaced events | -| Config additions | Backward compatibility (P12) | Defaults for all new fields; test with old config files | -| All features | Event name drift (P15) | Go-first workflow; grouped event constants; codegen validation | -| All features | Store/controller proliferation (P14) | Extend existing stores; use component-local state where appropriate | -| MusicBrainz + Layout | Event payload size (P13) | Use bindings for data; events for notifications only | - ---- - -## Feature Interaction Matrix - -Some pitfalls emerge from the interaction between features, not from individual features: - -| Feature A | Feature B | Interaction Pitfall | -|-----------|-----------|-------------------| -| Tag editing | Library scan | Writer contention + file access races (P9) | -| Tag editing | Gapless playback | Can't write tags on file being played or pre-decoded (P1) | -| Smart playlists | Tag editing | Smart playlists must re-evaluate after tag edits change matching criteria | -| Smart playlists | Library scan | Smart playlists must re-evaluate after scan adds/removes tracks (P6) | -| Gapless playback | Plugin system | Plugins must not interfere with speaker lock during transitions (P4 + P2) | -| Layout customization | Plugin system | Plugins may want to register custom layout sections — layout system must be extensible | -| Keyboard shortcuts | Plugin system | Plugins may want to register custom shortcuts — shortcut system must be extensible | -| MusicBrainz browser | Tag editing | Future feature: apply MusicBrainz metadata to local files (tag write from MB data) | - ---- - -## Sources - -- **Codebase analysis:** `backend/player/player.go` (lock ordering, callback pattern, BufferedStreamer), `backend/library/library.go` (scan pipeline phases, context cancellation), `backend/database/` (schema, single-writer), `frontend/index.ts` (keyboard handling, navigation), `frontend/index.html` + `index.css` (hardcoded grid layout) -- **MusicBrainz rate limiting:** https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting — confirmed 1 req/s per IP, User-Agent requirement, 503 on violation -- **id3v2 Go library:** https://github.com/n10v/id3v2 — 359 stars, supports ID3v2.3/v2.4 read/write, last release v2.1.4 (Feb 2023) -- **beep wiki (composing/controlling):** https://github.com/gopxl/beep/wiki/Composing-and-controlling — confirmed speaker.Lock() usage, beep.Seq for chaining, beep.Ctrl for pause, effects.Volume for volume control -- **Project context:** `.planning/PROJECT.md`, `.planning/codebase/ARCHITECTURE.md`, `.planning/codebase/CONCERNS.md`, `.planning/codebase/INTEGRATIONS.md` - ---- - -*Pitfalls research: 2026-03-06* +**Phase:** All phases (accompanying tests) diff --git a/.planning/research/STACK.md b/.planning/research/STACK.md index dd7e012..3c55708 100644 --- a/.planning/research/STACK.md +++ b/.planning/research/STACK.md @@ -1,511 +1,62 @@ -# Technology Stack Additions: v1.1 Features & Extensibility +# Stack Research: Multi-Library Support -**Project:** YellowJacket v1.1 -**Researched:** 2026-03-06 -**Overall confidence:** HIGH (tag writing, beep audio) / MEDIUM (MusicBrainz API, plugin architecture) +**Researched:** 2026-03-08 +**Confidence:** HIGH -This document covers **only new libraries and patterns** needed for v1.1 features. The existing stack (Go 1.25, Wails v2.10.2, Lit 3.2.1, beep v2.1.1, modernc.org/sqlite, dhowden/tag, BurntSushi/toml, etc.) is validated and unchanged. +## Summary ---- +Zero new Go packages needed. The existing stack handles everything required for multi-library support. -## Recommended Stack Additions +## Existing Stack (No Changes) -### 1. Tag Writing — MP3 (ID3v2) +| Component | Package | Version | Role in Multi-Library | +|-----------|---------|---------|----------------------| +| Database | modernc.org/sqlite | v1.46.1 | ALTER TABLE, new tables, migration 6 | +| Query Gen | sqlc | v1.30.0 | New query files for libraries CRUD + filtered queries | +| Config | BurntSushi/toml | v1.6.0 | Migration source (DirectoryPath to DB) | +| Desktop | Wails | v2.10.2 | Binding patterns for library CRUD | +| Frontend | Lit | 3.2.1 | Reactive controllers for library state | +| Audio | gopxl/beep | v2 | No changes needed | -| Technology | Version | Import Path | Purpose | Why | -|------------|---------|-------------|---------|-----| -| n10v/id3v2 | v2.1.4 | `github.com/n10v/id3v2/v2` | Read/write ID3v2.3 and v2.4 tags for MP3 files | The only maintained pure-Go library with full ID3v2 write support. 359 stars, 43 releases, active (last release Feb 2023, stable). dhowden/tag (existing) is read-only — it cannot write tags back. | +## Do NOT Add -**Confidence:** HIGH — verified via GitHub repo, pkg.go.dev. The v2 module path uses the `/v2` subdirectory pattern (`github.com/n10v/id3v2/v2`). +- No ORM or query builder (fights existing sqlc architecture) +- No migration framework (goose, golang-migrate) — PRAGMA user_version works well with 5 existing migrations +- No UUID package — INTEGER PRIMARY KEY is the pattern -**API surface used:** -```go -tag, err := id3v2.Open("file.mp3", id3v2.Options{Parse: true}) -defer tag.Close() -tag.SetArtist("New Artist") -tag.SetTitle("New Title") -tag.SetAlbum("New Album") -tag.SetGenre("Electronic") -tag.SetYear("2024") -// Write back to file -err = tag.Save() +## SQLite Migration Patterns + +### Adding library_id to audio_files + +```sql +ALTER TABLE audio_files ADD COLUMN library_id INTEGER NOT NULL DEFAULT 0; +CREATE INDEX IF NOT EXISTS idx_audio_files_library_id ON audio_files(library_id); ``` -**Integration notes:** -- Operates on the file directly (open, modify, save). Does not need the existing beep pipeline. -- Must close the file before beep can play it — coordinate with player via a "stop playback → write tags → reload" flow. -- Keep `dhowden/tag` for reading (scan pipeline uses it). Use `n10v/id3v2` only for writing MP3 files. -- Thread safety: id3v2 file operations are not concurrent-safe. The tag editor backend service should serialize writes. +SQLite limitation: `ALTER TABLE ADD COLUMN` cannot add FK constraints. Enforce at application level. -### 2. Tag Writing — FLAC (Vorbis Comments) +### track_metadata VIEW -| Technology | Version | Import Path | Purpose | Why | -|------------|---------|-------------|---------|-----| -| go-flac/go-flac | v2.x | `github.com/go-flac/go-flac/v2` | Parse and reassemble FLAC file metadata blocks | Low-level FLAC metadata manipulation. 44 stars. Provides `ParseFile`, modify `Meta` slice, `Save`. | -| go-flac/flacvorbis | v2.x | `github.com/go-flac/flacvorbis/v2` | Read/write Vorbis comment metadata blocks within FLAC files | Companion to go-flac. Provides `ParseFromMetaDataBlock`, `Add`, `Set` for FLAC vorbis comments. 11 stars, but the only option in the Go ecosystem. | +Must DROP VIEW + CREATE VIEW (SQLite doesn't support ALTER VIEW). Add `af.library_id` to SELECT list. Follows migration 5 pattern exactly. -**Confidence:** MEDIUM — both libraries are small and niche but are the standard Go solution for FLAC tag writing. v2 modules exist in `/v2` subdirectories. +### playlist_tracks Table Rebuild -**API surface used:** -```go -f, err := flac.ParseFile("file.flac") -// Find existing vorbis comment block -var cmt *flacvorbis.MetadataBlockVorbisComment -var cmtIdx int -for idx, meta := range f.Meta { - if meta.Type == flac.VorbisComment { - cmt, _ = flacvorbis.ParseFromMetaDataBlock(*meta) - cmtIdx = idx - } -} -if cmt == nil { - cmt = flacvorbis.New() -} -cmt.Add(flacvorbis.FIELD_TITLE, []byte("New Title")) -cmt.Add(flacvorbis.FIELD_ARTIST, []byte("New Artist")) -cmtMeta := cmt.Marshal() -if cmtIdx > 0 { - f.Meta[cmtIdx] = &cmtMeta -} else { - f.Meta = append(f.Meta, &cmtMeta) -} -f.Save("file.flac") -``` +Change `ON DELETE CASCADE` to `ON DELETE SET NULL` on `audio_file_id`. Requires full table rebuild (migration 5 pattern: PRAGMA foreign_keys OFF -> create _new -> copy -> drop -> rename -> PRAGMA foreign_keys ON). -**Integration notes:** -- go-flac reads the entire FLAC file into memory (metadata + audio frames). For large FLAC files (100MB+), this uses significant memory. The write operation is atomic (writes full file). -- Same coordination needed: stop playback → write → reload. +## sqlc Query Patterns -### 3. Tag Writing — OGG Vorbis and WAV +- Create `sql/queries/libraries.sql` for CRUD +- Create `ByLibrary` variants of key queries (GetAllTracksWithFullMetadataByLibrary, GetAllAlbumsWithDetailsByLibrary, etc.) +- Separate queries preferred over dynamic WHERE (cleaner types, better query plans) +- Hand-crafted FTS5 queries get `AND tm.library_id = ?` filter -| Format | Approach | Why | -|--------|----------|-----| -| OGG Vorbis | Defer to v1.2 or use external tool | No mature pure-Go library exists for writing OGG Vorbis comments. The OGG container format makes in-place tag editing complex. Consider shelling out to `vorbiscomment` CLI tool if needed, or defer. | -| WAV | Not needed for v1.1 | WAV files rarely have meaningful tags (no standard tagging convention). INFO chunks exist but are rarely used in music libraries. | +## Frontend Patterns -**Confidence:** HIGH — exhaustive search found no viable pure-Go OGG Vorbis tag writer. +- `LibraryStore` gains `selectedLibraryId` state (null = all libraries) +- Backend filtering (not frontend) — don't load 150K tracks when viewing one library +- Persist selection in localStorage +- `invalidate()` on library switch triggers refetch -**Recommendation:** Implement tag editing for MP3 and FLAC first (covers ~95% of music libraries). Show "read-only" indicator for OGG/WAV files in the tag editor UI. Add OGG support later if demand exists. +## Config Migration -### 4. MusicBrainz API Client - -| Technology | Version | Import Path | Purpose | Why | -|------------|---------|-------------|---------|-----| -| **Direct HTTP + encoding/json** | stdlib | — | Query MusicBrainz REST API (JSON format) | Use Go's standard library rather than a third-party client. See rationale below. | - -**Confidence:** HIGH — MusicBrainz API is well-documented REST/JSON. The API is simple enough that a custom thin client is better than available libraries. - -**Why NOT use `michiwend/gomusicbrainz`:** -- Last meaningful commit was years ago, no Go modules support initially (added by community), uses XML parsing. 64 stars but effectively unmaintained. -- The library only supports search and lookup — no browse requests. -- MusicBrainz API supports JSON natively (`fmt=json` or `Accept: application/json`), making XML parsing unnecessary. - -**Why NOT use `go.uploadedlobster.com/musicbrainzws2`:** -- Hosted on SourceHut, harder to verify maintenance status. -- Low adoption (not visible on GitHub). - -**Custom client approach (recommended):** -```go -// backend/musicbrainz/client.go -package musicbrainz - -type Client struct { - httpClient *http.Client - baseURL string - userAgent string - rateLimiter *time.Ticker // MusicBrainz requires max 1 req/sec -} - -func NewClient(appName, appVersion, contactURL string) *Client { - return &Client{ - httpClient: &http.Client{Timeout: 10 * time.Second}, - baseURL: "https://musicbrainz.org/ws/2", - userAgent: fmt.Sprintf("%s/%s (%s)", appName, appVersion, contactURL), - rateLimiter: time.NewTicker(time.Second), // 1 request per second - } -} -``` - -**MusicBrainz API integration points:** -- **Rate limiting:** MANDATORY — max 1 request per second. Use `time.Ticker` with channel-based throttling. -- **User-Agent:** MANDATORY — must include app name, version, and contact URL. MusicBrainz blocks requests without meaningful user-agents. -- **Endpoints needed for read-only browser:** - - `GET /ws/2/artist/?inc=release-groups&fmt=json` — Artist lookup with discography - - `GET /ws/2/release-group/?inc=releases&fmt=json` — Album editions - - `GET /ws/2/release/?inc=recordings+media&fmt=json` — Track listings - - `GET /ws/2/artist?query=&fmt=json` — Artist search - - `GET /ws/2/release-group?query=&fmt=json` — Album search -- **Response caching:** Cache API responses in SQLite with TTL (e.g., 7 days). MusicBrainz data is slow-changing. Reduces API calls and improves UI responsiveness. -- **No authentication needed:** Read-only lookups and searches are unauthenticated. - -### 5. Gapless Playback + Crossfade - -| Technology | Version | Purpose | Why | -|------------|---------|---------|-----| -| **beep.Mixer** | v2.1.1 (existing) | Mix two streams for crossfade | Already in the dependency tree. `beep.Mixer` dynamically adds/removes streamers and mixes them. `KeepAlive(true)` keeps it playing silence when no streamers are active. | -| **beep.Seq** | v2.1.1 (existing) | Chain streams for gapless | Already used in `startPaused()`. `beep.Seq(s1, s2)` plays s1 then s2 without gap. | -| **effects.Volume** | v2.1.1 (existing) | Per-stream volume for fade curves | Already used for main volume. Create separate Volume wrappers for fade-in/fade-out. | - -**Confidence:** HIGH — all primitives already exist in beep v2.1.1. - -**No new dependencies needed.** Gapless and crossfade are implemented by changing how the streamer chain is composed, not by adding new libraries. - -**Gapless architecture:** -```go -// Instead of: speaker.Play(beep.Seq(currentStream, beep.Callback(onFinished))) -// Use: pre-decode next track and Seq them together. - -// When current track nears end (e.g., 2 seconds remaining): -nextStreamer, nextFormat := decodeNextTrack() -resampled := beep.Resample(4, nextFormat.SampleRate, speakerSampleRate, nextStreamer) -// The beep.Seq already playing will seamlessly transition to the next stream. -``` - -**Crossfade architecture:** -```go -// Use a Mixer as the root streamer instead of a single chain. -type CrossfadeMixer struct { - mixer beep.Mixer - fadeInMs int - fadeOutMs int -} - -// When transitioning: -// 1. Create fade-out volume wrapper on current stream -// 2. Create fade-in volume wrapper on next stream -// 3. Add both to mixer -// 4. Use beep.StreamerFunc to drive the volume ramps over time -``` - -**Key integration changes:** -- The `Player` struct currently uses `speaker.Play(beep.Seq(...))` for single-stream playback. For gapless/crossfade, switch to a persistent `beep.Mixer` registered with the speaker once at init. -- Add/remove streams from the mixer rather than calling `speaker.Play()` per track. -- The `beep.Callback` for end-of-track still works but fires per-stream in the mixer, not per-speaker-play. -- Pre-decoding the next track requires knowing what the next track IS. This means the player needs awareness of the queue (currently it only knows about the current file). Wire this via a "next track provider" interface. - -### 6. Plugin System Architecture - -| Technology | Version | Purpose | Why | -|------------|---------|---------|-----| -| **Go `plugin` package** | stdlib | ❌ NOT recommended | Linux-only, same Go version required, fragile. | -| **hashicorp/go-plugin** | — | ❌ NOT recommended | gRPC-based, heavy for a desktop app, designed for server-side tools. | -| **Custom interface + registration** | — | ✅ Recommended | Define Go interfaces for backend hooks. Plugins implement interfaces and register at init. | - -**Confidence:** MEDIUM — plugin architecture is inherently design-specific. No off-the-shelf solution fits perfectly. - -**Recommended approach: Compiled-in plugin system with runtime-loaded UI** - -**Backend plugins (Go):** -```go -// backend/plugin/api.go -package plugin - -// Plugin is the interface all backend plugins must implement. -type Plugin interface { - ID() string - Name() string - Version() string - Init(ctx PluginContext) error - Shutdown() error -} - -// PluginContext provides access to app services. -type PluginContext struct { - DB *database.DB - Events EventEmitter - Config ConfigAccess - Logger *slog.Logger -} - -// Hook interfaces — plugins implement the ones they care about. -type OnTrackChangeHook interface { - OnTrackChange(track TrackInfo) error -} -type OnScanCompleteHook interface { - OnScanComplete(metrics ScanMetrics) error -} -``` - -For v1.1, backend plugins are compiled into the binary (via Go build tags or registration in main.go). True dynamic loading can come later via process-based plugins (subprocess + JSON-RPC). - -**Frontend plugins (TypeScript/Lit):** -- Plugins provide Lit web components that register themselves via `customElements.define()`. -- The layout system (see below) allows placing plugin components in UI sections. -- Plugin JS bundles are loaded at runtime from a plugins directory via dynamic `import()`. - -**No new Go dependencies needed** for the initial plugin system. The complexity is in API design, not in libraries. - -### 7. Layout Customization System - -| Technology | Purpose | Why | -|------------|---------|-----| -| **Existing: Lit + config.toml** | Section-based layout config | The existing TOML config system and Lit component architecture are sufficient. No new dependencies needed. | - -**Confidence:** HIGH — this is a UI architecture problem, not a library problem. - -**Architecture:** -```toml -# config.toml additions: -[Layout] - [Layout.Sidebar] - components = ["navigation", "now-playing-art"] - width = 250 - - [Layout.MainPanel] - components = ["track-list"] - - [Layout.BottomBar] - components = ["audio-player", "queue-mini"] -``` - -**Frontend implementation:** -```typescript -// A layout-section component that renders configured child components -@customElement('layout-section') -class LayoutSection extends LitElement { - @property() section: string = ''; - @property({ type: Array }) components: string[] = []; - - render() { - return html`${this.components.map(name => { - const tag = document.createElement(name); - return tag; - })}`; - } -} -``` - -**Component registry pattern:** -```typescript -// Each component declares its constraints -interface LayoutComponent { - tagName: string; - displayName: string; - minWidth?: number; - minHeight?: number; - allowedSections: string[]; -} - -const registry = new Map(); -``` - -**No new npm dependencies needed.** Lit's `customElements.define()` provides the dynamic component loading mechanism. The config system already handles TOML persistence and live reload. - -### 8. Smart Playlists - -| Technology | Purpose | Why | -|------------|---------|-----| -| **Existing: SQLite + sqlc** | Dynamic query builder for filter rules | Smart playlists are SQL WHERE clauses stored as structured data. No new dependencies needed. | - -**Confidence:** HIGH — smart playlists are a database query problem. - -**Architecture:** -```go -// Smart playlist rule stored in SQLite -type SmartPlaylistRule struct { - Field string // "genre", "year", "artist", "play_count", "date_added" - Operator string // "equals", "contains", "greater_than", "less_than", "between" - Value string // The comparison value(s) -} - -type SmartPlaylist struct { - ID int64 - Name string - Rules []SmartPlaylistRule // Stored as JSON in SQLite - MatchAll bool // AND vs OR for combining rules - SortBy string - SortOrder string - Limit int // 0 = unlimited -} -``` - -**Query generation (not sqlc — dynamic WHERE clauses):** -```go -// Hand-crafted SQL builder for smart playlists. -// Cannot use sqlc because the WHERE clause is dynamic. -func (sp *SmartPlaylist) BuildQuery() (string, []any) { - // Build parameterized query from rules. - // Always use parameterized queries — never interpolate values. -} -``` - -**Schema addition:** New `smart_playlists` table with JSON rules column. New migration in the existing `PRAGMA user_version` system. - -**No new dependencies needed.** The existing `encoding/json` handles rule serialization. - -### 9. Customizable Keyboard Shortcuts - -| Technology | Purpose | Why | -|------------|---------|-----| -| **Existing: Wails runtime + config.toml + Lit** | Frontend keyboard event handling with configurable bindings | Keyboard shortcuts are a frontend concern in WebView. No new dependencies. | - -**Confidence:** HIGH — standard web keyboard event handling. - -**Architecture:** -```toml -# config.toml additions: -[KeyboardShortcuts] -play_pause = "Space" -next_track = "MediaTrackNext" -prev_track = "MediaTrackPrevious" -volume_up = "ArrowUp" -volume_down = "ArrowDown" -seek_forward = "ArrowRight" -seek_backward = "ArrowLeft" -toggle_queue = "Q" -search = "Ctrl+F" -``` - -**Frontend implementation:** -```typescript -// Global keyboard handler — listens on document, maps keys to actions -class KeyboardShortcutManager { - private bindings: Map; // key combo → action name - private actions: Map void>; // action name → handler - - handleKeyDown(e: KeyboardEvent) { - const combo = this.normalizeCombo(e); - const action = this.bindings.get(combo); - if (action) { - e.preventDefault(); - this.actions.get(action)?.(); - } - } -} -``` - -**No new dependencies needed.** The Web platform's `KeyboardEvent` API provides everything. Store bindings in TOML config, load on startup, emit config change events on update. - -### 10. Scan Cancellation - -| Technology | Purpose | Why | -|------------|---------|-----| -| **Existing: `context.WithCancel`** | Cancel in-progress library scan | Go's context cancellation is the standard pattern. The scan pipeline already uses `errgroup` which respects context cancellation. | - -**Confidence:** HIGH — standard Go pattern. - -**Implementation:** -```go -// In Library struct: -type Library struct { - scanCancel context.CancelFunc // nil when no scan is running - // ... -} - -func (l *Library) Scan() { - ctx, cancel := context.WithCancel(l.ctx) - l.scanCancel = cancel - defer func() { l.scanCancel = nil }() - - // Pass ctx to errgroup and all scan phases - g, gctx := errgroup.WithContext(ctx) - // Workers check gctx.Done() and exit early -} - -func (l *Library) CancelScan() { - if l.scanCancel != nil { - l.scanCancel() - } -} -``` - -**No new dependencies needed.** The existing `golang.org/x/sync/errgroup` already propagates context cancellation to worker goroutines. - ---- - -## Alternatives Considered - -| Category | Recommended | Alternative | Why Not | -|----------|-------------|-------------|---------| -| MP3 tag writing | n10v/id3v2 v2 | bogem/id3v2 (old path) | Same library — `n10v/id3v2` is the current canonical path after maintainer rename | -| FLAC tag writing | go-flac/go-flac + flacvorbis | mewkiz/flac | mewkiz/flac is a decoder/encoder, not a metadata editor. Would require full re-encode to change tags. | -| MusicBrainz client | Custom HTTP client | michiwend/gomusicbrainz | Unmaintained, XML-only, missing browse API, no Go modules initially | -| MusicBrainz client | Custom HTTP client | go-musicbrainzws2 (SourceHut) | Low adoption, hard to verify maintenance, adds unfamiliar dependency | -| Gapless/crossfade | beep.Mixer (existing) | External audio library | beep already provides all needed primitives (Mixer, Seq, Volume, Resample) | -| Plugin system | Interface-based registration | hashicorp/go-plugin | gRPC overhead is inappropriate for a desktop app; designed for distributed systems | -| Plugin system | Interface-based registration | Go `plugin` package | Linux-only, same Go version required, CGo required for loading, extremely fragile | -| Plugin system | Interface-based registration | Wasm runtime (wazero) | Massive complexity for v1.1; good future option for sandboxed plugins | -| Smart playlists | Dynamic SQL builder | SQLite views | Views can't be parameterized at query time; rules need runtime evaluation | -| Keyboard shortcuts | Web KeyboardEvent API | Frontend hotkey library | No library needed for the scope of shortcuts in a music player | - ---- - -## What NOT to Add - -These are things the existing stack already handles. Do NOT add duplicate libraries: - -| Capability | Already Handled By | DON'T Add | -|-----------|-------------------|-----------| -| Tag reading | `github.com/dhowden/tag` | Any other tag reading library — keep dhowden/tag for the scan pipeline | -| Audio decoding | `gopxl/beep/v2` (mp3, flac, vorbis, wav) | Any other audio decoder | -| Config persistence | `BurntSushi/toml` | YAML, JSON, or any other config library | -| Database | `modernc.org/sqlite` | Any other database or ORM | -| HTTP client | Go stdlib `net/http` | Any HTTP client library for MusicBrainz | -| JSON parsing | Go stdlib `encoding/json` | Any JSON library for MusicBrainz responses | -| Concurrency | Go stdlib `context`, `sync`, `golang.org/x/sync` | Any additional concurrency primitives | -| Frontend reactivity | Lit 3.2.1 + @lit-labs/signals | Any state management library | -| Virtual scrolling | @lit-labs/virtualizer | Any other virtual scrolling solution | - ---- - -## Installation - -```bash -# New Go dependencies (tag writing + FLAC metadata): -go get github.com/n10v/id3v2/v2@v2.1.4 -go get github.com/go-flac/go-flac/v2 -go get github.com/go-flac/flacvorbis/v2 - -# No new frontend (npm) dependencies needed for v1.1. -# All features use existing Lit + Web platform APIs. -``` - -**Total new dependencies: 3 Go packages, 0 npm packages.** - -This is intentionally minimal. The v1.1 features are primarily architecture and design challenges, not library selection challenges. The existing stack is comprehensive enough that most features require new code, not new dependencies. - ---- - -## Integration Points with Existing Stack - -### Tag Editing → Player Coordination -The player holds an open file handle (`p.currentFile`) during playback. Tag writing libraries also need exclusive file access. The workflow must be: -1. Player.Pause() or Player.Stop() — release the file -2. Write tags via id3v2/go-flac -3. Rescan the file's metadata into the database -4. Player.LoadFile() with the same path — resume - -### MusicBrainz → Database Caching -MusicBrainz API responses should be cached in SQLite (new tables: `mb_cache_artists`, `mb_cache_releases`, etc.) with a TTL column. This reuses the existing database infrastructure and avoids redundant API calls. The 1-request-per-second rate limit makes caching essential for a responsive UI. - -### Gapless/Crossfade → Speaker Architecture -Current: `speaker.Play()` called per track, creates new beep.Seq each time. -New: Register a persistent `beep.Mixer` with the speaker once at init. Add/remove per-track streamers to the mixer. This is the biggest architectural change — it affects Player, Queue auto-advance, and the playback-finished callback chain. - -### Smart Playlists → Existing Query Infrastructure -Smart playlists generate SQL queries against the existing `track_metadata` VIEW and related tables. They use the same `*database.DB` connection with the same `SetMaxOpenConns(1)` constraint. Rules are stored as JSON in a new `smart_playlists` table (schema migration via existing `PRAGMA user_version` system). - -### Layout Customization → Config + Frontend -New `[Layout]` section in config.toml, loaded via existing `BurntSushi/toml` config system. Layout changes emit config change events via existing Wails event bus. Frontend components register themselves in a component registry and the layout section components render them dynamically. - -### Plugin System → Everything -Backend plugins get a `PluginContext` with access to DB, events, config, logger. Frontend plugins load as JS modules via `import()` and register Lit web components. Both hook into the existing architecture rather than requiring new infrastructure. - ---- - -## Sources - -- n10v/id3v2: https://github.com/n10v/id3v2 — **HIGH confidence** (verified GitHub repo, 359 stars, 43 releases, MIT license) -- go-flac/go-flac: https://github.com/go-flac/go-flac — **MEDIUM confidence** (verified, 44 stars, v2 module available, Apache-2.0 license) -- go-flac/flacvorbis: https://github.com/go-flac/flacvorbis — **MEDIUM confidence** (verified, 11 stars, v2 module available, Apache-2.0 license) -- MusicBrainz API: https://musicbrainz.org/doc/MusicBrainz_API — **HIGH confidence** (official documentation, comprehensive) -- MusicBrainz rate limiting: https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting — **HIGH confidence** (official) -- beep v2.1.1 API: https://pkg.go.dev/github.com/gopxl/beep/v2 — **HIGH confidence** (official Go package docs, verified Mixer, Seq, Volume types) -- beep Mixer documentation: verified from pkg.go.dev — Add(), Clear(), KeepAlive(), Stream() methods confirmed -- michiwend/gomusicbrainz: https://github.com/michiwend/gomusicbrainz — **HIGH confidence** (verified, 64 stars, only search+lookup, no modules, effectively unmaintained) -- Go plugin package limitations: https://pkg.go.dev/plugin — **HIGH confidence** (official docs, Linux+macOS only, same Go version requirement documented) - ---- - -*Stack research for: YellowJacket v1.1 Features & Extensibility* -*Researched: 2026-03-06* +Libraries stored in SQLite (not TOML). Config `[Library].DirectoryPath` read once during migration, then deprecated. All library management through DB-backed methods. diff --git a/.planning/research/SUMMARY.md b/.planning/research/SUMMARY.md index ee1b398..4ccec2b 100644 --- a/.planning/research/SUMMARY.md +++ b/.planning/research/SUMMARY.md @@ -1,204 +1,50 @@ -# Project Research Summary +# Research Summary: Multi-Library Support -**Project:** YellowJacket v1.1 — Features & Extensibility -**Domain:** Desktop music player — feature expansion of existing Go/Wails/Lit/SQLite application -**Researched:** 2026-03-06 -**Confidence:** HIGH - -## Executive Summary - -YellowJacket v1.1 adds 8 features to a well-structured existing codebase: tag editing, scan cancellation, smart playlists, customizable keyboard shortcuts, gapless playback + crossfade, MusicBrainz browser, layout customization, and a plugin system foundation. The research confirms this is overwhelmingly an **architecture and design challenge, not a library selection challenge**. Only 3 new Go packages are needed (tag writing for MP3 and FLAC); the remaining features build entirely on the existing stack (beep v2.1.1, SQLite, Lit 3.2.1, Wails v2, stdlib). Zero new npm dependencies are required. - -The recommended approach is **integration-first**: every feature slots into established codebase patterns (two-phase init, event-driven sync, mutex-protected state, sqlc codegen, TOML config) rather than introducing new paradigms. Features vary dramatically in complexity — scan cancellation requires ~50 lines of changes to existing code, while gapless playback requires a fundamental restructuring of the audio pipeline. The build order should exploit this variance: ship quick wins first (scan cancel, keyboard shortcuts) to validate integration patterns, then tackle data model extensions (tag editing, smart playlists), then high-risk backend changes (gapless, MusicBrainz), and finally the extensibility foundations (layout, plugins). - -The primary risks are: (1) **tag writing corrupting audio files** — mitigated by write-to-temp-then-rename and blocking writes on playing files; (2) **gapless playback breaking the existing lock ordering and callback contract** — mitigated by pre-decoding in a separate goroutine and using beep's Mixer/Seq primitives; (3) **scan cancellation causing silent data loss** via orphan cleanup on partial scan data — mitigated by skipping orphan cleanup on cancelled scans; and (4) **MusicBrainz rate limiting** — mitigated by a strict 1 req/s rate limiter, aggressive SQLite caching, and proper User-Agent header. The plugin system is the highest architectural risk but is scoped to "foundation only" for v1.1, which limits blast radius. +**Synthesized:** 2026-03-08 +**Sources:** STACK.md, FEATURES.md, ARCHITECTURE.md, PITFALLS.md ## Key Findings -### Recommended Stack +### Stack +- **Zero new packages needed** — existing SQLite, sqlc, Wails, Lit stack handles everything +- Migration 6 follows existing PRAGMA user_version pattern (5 precedents) +- sqlc queries: create ByLibrary variants for ~5 key queries -The existing stack is comprehensive. v1.1 adds only 3 new Go dependencies and 0 npm dependencies. This is the right call — most features are solved by new code, not new libraries. +### Features +- **Desktop player pattern:** Merged view by default (foobar2000, Roon), optional filter (Navidrome) +- **Table stakes:** Multiple folders, unified view, per-folder scan, graceful removal, offline handling +- **Anti-features:** Separate databases, user access control, auto-dedup +- Cross-library playlists are expected by users (Navidrome, foobar2000) -**New dependencies (all Go):** -- **n10v/id3v2 v2.1.4**: MP3 tag writing (ID3v2.3/v2.4) — the only maintained pure-Go library with full write support (359 stars, active). Replaces nothing; `dhowden/tag` stays for reads. -- **go-flac/go-flac v2**: FLAC metadata block manipulation — low-level but the only Go option for FLAC tag writing. -- **go-flac/flacvorbis v2**: Vorbis comment read/write within FLAC files — companion to go-flac. - -**Reused from existing stack (no new deps):** -- **Gapless/Crossfade**: `beep.Mixer`, `beep.Seq`, `effects.Volume` — all already in beep v2.1.1. -- **MusicBrainz**: Custom HTTP client using stdlib `net/http` + `encoding/json`. Thin wrapper (~200 lines) beats unmaintained third-party clients. -- **Smart Playlists**: Dynamic SQL against existing `track_metadata` VIEW. No ORM needed. -- **Shortcuts**: Web platform `KeyboardEvent` API + TOML config persistence. -- **Layout**: Lit `customElements.define()` + component registry. CSS Container Queries for responsive components. -- **Plugins**: Interface-based Go hooks (compiled-in for v1.1) + dynamic JS module loading for frontend. - -**Critical version requirement:** OGG Vorbis and WAV tag writing should be deferred — no mature pure-Go libraries exist. MP3 + FLAC cover ~95% of music libraries. - -### Expected Features - -**Must have (table stakes):** -- Tag editing: single track + batch edit for title/artist/album/genre/year + write to file + DB sync -- Scan cancellation: cancel button, graceful stop (no DB corruption), progress reporting -- Smart playlists: filter by genre/year/artist, combine rules with AND, auto-update on library changes, save and name -- Keyboard shortcuts: play/pause, next/prev, volume, search focus, defaults that work out of box -- Gapless playback: no silence between tracks (this is expected by serious music listeners) -- Crossfade: on/off toggle with configurable duration (1-10 seconds) -- MusicBrainz browser: artist search, discography view, album track listing, rate limit compliance -- Layout: resizable panels, show/hide queue, persist across restarts - -**Should have (differentiators):** -- Batch tag editing with preview/confirmation -- Smart playlists with random/limit results ("random 50 Jazz tracks") -- Per-album gapless (disable crossfade within albums) -- MusicBrainz response caching in SQLite -- Layout presets (Compact, Full, Mini player) -- Full shortcut customization UI with conflict detection -- Cover art assignment in tag editor - -**Defer (v2+):** -- Tag-to-filename rename, undo/redo for tag edits -- Play count tracking and rating system (needed for advanced smart playlist rules) -- Plugin marketplace and dynamic Go plugin loading -- Auto-tag from MusicBrainz (this is Picard's domain) -- Detachable panels (Wails v2 limitation) -- OGG Vorbis tag writing -- DSP effects chain (equalizer, reverb) - -### Architecture Approach - -Integration-first: 6 new backend packages + 5 new frontend stores/components slot into established patterns. Backend remains source of truth. Frontend stores are reactive mirrors. Events flow backend→frontend. Actions flow frontend→backend via Wails bindings. The one paradigm shift is the audio pipeline: switching from single-streamer to persistent `beep.Mixer` as the root speaker streamer. - -**Major new components:** -1. **`backend/tageditor/`** — Format-specific tag writing + DB cascade update + FTS5 re-index -2. **`backend/smartplaylist/`** — Rule-based dynamic query evaluation against `track_metadata` VIEW -3. **`backend/musicbrainz/`** — Rate-limited HTTP client + SQLite response cache -4. **`backend/shortcuts/`** — Shortcut registry mapping key combos to backend action handlers -5. **`backend/layout/`** — Section-based layout config read from TOML, exposed to frontend -6. **`backend/plugin/`** — Plugin manifest parsing, JS loader, hook registry, API surface - -**Modified components:** -- **`backend/player/`** — Gapless pre-loading, crossfade mixer, persistent speaker mixer -- **`backend/library/`** — Scan-specific cancellable context, suppressed orphan cleanup on cancel -- **`backend/queue/`** — `TrackLoader` interface gains `PreloadNext()`, queue exposes "peek next" capability - -**Database migrations** (current version = 5): +2 new tables (`smart_playlists`, `musicbrainz_cache`), most features use TOML config not DB. +### Architecture +- **Hybrid model:** `library_id` on `audio_files` only; artists/albums/genres stay global +- **Migration 6:** Create libraries table -> add library_id column -> rebuild playlist_tracks for SET NULL + phantom columns -> recreate track_metadata VIEW +- **Scan pipeline:** `ScanLibrary(id)` replaces `Scan()`, sequential coordination +- **Orphan cleanup:** Reference-counting bottom-up deletes for shared entities +- **FTS5:** Contentless table works via JOIN filtering; consider contentless_delete migration ### Critical Pitfalls +1. ALTER TABLE ADD COLUMN requires DEFAULT for NOT NULL — create libraries first +2. Table rebuild must audit ALL CASCADE FKs (playlist_tracks AND queue_tracks) +3. FTS5 contentless can't DELETE rows — stale entries accumulate after library removal +4. Orphan cleanup must not delete shared entities across libraries +5. Existing user migration must be seamless (TOML to DB) +6. Scan coordination must serialize (single-writer SQLite) -1. **Tag writing corrupts audio files (P1)** — `dhowden/tag` is read-only; new write libraries must use write-to-temp-then-rename. Block writes on currently-playing file (beep holds `*os.File` handle). Preserve all existing tag frames when editing; never create tags from scratch. +## Architecture Decision Record -2. **Gapless playback breaks lock ordering (P2)** — The existing `p.mu → speaker.Lock()` ordering assumes one streamer at a time. Pre-decoding a second track with crossfade means two concurrent streamer chains. Must suppress `onPlaybackFinished` callback during transitions, pre-decode in background goroutine, and close old `BufferedStreamer` only after crossfade completes. +| Decision | Rationale | +|----------|-----------| +| library_id on audio_files only | Physical files belong to libraries; logical entities (artists, albums) are global | +| Libraries in DB, not TOML | CRUD through UI shouldn't require TOML manipulation; DB is already source of truth | +| SET NULL for playlist_tracks FK | Phantom tracks preserve playlist structure when library removed | +| CASCADE for queue_tracks FK | Queue is ephemeral, not user-curated like playlists | +| Sequential scanning | SQLite single-writer makes parallel scans pointless | +| Backend filtering, not frontend | Don't load 150K tracks when viewing one library | -3. **Scan cancellation triggers orphan cleanup on partial data (P3)** — If walk is cancelled early, `existingPaths` sync.Map still contains valid files → orphan cleanup deletes them. **Must skip orphan cleanup on cancelled scans.** Check cancellation between DB writer batches, not mid-batch. +## Build Order -4. **Plugin system crashes host app (P4)** — Go `plugin` package is Linux-only and fragile. For v1.1: JS-only frontend plugins (loaded via dynamic `import()`), Go hooks compiled-in (not dynamic). Wrap all plugin callbacks in `recover()`. Give plugins read-only DB access. - -5. **MusicBrainz rate limiting (P5)** — Strict 1 req/s enforced by IP ban. Must set meaningful User-Agent, cache responses in SQLite (24hr for searches, 7 days for entities), use `time.Ticker` rate limiter, handle 503 with exponential backoff. - -## Implications for Roadmap - -Based on research, suggested phase structure: - -### Phase 1: Quick Wins — Scan Cancellation + Keyboard Shortcuts -**Rationale:** Lowest complexity, highest certainty, no new dependencies. Validates core integration patterns (context cancellation, config extension, event-driven sync) that every subsequent phase depends on. -**Delivers:** Cancellable library scans with graceful stop; configurable keyboard shortcuts with sensible defaults. -**Addresses:** Scan cancellation (all table stakes), keyboard shortcuts (all table stakes) -**Avoids:** P3 (skip orphan cleanup on cancel), P7 (capture phase listener, skip shortcuts on input focus), P12 (config backward compat — test with old config files) -**Stack:** No new dependencies. stdlib `context.WithCancel`, TOML config extension, Web `KeyboardEvent` API. - -### Phase 2: Tag Editing -**Rationale:** Introduces the 3 new Go dependencies and validates the "write file → update DB → emit event → refresh frontend" pipeline. This pipeline is reused by smart playlists (DB updates trigger re-evaluation) and is a prerequisite for MusicBrainz becoming useful (users see MB data then want to apply it to their files). -**Delivers:** Single-track and batch tag editing for MP3 and FLAC files; cover art assignment; DB cascade updates; FTS5 re-indexing. -**Addresses:** Tag editing (all table stakes), cover art assignment -**Avoids:** P1 (write-to-temp-then-rename, block writes on playing file, preserve unedited frames), P9 (block tag edits during active scans) -**Stack:** n10v/id3v2 v2.1.4, go-flac/go-flac v2, go-flac/flacvorbis v2 - -### Phase 3: Smart Playlists -**Rationale:** Builds on validated DB infrastructure from Phase 2. Independent of audio pipeline. Medium complexity with well-understood patterns (SQL WHERE clause generation). Benefits from tag editing being complete (edited metadata changes smart playlist membership). -**Delivers:** Rule-based dynamic playlists with AND logic, configurable sort/limit, auto-refresh on library changes, sidebar integration. -**Addresses:** Smart playlists (all table stakes + random/limit differentiator) -**Avoids:** P6 (lazy evaluation — only re-evaluate on view, not on every library change; dedicated indexed queries, not VIEW-based full scans) -**Stack:** No new dependencies. Dynamic SQL with parameterized queries, new `smart_playlists` table (migration 6). - -### Phase 4: Gapless Playback + Crossfade -**Rationale:** Highest technical risk — must be built with full focus and thorough testing. No dependencies on other v1.1 features. The audio pipeline refactor (switching from per-track `speaker.Play()` to persistent `beep.Mixer`) is the biggest architectural change in v1.1. Build gapless first, then layer crossfade on top. -**Delivers:** Seamless track transitions; optional crossfade with configurable duration; pre-decoded next track for zero-gap playback. -**Addresses:** Gapless playback (table stakes), crossfade (table stakes), crossfade duration control -**Avoids:** P2 (pre-decode in background goroutine, suppress callback during transitions, close old BufferedStreamer after crossfade completes), P11 (always crossfade post-resample) -**Stack:** No new dependencies. beep.Mixer, beep.Seq, effects.Volume (all existing). - -### Phase 5: MusicBrainz Browser -**Rationale:** First network feature — introduces HTTP client, caching, offline handling, rate limiting. Orthogonal to audio pipeline work. Can be developed independently. Becomes more valuable after tag editing exists (users can browse MB, then manually apply metadata). -**Delivers:** Artist search, discography browsing, release/track listing, response caching, offline-safe degradation. -**Addresses:** MusicBrainz browser (all table stakes + caching differentiator) -**Avoids:** P5 (1 req/s rate limiter, proper User-Agent, SQLite cache, exponential backoff on 503), P10 (separate cache table, display-only DTOs — never merge MB data into library schema), P13 (use bindings for data retrieval, events for notifications only) -**Stack:** No new dependencies. stdlib net/http + encoding/json, new `musicbrainz_cache` table (migration 7). - -### Phase 6: Layout Customization + Plugin Foundation -**Rationale:** Meta-features that wrap all other features. Must come last because they need a stable API surface and complete component set. Layout customization is the prerequisite for plugin UI registration. Plugin system defines the extensibility API but ships as "foundation" (working loader + core API surface + example plugin). -**Delivers:** Section-based layout config (MusicBee-style); resizable panels with persistence; component registry; JS plugin loading; plugin API surface (events, player, queue, library); one example plugin. -**Addresses:** Layout customization (table stakes + section-based differentiator), plugin system (foundation — API definition, loading mechanism, core hooks) -**Avoids:** P4 (JS-only plugins, recover() wrappers, read-only DB for plugins, namespaced events), P8 (section-level operation not component-level, CSS Container Queries, explicit height for virtualized sections), P14 (extend existing stores where possible, component-local state for view-specific data) -**Stack:** No new dependencies. Lit customElements, dynamic import(), TOML config extension. - -### Phase Ordering Rationale - -- **Dependency chain:** Scan cancel → validates context patterns used everywhere. Tag editing → validates file-write-DB-update-event pipeline. Smart playlists → uses validated DB patterns. Layout → provides component registry needed by plugins. Plugins → last because it depends on everything being stable. -- **Risk isolation:** Gapless playback (Phase 4) is the highest-risk change. Placing it mid-sequence means foundational patterns are proven and later features (MusicBrainz, layout, plugins) don't block on audio work. -- **Value delivery curve:** Phases 1-3 are low-to-medium risk and deliver immediate user-facing value. If the project stalls after Phase 3, users still get scan cancellation, keyboard shortcuts, tag editing, and smart playlists — a strong v1.1. -- **Feature grouping:** Each phase touches a distinct subsystem (config, files+DB, DB queries, audio pipeline, network, UI architecture), minimizing merge conflicts for parallel development. - -### Research Flags - -**Phases likely needing deeper research during planning:** -- **Phase 4 (Gapless + Crossfade):** The beep library's Mixer/Seq composition for real-time crossfade is not well-documented beyond basic examples. Need to prototype the persistent-mixer architecture and validate lock ordering with two concurrent BufferedStreamers before committing to implementation approach. -- **Phase 6 (Plugin System):** The plugin API surface needs careful design — what's exposed, what's sandboxed, how errors are contained. No off-the-shelf solution fits; this is bespoke design work. Consider a spike/prototype before full implementation. - -**Phases with standard patterns (skip deep research):** -- **Phase 1 (Scan Cancel + Shortcuts):** Well-documented Go context cancellation + standard web keyboard handling. The codebase already has the patterns. -- **Phase 2 (Tag Editing):** Tag writing libraries have clear APIs. The DB cascade is the main design work. -- **Phase 3 (Smart Playlists):** Dynamic SQL generation is a solved problem. Rules → WHERE clause mapping is straightforward. -- **Phase 5 (MusicBrainz):** REST API with excellent official documentation. Rate limiting patterns are standard. - -## Confidence Assessment - -| Area | Confidence | Notes | -|------|------------|-------| -| Stack | HIGH | Only 3 new deps, all verified on pkg.go.dev. Existing stack covers 7/10 features with no additions. | -| Features | HIGH | Grounded in codebase analysis + established desktop music player patterns (foobar2000, MusicBee, Strawberry). | -| Architecture | HIGH | Derived from complete codebase read. Integration patterns validated against existing code structure. | -| Pitfalls | HIGH | 15 pitfalls identified with specific line-number references to codebase. Critical pitfalls have concrete prevention strategies. | - -**Overall confidence:** HIGH - -### Gaps to Address - -- **OGG Vorbis tag writing:** No pure-Go solution exists. Deferred to v1.2+. Need to show "read-only" indicator in tag editor UI for OGG files. May need to revisit if user demand is high. -- **Play count tracking:** Required for advanced smart playlist rules ("most played", "never played") but not in current schema. Needs a schema migration and playback-completion hook. Defer to Phase 3 as an optional add-on. -- **Plugin security model:** The v1.1 foundation intentionally skips a permissions system. Plugins run with full API access. This is acceptable for "power user installs plugins manually" but needs a permissions model before any marketplace/discovery feature. -- **FLAC memory usage during tag writes:** `go-flac` reads entire files into memory. For 100MB+ FLAC files, this is significant. May need a streaming approach in the future, but acceptable for v1.1. -- **Crossfade timing accuracy:** Detecting "N seconds from track end" requires comparing `seeker.Position()` to `seeker.Len()` at the speaker sample rate. Accuracy depends on the polling interval. Need to prototype during Phase 4 to determine if a polling approach is sufficient or if a sample-counting approach is needed. - -## Sources - -### Primary (HIGH confidence) -- YellowJacket codebase: complete analysis of all Go packages and TypeScript sources (2026-03-06) -- n10v/id3v2: https://github.com/n10v/id3v2 — 359 stars, v2.1.4, MIT license, full ID3v2 read/write -- beep v2.1.1: https://pkg.go.dev/github.com/gopxl/beep/v2 — Mixer, Seq, Volume, Resample confirmed -- beep wiki: https://github.com/gopxl/beep/wiki/Composing-and-controlling — speaker.Lock(), Seq chaining, Ctrl pause -- MusicBrainz API: https://musicbrainz.org/doc/MusicBrainz_API — rate limiting, JSON format, entity types -- MusicBrainz rate limiting: https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting — 1 req/s, User-Agent requirement -- dhowden/tag: confirmed read-only (no Save/Write methods in API) - -### Secondary (MEDIUM confidence) -- go-flac/go-flac: https://github.com/go-flac/go-flac — 44 stars, v2 available, Apache-2.0 -- go-flac/flacvorbis: https://github.com/go-flac/flacvorbis — 11 stars, v2 available, Apache-2.0 -- Desktop music player patterns: foobar2000, MusicBee, Strawberry, Deadbeef, Audacious (training data knowledge) -- michiwend/gomusicbrainz: https://github.com/michiwend/gomusicbrainz — 64 stars, confirmed unmaintained - -### Tertiary (LOW confidence) -- Plugin architecture recommendations: based on Go ecosystem analysis and desktop app patterns; no direct precedent for Wails plugin systems exists - ---- -*Research completed: 2026-03-06* -*Ready for roadmap: yes* +1. **Schema & Migration** — Foundation everything else depends on +2. **Backend Scan Pipeline** — Per-library scanning before exposing in UI +3. **Backend API** — CRUD, filtered queries, events, orphan cleanup +4. **Frontend** — Library manager, filter, store updates, phantom display