From 31ada14111c269bbbc984818f1cd61ea16861e74 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Tue, 18 Aug 2026 16:39:21 -0400 Subject: [PATCH] docs: delete four documents that contradict the code They do not merely lag it, they contradict it, which is worse than absent: the only way to find out one is wrong is to trust it. docs/dev/roadmap.md, 1648 lines. Its "Current State" describes an app with basic playback, a track list and an album grid. Phases 1, 2, 3 and 5 have shipped entire -- playlists, shuffle and repeat, shortcuts, virtualised lists, search, custom columns, smart playlists, the MusicBrainz client, autotag. Its Decision Log records "Testing: Deferred" against 836 Vitest tests, a Playwright suite on two engines and race-detector passes in three build configurations. .pi/journal.md, "what happened and what's next", frozen at plan 005 and still saying everything from phase 1 onward is uncommitted. That is the tracker's job now, and a work log that is wrong about what is committed is a hazard rather than a stale file. docs/dev/overview.md, a shallower and partly incorrect CLAUDE.md architecture section. README pointed at it and points at CLAUDE.md now. docs/dev/config-suggestions.md, eight suggestions of which one survived -- the rest were overtaken by rewrites: applyDefaults exists, scan concurrency is configurable with SSD/HDD detection, and httphandler.go and the WriteHeader-after-render bug it described are gone entirely. Nothing is lost: the genuinely unbuilt work was filed first, as #94 device sync, #95 layout customisation, #96 AcoustID and #97 the config setters that take no lock. Refs #98 --- .pi/journal.md | 118 --- README.md | 7 +- docs/dev/config-suggestions.md | 194 ---- docs/dev/overview.md | 53 - docs/dev/roadmap.md | 1648 -------------------------------- 5 files changed, 5 insertions(+), 2015 deletions(-) delete mode 100644 .pi/journal.md delete mode 100644 docs/dev/config-suggestions.md delete mode 100644 docs/dev/overview.md delete mode 100644 docs/dev/roadmap.md diff --git a/.pi/journal.md b/.pi/journal.md deleted file mode 100644 index d0b41ea..0000000 --- a/.pi/journal.md +++ /dev/null @@ -1,118 +0,0 @@ -# Work log - -Temporal memory: what happened and what's next. Structure lives in -`CLAUDE.md`, operational instructions in `.pi/skills/yellowjacket-dev/`, -measured discoveries in `.planning/NOTES.md`. Don't duplicate those here. - -## Current state - -Plan 005 (agent development harness) is **complete — all seven -phases**. Everything from phase 1 onward is still **uncommitted**: one -large but coherent working-tree diff, nothing pushed. - -All four tiers verified green from a cold, cleaned state: -`make ui-test` 313 passed, `make lint` 0 issues × 3 configurations, -`make test` green × 3 passes, `make e2e` 19 passed. Both CI jobs -verified green in a bare `ubuntu:24.04` container, including 19/19 on -WebKit. - -**Committed and pushed** as `5ca6cad` (the harness) + `ccacd67` (a CI -fix), and **green on the real runner**: job `check` ~4 min, job `e2e` -~3 min with 19/19 chromium *and* 19/19 webkit. One commit rather than -seven because the working tree was the end state, not per-phase -snapshots — `Makefile`, `CLAUDE.md` and `lefthook.yml` are touched by -nearly every phase, so a split would have been fabricated history. - -Still unverified, because no run has failed yet: the -`actions/upload-artifact` step (`continue-on-error`, so it cannot mask -a real failure) and whether pnpm honours `npm_config_store_dir` for -store caching. Worth checking the next time a spec legitimately fails. - -- [ ] `gitea_ci`'s `job_logs` returns 404 on Gitea 1.27.1 — the endpoint - is not exposed. Logs come from the VPS instead: `zstdcat` the file - under `gitea/actions_log////.log.zst`, - and note `zstdcat` is not in the gitea container, so - `docker cp` it out first. Job status is `action_run_job.status` - (1 success, 2 failure, 4 skipped, 5 waiting, 6 running). - Probably belongs in the `gitea` skill, not here. - -Open items deliberately not fixed: WAV tags are write-only -(`TestWAVTagsAreNotReadableYet`), `themeStore.loadFromBackend`'s failure -handler cannot recover, `backend/playlist` has no CRUD suite. - -## Log - -### 2026-08-11 — cold skill run, then phase 7 (CI) - -- **Followed the skill cold first**, as the last session asked. It - works: app up from a wiped `.dev/`, an undocumented flow driven - (queue panel + shuffle, asserted on `QueueModeChanged`), stopped — - ~1 minute, no dead ends. One real config bug: `outputDir` in - `.playwright/cli.config.json` resolves against **cwd**, not the - config file's directory (only `initScript` does that), so snapshots - were landing above the repo and a *stale* one from the previous - session answered `ls -t` instead. That cost a DOM walk to disprove a - regression that did not exist. Four smaller doc gaps fixed - (`sandbox-seed` already runs `testdata`; `ui-setup`/`e2e-setup` were - undocumented prerequisites; `snapshot` prints a path; `dev-stop` - leaves the browser open), plus `dev-headless.sh`'s own banner, which - was suggesting the bare `window.go` call its next paragraph warns - against. -- **Built both CI jobs as container scripts before writing any YAML**, - then transcribed the YAML back out and re-ran it to prove the - transcription. Push-and-see is a bad loop on a self-hosted runner. -- **It found a real bug immediately**: `make lint` omitted - `webkit2_41` on all three passes, so it was linting configurations - nothing builds. Invisible on Arch (which still ships - `webkit2gtk-4.0.pc`), fatal on Ubuntu 24.04. Tag sets now match - `make test`. -- **Both open decisions settled by measurement**: ALSA `null` PCM for - audio (no daemon; the elapsed clock really advances), dead-address - stub for the explore artifact (and setting it for the *app* run, not - just seeding, is worth 8x on suite wall clock). **WebKit is a - required step** — it had never been run anywhere, so one throwaway - container run replaced a coin flip with 19/19 at +11 s. - -### 2026-08-10 — phase 6, pi affordances - -- Added `.pi/skills/yellowjacket-dev/` as a directory rather than a flat - file: only the description is always in context, so `SKILL.md` stays - short enough that reading it whole is never a decision, and the deeper - material sits in `references/{harness,fixtures,ui-tier,schema-change}.md`. -- Settled the CLAUDE.md-vs-skill split **grammatically, not topically**, - because a topical split is what rots — every new fact gets two - plausible homes. Three docs, three tenses: NOTES.md is past - (measured, dated, append-only), CLAUDE.md is present (what the system - is), the skill is imperative (what to run). A new paragraph's tense - decides where it goes. -- The five gotchas (binding timeouts, first-run wizard, `pkill -f`, - seeds-by-running, WebKit-is-CI-only) went **inline in SKILL.md**, not - into a reference: you need them before the failure, not after. -- Trimmed CLAUDE.md's "Fixtures and the headless harness" section by - about half — the command sequences and gotchas it was carrying are now - the skill's, and leaving both would have created exactly the duplicate - description this repo has a standing rule against. -- Added `make skill-check` / `scripts/skill-check.sh` + a pre-commit - hook: every command in `.pi/**/*.md` must be a real `make` target, so - the Makefile stays the source of truth for invocation and a renamed - target fails a commit instead of misleading an agent later. Verified - it fails (it caught its own not-yet-created target) and passes. -- Added the `/e2e` prompt template: promoting a hand-driven - `playwright-cli` session into a spec is a transcription with four - fixed substitutions (refs → testids, sleeps → `waitForEvent`, raw - `window.go` → `callBinding`, short fixture → `LONG_TRACK`), plus three - runs — pass, pass again, pass after a DB restore — because the usual - failure is a spec depending on state the hand-driving left behind. -- One shell trap: under `set -euo pipefail`, `x="$(make -pqRr | …)"` - fails the whole assignment, because `make -q` exits non-zero when a - target is out of date and `pipefail` propagates it. - -### Earlier - -Phases 1–5 of plan 005: fixture generator and manifest, headless launch -and seeds, the event bridge + `data-testid` pass + `backend/testctl` + -`e2e/`, the Vitest component tier + `make bindings-check`, and the -`events.Emit` wrapper with its in-process service-event tests. Recaps -and the five "verified end to end" blocks are in -`.planning/plans/active/005-agent-development-harness.md`; the lessons -are in `.planning/NOTES.md`. diff --git a/README.md b/README.md index a540431..778617e 100644 --- a/README.md +++ b/README.md @@ -106,5 +106,8 @@ make dev # run with hot-reload make build-prod # produce a release binary ``` -More detail for contributors lives in -[`docs/dev/overview.md`](./docs/dev/overview.md) and [`CLAUDE.md`](./CLAUDE.md). +More detail for contributors lives in [`CLAUDE.md`](./CLAUDE.md) — the +architecture, the conventions and the reasons behind them. What is +being worked on is [the issue +tracker](https://git.ljones.me/yonlu/yellowjacket/issues); #73 is the +roadmap. diff --git a/docs/dev/config-suggestions.md b/docs/dev/config-suggestions.md deleted file mode 100644 index f60bc66..0000000 --- a/docs/dev/config-suggestions.md +++ /dev/null @@ -1,194 +0,0 @@ -# Config Improvement Suggestions - -Remaining suggestions for improving the configuration system in YellowJacket. - -## 2. Thread Safety Concerns - -The current `Config` struct lacks synchronization: -- `Load()` and `Save()` can race with concurrent reads -- `handleConfigUpdate()` in library mutates `l.conf.DirectoryPath` without locks - -**Suggestion:** Add a `sync.RWMutex` to protect config access, especially if config is read during scans. - -```go -type Config struct { - mu sync.RWMutex - ctx context.Context - logger *slog.Logger - // ... -} - -func (c *Config) Load() error { - c.mu.Lock() - defer c.mu.Unlock() - // ... -} -``` - -## 3. Nil Safety in Validation - -In `config.go`, validation only runs if `c.Library != nil`, but `handleConfigPost` dereferences `postedConfig.Library` without checking for nil: - -```go -if postedConfig.Library != nil { - c.Library = postedConfig.Library - // ... -} -``` - -**Status:** Partially addressed in the event refactor, but consider adding explicit nil checks in `Validate()` as well. - -## 4. Inconsistent Error Handling on HTTP Responses - -In `httphandler.go:28-31`, `WriteHeader` is called *after* rendering the error template, which won't work as expected (headers must be set before writing body): - -```go -c.formSubmitError(err.Error()).Render(r.Context(), w) -w.WriteHeader(http.StatusInternalServerError) // Too late! -``` - -**Fix:** Set the status code before rendering: - -```go -w.WriteHeader(http.StatusInternalServerError) -c.formSubmitError(err.Error()).Render(r.Context(), w) -``` - -## 5. Make `scanWorkerCount` Configurable - -There's a TODO at `library.go:289`: -```go -// TODO: make configurable via Config. -var scanWorkerCount = goruntime.NumCPU() -``` - -**Suggestion:** Add this to `library.Config`: - -```go -type Config struct { - DirectoryPath Directory `form:"Directory" schema:"directory,required"` - ScanWorkers int `form:"ScanWorkers" schema:"scan_workers"` -} -``` - -Then in `NewLibrary()` or `Scan()`: - -```go -workers := l.conf.ScanWorkers -if workers <= 0 { - workers = goruntime.NumCPU() -} -``` - -## 6. Consider Config Defaults - -Currently if no config exists, an empty one is saved. Consider providing sensible defaults (e.g., common music directories like `~/Music`). - -```go -func (c *Config) setDefaults() { - if c.Library == nil { - c.Library = &library.Config{} - } - if c.Library.DirectoryPath == "" { - // Try common music directories - home, _ := os.UserHomeDir() - musicDir := filepath.Join(home, "Music") - if info, err := os.Stat(musicDir); err == nil && info.IsDir() { - c.Library.DirectoryPath = library.Directory(musicDir) - } - } -} -``` - -## 7. Config Reload/Watch Capability - -The config is only loaded at startup. Consider adding: -- File watcher for external config changes (using `fsnotify`) -- Explicit reload method callable from UI - -```go -func (c *Config) Watch() error { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } - - go func() { - for event := range watcher.Events { - if event.Op&fsnotify.Write == fsnotify.Write { - c.Load() - // Emit event for listeners - } - } - }() - - return watcher.Add(c.filePath) -} -``` - -## 8. Validation Should Return Structured Errors - -Currently validation returns combined errors. Consider returning a structured validation result that the UI can map to specific fields for better user feedback. - -```go -type ValidationError struct { - Field string - Message string -} - -type ValidationResult struct { - Valid bool - Errors []ValidationError -} - -func (c *Config) ValidateStructured() ValidationResult { - var result ValidationResult - result.Valid = true - - if c.Library != nil { - if err := c.Library.Validate(); err != nil { - result.Valid = false - result.Errors = append(result.Errors, ValidationError{ - Field: "Library.DirectoryPath", - Message: err.Error(), - }) - } - } - - return result -} -``` - -## 9. Use Standard Library for Config Paths - -The path construction in `system/userdata.go` doesn't respect `$XDG_CONFIG_HOME` on Linux or use the standard Go `os.UserConfigDir()`. - -**Current implementation:** -```go -case "linux": - return fmt.Sprintf("/home/%s/%s/yellowjacket", username, unixSubdirs[dt]), nil -``` - -**Suggested improvement:** -```go -func GetUserConfigDirPath() (string, error) { - baseDir, err := os.UserConfigDir() // Respects XDG_CONFIG_HOME - if err != nil { - return "", fmt.Errorf("could not get user config directory: %w", err) - } - - path := filepath.Join(baseDir, "yellowjacket") - - if err := os.MkdirAll(path, 0o755); err != nil { - return "", fmt.Errorf("could not create config directory: %w", err) - } - - return path, nil -} -``` - -This approach: -- Respects `$XDG_CONFIG_HOME` on Linux -- Uses proper macOS paths (`~/Library/Application Support`) -- Uses `%AppData%` on Windows -- Is more portable and follows platform conventions diff --git a/docs/dev/overview.md b/docs/dev/overview.md deleted file mode 100644 index a8fe0f1..0000000 --- a/docs/dev/overview.md +++ /dev/null @@ -1,53 +0,0 @@ -# Development Overview - -YellowJacket is a moderately complex application. This document gives an overview of how development of it works. - -## Logical Breakdown - -YellowJacket can be thought about in a heirarchy of logical modules and components. The borders of these logical sections are mostly represented in the code and directory structure as well. - -- Frontend - - UI Components (see [Lit](###lit-web-components)) -- Backend - - App - - Asset Handler - - Logging - - System - - Player - - Library - - Config - - Database - - Queries (see [sqlc](###sqlc)) - -## Dependencies - -YellowJacket uses many tools and libraries to provide its functionality. -This section lists each of these dependencies and explains how they are used. - -### [Wails](https://wails.io) - -Used to create desktop apps with Go and web technologies. - -### [SQLite](https://github.com/mattn/go-sqlite3?tab=readme-ov-file#go-sqlite3) - -Used for local database. - -### [sqlc](https://sqlc.dev/) - -Used to generate Go code from SQL. - -### [Templ](https://templ.guide/) - -Used to generate HTML templates with Go code. - -### [Beep](https://github.com/gopxl/beep?tab=readme-ov-file#beep) - -Used for audio playback. - -### [Lit Web Components](https://lit.dev/) - -Used for dynamic/reactive frontend components. - -### [HTMX](https://htmx.org/) - -Used for requesting HTML fragments from the backend and rendering them on the frontend. diff --git a/docs/dev/roadmap.md b/docs/dev/roadmap.md deleted file mode 100644 index cd29bf2..0000000 --- a/docs/dev/roadmap.md +++ /dev/null @@ -1,1648 +0,0 @@ -# Yellowjacket Development Roadmap - -This document outlines the phased development plan for Yellowjacket, from current state to a fully-featured desktop music player. - -## Project Vision - -Yellowjacket aims to be a modern, cross-platform desktop music player inspired by MusicBee, featuring: -- High performance with large libraries (50,000+ tracks) -- MusicBrainz-powered metadata autotagging -- Device syncing with re-encoding -- Highly customizable UI with arrangeable components - -## Current State - -As of the start of this roadmap: -- Basic playback working (MP3, FLAC, OGG, WAV) -- Library scanning with metadata extraction -- Track list and album grid views -- Play/pause, seek, volume (backend) -- Configuration page - ---- - -## Cross-Cutting Concern: Configuration/Settings Infrastructure - -**IMPORTANT:** Settings and configuration should be considered at the forefront of every feature. Each new feature should have its configurable options designed alongside the feature itself, not bolted on afterwards. - -### Settings Architecture Overview - -The application needs a unified settings system that: -1. Stores preferences persistently (TOML config file on backend) -2. Exposes settings to both backend and frontend -3. Allows components to register their own settings sections -4. Provides a consistent UI for editing settings - -### Backend Settings Infrastructure - -#### Config Package Structure - -The existing `backend/config/` package should be extended to support: - -```go -// backend/config/config.go - -type Config struct { - Library LibraryConfig `toml:"library"` - Player PlayerConfig `toml:"player"` - Queue QueueConfig `toml:"queue"` - UI UIConfig `toml:"ui"` - // New sections added as features are built -} - -type PlayerConfig struct { - DefaultVolume int `toml:"default_volume"` // 0-100 - ResumePlayback bool `toml:"resume_playback"` // Resume on startup - CrossfadeSeconds int `toml:"crossfade_seconds"` // 0 = disabled - ReplayGain string `toml:"replay_gain"` // "off", "track", "album" -} - -type QueueConfig struct { - RememberQueue bool `toml:"remember_queue"` // Persist queue across sessions - DefaultRepeat string `toml:"default_repeat"` // "none", "all", "one" - DefaultShuffle bool `toml:"default_shuffle"` -} - -type UIConfig struct { - Theme string `toml:"theme"` - SidebarWidth int `toml:"sidebar_width"` - Layout string `toml:"layout_preset"` - ColumnConfig map[string][]string `toml:"column_config"` // Per-view column selection -} -``` - -#### Config Change Notification - -When config values change, components need to be notified: - -```go -// Config change callback pattern (already exists for Library) -type ConfigSection interface { - OnConfigChanged(newConfig any) error -} - -// Or use events -runtime.EventsEmit(ctx, events.ConfigChanged, map[string]any{ - "section": "player", - "key": "default_volume", - "value": 75, -}) -``` - -### Frontend Settings Infrastructure - -#### SettingsStore - -```typescript -// frontend/src/store/settings-store.ts - -interface SettingsState { - player: PlayerSettings; - queue: QueueSettings; - ui: UISettings; - library: LibrarySettings; - // Extensible for new features -} - -class SettingsStore { - private state: SettingsState; - - // Load all settings from backend on startup - async initialize(): Promise; - - // Get a specific setting - get(section: string, key: string): T; - - // Update a setting (persists to backend) - async set(section: string, key: string, value: any): Promise; - - // Subscribe to changes - subscribe(callback: () => void): () => void; -} -``` - -#### Settings UI Component Architecture - -Each feature's settings should be encapsulated in a dedicated component: - -```typescript -// Pattern for settings sub-panels -interface SettingsPanel { - id: string; // e.g., "player-settings" - title: string; // e.g., "Playback" - icon: string; // Icon for settings nav - component: typeof LitElement; // The settings panel component - order: number; // Display order in settings nav -} - -// Registry for settings panels -class SettingsRegistry { - register(panel: SettingsPanel): void; - getAll(): SettingsPanel[]; -} -``` - -#### Unified Settings Window - -```typescript -// frontend/src/components/settings/settings-window.ts - -@customElement('settings-window') -class SettingsWindow extends LitElement { - // Left sidebar: list of settings sections - // Right panel: active settings section component - // Each section component handles its own settings -} -``` - -### Settings Design Checklist for New Features - -When implementing any new feature, consider: - -1. **What user preferences exist?** - - Default values - - Behavior toggles - - Display options - -2. **Where should settings be stored?** - - Backend config (persistent, affects backend behavior) - - Frontend localStorage (UI-only preferences) - - Both (synced) - -3. **How are settings exposed?** - - Add to appropriate Config struct section - - Create settings panel component - - Register with SettingsRegistry - -4. **How do components react to changes?** - - Subscribe to SettingsStore - - Handle ConfigChanged events - - Apply changes immediately vs. on restart - -### Settings Infrastructure Tasks (Integrated with Features) - -These tasks should be completed early and extended as features are added: - -#### Task: Extend backend Config structure - -As each feature is built, add its configuration section to `backend/config/config.go`. Follow the existing pattern used for `LibraryConfig`. - -#### Task: Create SettingsStore on frontend - -Create `frontend/src/store/settings-store.ts` following the same pattern as PlayerStore. Load settings from backend on app startup. - -#### Task: Create settings panel registry - -Create `frontend/src/registry/settings-registry.ts` to allow features to register their settings panels. - -#### Task: Create unified settings window component - -Create `frontend/src/components/settings/settings-window.ts` that: -- Shows navigation sidebar with all registered settings panels -- Renders the active panel -- Handles save/cancel/apply actions - -#### Task: Migrate existing config page - -The current HTMX-based config page (`/config`) should be migrated to use the new settings infrastructure, becoming the "Library" settings panel. - ---- - -## Roadmap Phases - ---- - -## Phase 1: Core Playback Experience - -**Goal:** Complete the fundamental playback features that users expect from a music player. - -**Settings to consider for this phase:** -- Default volume level -- Resume playback on startup (remember last track/position) -- Remember queue across sessions -- Default shuffle/repeat modes -- "Previous" button behavior (restart threshold in seconds) - -### 1.1 Playlist System (Go Backend) - -Implement a playlist system in the Go backend. The "Now Playing" queue is a special transient playlist that coordinates with the Player. All playlists (including the queue) share the same underlying data structures and operations. - -**Key insight:** The queue is simply a playlist with special behavior: -- It's transient (not persisted by default, but optionally can be) -- It's always "active" (connected to the Player) -- It has shuffle/repeat modes that affect playback order - -#### Task 1.1.1: Create playlist package structure - -Create `backend/playlist/` package with the following files: -- `playlist.go` - Playlist struct and methods -- `queue.go` - Queue (active playlist) with Player integration -- `storage.go` - Persistence for saved playlists - -#### Task 1.1.2: Design playlist database schema - -Add to `backend/database/sql/schemas/`: - -```sql --- playlists.sql -CREATE TABLE IF NOT EXISTS playlists ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - description TEXT, - is_smart BOOLEAN NOT NULL DEFAULT false, - smart_rules TEXT, -- JSON for smart playlist rules - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- playlist_tracks.sql -CREATE TABLE IF NOT EXISTS playlist_tracks ( - id INTEGER PRIMARY KEY, - playlist_id INTEGER NOT NULL, - audio_file_id INTEGER NOT NULL, - position INTEGER NOT NULL, - added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(playlist_id) REFERENCES playlists(id) ON DELETE CASCADE, - FOREIGN KEY(audio_file_id) REFERENCES audio_files(id) ON DELETE CASCADE -); - -CREATE INDEX idx_playlist_tracks_playlist ON playlist_tracks(playlist_id); -CREATE INDEX idx_playlist_tracks_position ON playlist_tracks(playlist_id, position); -``` - -#### Task 1.1.3: Define Playlist data structures - -```go -// backend/playlist/playlist.go - -type PlaylistTrack struct { - ID int64 - FilePath string - FileName string - Title string - Artist string - Album string - TrackLength int64 // milliseconds - Position int // Position in playlist -} - -type Playlist struct { - ID int64 - Name string - Description string - IsSmart bool - SmartRules string // JSON - Tracks []PlaylistTrack - CreatedAt time.Time - UpdatedAt time.Time -} -``` - -#### Task 1.1.4: Define Queue (Active Playlist) data structures - -```go -// backend/playlist/queue.go - -type RepeatMode string -const ( - RepeatNone RepeatMode = "none" - RepeatAll RepeatMode = "all" - RepeatOne RepeatMode = "one" -) - -// Queue is the "Now Playing" playlist - always exactly one exists -type Queue struct { - ctx context.Context - logger *slog.Logger - db *database.DB - player *player.Player - - tracks []PlaylistTrack - currentIndex int - originalOrder []PlaylistTrack // For unshuffle restoration - shuffleOrder []int // Shuffled indices - - shuffleEnabled bool - repeatMode RepeatMode - - // Config-driven settings - rememberQueue bool // Persist queue across sessions -} -``` - -#### Task 1.1.5: Implement Playlist CRUD operations - -```go -// backend/playlist/storage.go - -func (s *Storage) CreatePlaylist(name, description string) (*Playlist, error) -func (s *Storage) GetPlaylist(id int64) (*Playlist, error) -func (s *Storage) GetAllPlaylists() ([]Playlist, error) -func (s *Storage) UpdatePlaylist(playlist *Playlist) error -func (s *Storage) DeletePlaylist(id int64) error - -func (s *Storage) AddTracksToPlaylist(playlistID int64, trackIDs []int64) error -func (s *Storage) RemoveTrackFromPlaylist(playlistID int64, position int) error -func (s *Storage) ReorderPlaylistTrack(playlistID int64, fromPos, toPos int) error -func (s *Storage) GetPlaylistTracks(playlistID int64) ([]PlaylistTrack, error) -``` - -#### Task 1.1.6: Implement playlist CRUD SQL queries - -Add to `backend/database/sql/queries/playlists.sql`: - -```sql --- name: CreatePlaylist :one -INSERT INTO playlists (name, description, is_smart, smart_rules) -VALUES (?, ?, ?, ?) RETURNING *; - --- name: GetPlaylist :one -SELECT * FROM playlists WHERE id = ?; - --- name: GetAllPlaylists :many -SELECT * FROM playlists WHERE is_smart = false ORDER BY name; - --- name: UpdatePlaylist :exec -UPDATE playlists SET name = ?, description = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?; - --- name: DeletePlaylist :exec -DELETE FROM playlists WHERE id = ?; - --- name: GetPlaylistTracks :many -SELECT - af.id, af.file_path, af.length_milliseconds, - r.name as title, - COALESCE(ac.text, '') as artist, - COALESCE(rg.name, '') as album, - pt.position -FROM playlist_tracks pt -JOIN audio_files af ON pt.audio_file_id = af.id -JOIN recordings r ON af.recording_id = r.id -LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id -LEFT JOIN release_group_recordings rgr ON r.id = rgr.recording_id -LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id -WHERE pt.playlist_id = ? -ORDER BY pt.position; - --- name: AddTrackToPlaylist :exec -INSERT INTO playlist_tracks (playlist_id, audio_file_id, position) -VALUES (?, ?, (SELECT COALESCE(MAX(position), 0) + 1 FROM playlist_tracks WHERE playlist_id = ?)); - --- name: RemoveTrackFromPlaylist :exec -DELETE FROM playlist_tracks WHERE playlist_id = ? AND position = ?; - --- name: ReorderPlaylistTracks :exec -UPDATE playlist_tracks SET position = ? WHERE playlist_id = ? AND audio_file_id = ?; -``` - -#### Task 1.1.7: Implement Queue constructor - -```go -func NewQueue(ctx context.Context, logger *slog.Logger, db *database.DB, player *player.Player, config *config.QueueConfig) *Queue -``` - -- Initialize with empty tracks slice -- Set currentIndex to -1 (nothing playing) -- Load shuffle/repeat defaults from config -- Store reference to Player for playback control -- If `config.RememberQueue` is true, load persisted queue from DB - -#### Task 1.1.8: Implement queue manipulation methods - -```go -// Set the entire queue (e.g., when user clicks "Play All" or clicks a track) -func (q *Queue) Set(tracks []PlaylistTrack, startIndex int) - -// Add tracks to end of queue -func (q *Queue) Add(tracks ...PlaylistTrack) - -// Insert tracks at specific position -func (q *Queue) InsertAt(index int, tracks ...PlaylistTrack) - -// Remove track at index -func (q *Queue) Remove(index int) - -// Clear entire queue -func (q *Queue) Clear() - -// Move track from one position to another (for drag-and-drop reordering) -func (q *Queue) Move(fromIndex, toIndex int) - -// Load from a saved playlist -func (q *Queue) LoadPlaylist(playlistID int64) error - -// Save current queue as a new playlist -func (q *Queue) SaveAsPlaylist(name string) (*Playlist, error) -``` - -#### Task 1.1.9: Implement playback control methods - -```go -// Play track at current index -func (q *Queue) PlayCurrent() error - -// Skip to next track (respects shuffle and repeat modes) -func (q *Queue) Next() error - -// Skip to previous track -func (q *Queue) Previous() error - -// Jump to specific index in queue -func (q *Queue) PlayAt(index int) error -``` - -**Logic for Next():** -1. If repeat mode is "one", restart current track -2. If shuffle enabled, pick next from shuffle order -3. Otherwise, increment currentIndex -4. If at end of queue: - - If repeat mode is "all", go to index 0 - - Otherwise, stop playback -5. Call `q.player.LoadFile()` and `q.player.Play()` - -**Logic for Previous():** -1. If current position > N seconds (configurable, default 3), restart current track -2. Otherwise, go to previous track (respecting shuffle order) -3. If at beginning, stay at index 0 - -#### Task 1.1.10: Implement shuffle functionality - -```go -func (q *Queue) SetShuffle(enabled bool) -``` - -**When enabling shuffle:** -1. Store current order in `originalOrder` -2. Create shuffled index order (Fisher-Yates shuffle) -3. Keep current track at current position in shuffle order - -**When disabling shuffle:** -1. Restore `originalOrder` -2. Find current track's position in original order -3. Set currentIndex to that position - -#### Task 1.1.11: Implement repeat functionality - -```go -func (q *Queue) SetRepeat(mode RepeatMode) -``` - -This just sets the mode; the logic is in `Next()`. - -#### Task 1.1.12: Implement queue state getters - -```go -func (q *Queue) GetTracks() []PlaylistTrack -func (q *Queue) GetCurrentIndex() int -func (q *Queue) GetCurrentTrack() *PlaylistTrack -func (q *Queue) IsShuffleEnabled() bool -func (q *Queue) GetRepeatMode() RepeatMode -func (q *Queue) GetDuration() int64 // Total queue duration in ms -``` - -#### Task 1.1.13: Register event handlers for queue - -```go -func (q *Queue) registerEventHandlers() { - // Listen for PlaybackFinished to auto-advance - runtime.EventsOn(q.ctx, events.PlaybackFinished, func(_ ...any) { - q.Next() - }) - - // Listen for frontend requests - runtime.EventsOn(q.ctx, events.RequestNext, func(_ ...any) { - q.Next() - }) - - runtime.EventsOn(q.ctx, events.RequestPrevious, func(_ ...any) { - q.Previous() - }) - - runtime.EventsOn(q.ctx, events.RequestSetShuffle, func(data ...any) { - enabled := data[0].(bool) - q.SetShuffle(enabled) - }) - - runtime.EventsOn(q.ctx, events.RequestSetRepeat, func(data ...any) { - mode := RepeatMode(data[0].(string)) - q.SetRepeat(mode) - }) -} -``` - -#### Task 1.1.14: Implement queue change event emission - -```go -func (q *Queue) emitQueueChanged() { - runtime.EventsEmit(q.ctx, events.QueueChanged, map[string]any{ - "tracks": q.tracks, - "currentIndex": q.currentIndex, - "shuffle": q.shuffleEnabled, - "repeat": string(q.repeatMode), - }) -} -``` - -Call this after any queue modification. - -#### Task 1.1.15: Add queue and playlist events to events package - -Update `backend/events/events.go`: - -```go -// Queue events -const ( - QueueChanged = "QueueChanged" - RequestNext = "RequestNext" - RequestPrevious = "RequestPrevious" - RequestSetShuffle = "RequestSetShuffle" - RequestSetRepeat = "RequestSetRepeat" - RequestAddToQueue = "RequestAddToQueue" - RequestClearQueue = "RequestClearQueue" -) - -// Playlist events -const ( - PlaylistsChanged = "PlaylistsChanged" // When playlists are created/deleted/renamed - PlaylistUpdated = "PlaylistUpdated" // When a playlist's tracks change -) -``` - -#### Task 1.1.16: Add events to frontend events.ts - -Update `frontend/src/events.ts` to mirror backend events for queue and playlists. - -#### Task 1.1.17: Integrate Queue and Playlist Storage into app.go - -- Create playlist Storage in `NewYellowJacketApp()` -- Create Queue in `OnStartup()` after Player is created -- Pass Player reference and config to Queue constructor -- Add Queue and playlist Storage to FEBindings -- Ensure Queue's event handlers are registered - -#### Task 1.1.18: Update PlayerStore to cache queue state - -Update `frontend/src/store/player-store.ts`: - -```typescript -interface PlayerState { - // Existing... - isPlaying: boolean; - currentTrack: TrackInfo | null; - volume: number; - - // New queue state - queue: PlaylistTrack[]; - queueIndex: number; - shuffleEnabled: boolean; - repeatMode: 'none' | 'all' | 'one'; -} -``` - -Add event listener for `QueueChanged`. - -#### Task 1.1.19: Update PlayerController with queue actions - -Add methods to PlayerController: -- `next()`, `previous()` -- `setShuffle(enabled: boolean)` -- `setRepeat(mode: string)` -- `addToQueue(tracks: Track[])` -- `clearQueue()` -- `loadPlaylist(playlistId: number)` -- `saveQueueAsPlaylist(name: string)` - -#### Task 1.1.20: Create PlaylistStore for saved playlists - -Create `frontend/src/store/playlist-store.ts`: - -```typescript -interface PlaylistState { - playlists: Playlist[]; - loading: boolean; -} - -class PlaylistStore { - // Load all playlists from backend - async loadPlaylists(): Promise; - - // CRUD operations (delegate to backend) - async createPlaylist(name: string): Promise; - async deletePlaylist(id: number): Promise; - async renamePlaylist(id: number, name: string): Promise; - - // Track operations - async addTracksToPlaylist(playlistId: number, trackIds: number[]): Promise; - async removeTrackFromPlaylist(playlistId: number, position: number): Promise; -} -``` - ---- - -### 1.2 Playlist UI Components - -#### Task 1.2.1: Create playlist sidebar section - -Update `frontend/src/components/sidebar/app-sidebar.ts` or create new component: -- Show list of playlists below navigation -- "New Playlist" button -- Click playlist to view contents -- Right-click for context menu (rename, delete) - -#### Task 1.2.2: Create playlist view component - -Create `frontend/src/components/playlist/playlist-view.ts`: -- Display tracks in a playlist -- Drag to reorder tracks -- Remove track button -- Play all / shuffle play buttons -- Edit playlist name/description - -#### Task 1.2.3: Add "Add to Playlist" context menu - -Create reusable context menu component: -- Right-click track -> Add to Playlist -> [list of playlists] -- Option to create new playlist - -#### Task 1.2.4: Create "Now Playing" queue panel - -Create `frontend/src/components/queue/queue-panel.ts`: -- Shows current queue -- Highlights currently playing track -- Drag to reorder -- Remove tracks -- Clear queue button -- Save as playlist button - ---- - -### 1.3 Skip Next/Previous - -#### Task 1.3.1: Add skip buttons to player-controls component - -Update `frontend/src/components/audio-player/controls/player-controls.ts`: - -- Add "previous" button (calls `controller.previous()`) -- Add "next" button (calls `controller.next()`) -- Use appropriate icons from webawesome - -#### Task 1.3.2: Style skip buttons - -Ensure buttons match existing play/pause styling. - ---- - -### 1.4 Shuffle & Repeat Modes - -#### Task 1.4.1: Add shuffle toggle to player-controls - -- Add shuffle button that toggles `controller.setShuffle(!current)` -- Visual indicator when shuffle is enabled (icon color change or background) - -#### Task 1.4.2: Add repeat toggle to player-controls - -- Add repeat button that cycles through modes: none -> all -> one -> none -- Different icon or indicator for each mode: - - none: repeat icon, dimmed - - all: repeat icon, highlighted - - one: repeat-one icon, highlighted - ---- - -### 1.5 Keyboard Shortcuts - -#### Task 1.5.1: Create keyboard shortcut handler - -Create `frontend/src/utils/keyboard-shortcuts.ts`: - -```typescript -import { playerStore } from '@store/player-store'; - -export function initKeyboardShortcuts() { - document.addEventListener('keydown', (e) => { - // Don't trigger if user is typing in an input - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { - return; - } - - switch (e.code) { - case 'Space': - e.preventDefault(); - playerStore.getState().isPlaying ? playerStore.pause() : playerStore.play(); - break; - case 'ArrowRight': - if (e.ctrlKey || e.metaKey) { - playerStore.next(); - } - break; - case 'ArrowLeft': - if (e.ctrlKey || e.metaKey) { - playerStore.previous(); - } - break; - // Add more shortcuts as needed - } - }); -} -``` - -#### Task 1.5.2: Initialize shortcuts in index.ts - -Call `initKeyboardShortcuts()` in `frontend/index.ts`. - -#### Task 1.5.3: Document keyboard shortcuts - -Consider adding a help modal or tooltip showing available shortcuts. - ---- - -### 1.6 Volume UI - -#### Task 1.6.1: Verify backend volume support - -Check that `Player.SetVolume()` is working and exposed via FEBindings or events. - -If not exposed via events, add: -- `RequestSetVolume` event in `backend/events/events.go` -- Event handler in Player that calls `SetVolume()` -- Emit `VolumeChanged` event after volume changes - -#### Task 1.6.2: Implement volume-control component - -Update `frontend/src/components/audio-player/volume-control/volume-control.ts`: - -- Add PlayerController -- Render slider from 0-100 -- Display current volume from `controller.volume` -- On change, call `controller.setVolume(value)` - -#### Task 1.6.3: Add mute toggle - -- Add mute button that sets volume to 0 (store previous volume) -- Click again to restore previous volume -- Icon changes based on volume level (muted, low, medium, high) - ---- - -## Phase 2: Performance & Scale - -**Goal:** Optimize the application for large music libraries (50,000+ tracks). - -**Settings to consider for this phase:** -- Page size for virtualized lists -- Prefetch buffer size (how many pages to load ahead) -- Library scan behavior (auto-scan on startup, watch for changes) -- Cache settings (cover art cache size, etc.) - -### 2.1 Database Optimization - -#### Task 2.1.1: Add indexes to frequently queried columns - -Create new migration file `backend/database/sql/schemas/indexes.sql`: - -```sql --- Index for audio_files queries -CREATE INDEX IF NOT EXISTS idx_audio_files_recording_id ON audio_files(recording_id); -CREATE INDEX IF NOT EXISTS idx_audio_files_file_type_id ON audio_files(file_type_id); - --- Index for recordings queries -CREATE INDEX IF NOT EXISTS idx_recordings_artist_credit_id ON recordings(artist_credit_id); -CREATE INDEX IF NOT EXISTS idx_recordings_name ON recordings(name); -CREATE INDEX IF NOT EXISTS idx_recordings_year ON recordings(year); -CREATE INDEX IF NOT EXISTS idx_recordings_genre ON recordings(genre); - --- Index for release_groups queries -CREATE INDEX IF NOT EXISTS idx_release_groups_name ON release_groups(name); -CREATE INDEX IF NOT EXISTS idx_release_groups_album_artist_credit_id ON release_groups(album_artist_credit_id); -CREATE INDEX IF NOT EXISTS idx_release_groups_year ON release_groups(year); - --- Index for artist_credit -CREATE INDEX IF NOT EXISTS idx_artist_credit_text ON artist_credit(text); - --- Index for release_group_recordings -CREATE INDEX IF NOT EXISTS idx_rgr_release_group_id ON release_group_recordings(release_group_id); -CREATE INDEX IF NOT EXISTS idx_rgr_recording_id ON release_group_recordings(recording_id); -``` - -#### Task 2.1.2: Fix release_groups UNIQUE constraint issue - -Current schema has `name TEXT NOT NULL UNIQUE` which breaks if two albums have the same name by different artists. - -Options: -1. Remove UNIQUE constraint (allow duplicates, rely on other fields) -2. Create composite unique on (name, album_artist_credit_id, year) -3. Add a generated hash column for uniqueness - -**Recommended:** Option 2 - composite unique constraint. - -Create migration to alter table or recreate with proper constraints. - -#### Task 2.1.3: Analyze query performance - -Use SQLite `EXPLAIN QUERY PLAN` on common queries to verify indexes are being used: - -```sql -EXPLAIN QUERY PLAN SELECT * FROM recordings WHERE artist_credit_id = ?; -``` - ---- - -### 2.2 Paginated Backend Queries - -#### Task 2.2.1: Add paginated track query - -Add to `backend/database/sql/queries/audio_files.sql`: - -```sql --- name: GetTracksPaginated :many -SELECT - af.id, - af.file_path, - af.length_milliseconds, - r.name as title, - COALESCE(ac.text, '') as artist, - COALESCE(rg.name, '') as album -FROM audio_files af -JOIN recordings r ON af.recording_id = r.id -LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id -LEFT JOIN release_group_recordings rgr ON r.id = rgr.recording_id -LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id -ORDER BY r.name -LIMIT ? OFFSET ?; - --- name: GetTracksCount :one -SELECT COUNT(*) FROM audio_files; -``` - -#### Task 2.2.2: Add filtered/sorted track query - -```sql --- name: GetTracksFiltered :many -SELECT ... -WHERE - (r.name LIKE ? OR ? = '') AND - (ac.text LIKE ? OR ? = '') AND - (rg.name LIKE ? OR ? = '') -ORDER BY - CASE WHEN ? = 'title' THEN r.name END, - CASE WHEN ? = 'artist' THEN ac.text END, - CASE WHEN ? = 'album' THEN rg.name END -LIMIT ? OFFSET ?; -``` - -#### Task 2.2.3: Update Library package with paginated methods - -Add to `backend/library/query.go`: - -```go -type TrackQuery struct { - Offset int - Limit int - SortBy string // "title", "artist", "album", "year" - SortDir string // "asc", "desc" - Search string // Search across title, artist, album -} - -type TracksResult struct { - Tracks []Track - TotalCount int -} - -func (l *Library) GetTracks(query TrackQuery) (TracksResult, error) -``` - -#### Task 2.2.4: Expose paginated query via Wails binding - -Add `GetTracks(query TrackQuery)` to FEBindings. - ---- - -### 2.3 Virtualized Lists - -#### Task 2.3.1: Install @lit-labs/virtualizer - -```bash -cd frontend && npm install @lit-labs/virtualizer -``` - -#### Task 2.3.2: Create virtualized track list component - -Create `frontend/src/components/track-list/virtualized-track-list.ts`: - -```typescript -import { LitElement, html, css } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; -import '@lit-labs/virtualizer'; -import { flow } from '@lit-labs/virtualizer/layouts/flow.js'; -import { GetTracks } from '@go/library/Library'; - -@customElement('virtualized-track-list') -export class VirtualizedTrackList extends LitElement { - @state() private tracks: Track[] = []; - @state() private totalCount = 0; - - private pageSize = 100; - private loadedPages = new Set(); - - override async connectedCallback() { - super.connectedCallback(); - await this.loadPage(0); - } - - private async loadPage(page: number) { - if (this.loadedPages.has(page)) return; - - const result = await GetTracks({ - offset: page * this.pageSize, - limit: this.pageSize, - sortBy: 'title', - sortDir: 'asc', - search: '', - }); - - this.totalCount = result.TotalCount; - this.loadedPages.add(page); - - // Merge into sparse array - const newTracks = [...this.tracks]; - result.Tracks.forEach((track, i) => { - newTracks[page * this.pageSize + i] = track; - }); - this.tracks = newTracks; - } - - private onVisibilityChanged(e: CustomEvent) { - const { first, last } = e; - const firstPage = Math.floor(first / this.pageSize); - const lastPage = Math.floor(last / this.pageSize); - - for (let p = firstPage; p <= lastPage + 1; p++) { - this.loadPage(p); - } - } - - override render() { - return html` - this.tracks[i])} - .renderItem=${(track: Track | undefined, index: number) => - track - ? html` this.onTrackClick(track)}>` - : html`` - } - .layout=${flow()} - @visibilityChanged=${this.onVisibilityChanged} - > - `; - } -} -``` - -#### Task 2.3.3: Create track-row component - -Create `frontend/src/components/track-list/track-row.ts` for individual track rendering. - -#### Task 2.3.4: Create track-row-skeleton component - -Loading placeholder while data is being fetched. - -#### Task 2.3.5: Create virtualized album grid - -Similar to track list but using `grid` layout: - -```typescript -import { grid } from '@lit-labs/virtualizer/layouts/grid.js'; - -.layout=${grid({ itemSize: { width: '180px', height: '220px' } })} -``` - -#### Task 2.3.6: Replace existing track-list and cover-grid - -Swap out the old components for virtualized versions. - ---- - -## Phase 3: Library Organization - -**Goal:** Provide powerful tools for organizing and finding music. - -**Settings to consider for this phase:** -- Default sort order for track lists -- Default columns displayed -- Search behavior (instant vs. press enter, search scope) -- Smart playlist default settings - -### 3.1 Search & Filtering - -#### Task 3.1.1: Add search input component - -Create `frontend/src/components/search/search-input.ts`: -- Text input with debounced onChange -- Dispatches search event or updates LibraryStore - -#### Task 3.1.2: Create LibraryStore - -Create `frontend/src/store/library-store.ts`: - -```typescript -interface LibraryState { - searchQuery: string; - sortBy: string; - sortDir: 'asc' | 'desc'; - filters: { - genre?: string; - year?: number; - artist?: string; - }; -} -``` - -#### Task 3.1.3: Create LibraryController - -Similar to PlayerController, connects components to LibraryStore. - -#### Task 3.1.4: Integrate search with virtualized list - -When search query changes: -1. Reset loaded pages -2. Update query parameters -3. Reload from page 0 - -#### Task 3.1.5: Add filter dropdowns - -Genre, year, artist filters that update LibraryStore. - ---- - -### 3.2 Custom Columns - -#### Task 3.2.1: Define available columns - -```typescript -interface ColumnDefinition { - id: string; - label: string; - field: string; // Path into track object - width: number; - sortable: boolean; -} - -const availableColumns: ColumnDefinition[] = [ - { id: 'title', label: 'Title', field: 'name', width: 200, sortable: true }, - { id: 'artist', label: 'Artist', field: 'artistName', width: 150, sortable: true }, - { id: 'album', label: 'Album', field: 'albumName', width: 150, sortable: true }, - { id: 'duration', label: 'Duration', field: 'lengthMilliseconds', width: 80, sortable: true }, - { id: 'year', label: 'Year', field: 'year', width: 60, sortable: true }, - { id: 'genre', label: 'Genre', field: 'genre', width: 100, sortable: true }, - { id: 'trackNum', label: '#', field: 'trackNumber', width: 40, sortable: true }, - // ... more columns -]; -``` - -#### Task 3.2.2: Create column selector UI - -Modal or dropdown where user can: -- Check/uncheck columns to show -- Drag to reorder columns - -#### Task 3.2.3: Persist column preferences - -Save selected columns and order to config. - -#### Task 3.2.4: Update track list to use dynamic columns - -Read column configuration and render accordingly. - ---- - -### 3.3 Smart Playlists - -**Note:** Basic playlist functionality (database schema, CRUD, UI) is implemented in Phase 1. This section extends playlists with smart/dynamic features. - -#### Task 3.3.1: Define smart playlist rule structure - -```typescript -interface SmartPlaylistRule { - field: string; // 'genre', 'year', 'artist', 'playCount', etc. - operator: string; // 'equals', 'contains', 'greaterThan', 'lessThan' - value: string | number; -} - -interface SmartPlaylistRules { - matchType: 'all' | 'any'; // AND vs OR - rules: SmartPlaylistRule[]; - limit?: number; - sortBy?: string; -} -``` - -#### Task 3.3.2: Implement smart playlist query builder - -Convert rules to SQL WHERE clause dynamically. - -#### Task 3.3.3: Create smart playlist editor UI - -Form to add/remove rules, preview results. - ---- - -### 3.4 Auto-Playlists - -#### Task 3.4.1: Implement "Recently Added" auto-playlist - -Query tracks sorted by date added, limit 100. - -#### Task 3.4.2: Implement "Recently Played" auto-playlist - -Requires tracking play history (new table). - -#### Task 3.4.3: Add play history tracking - -```sql -CREATE TABLE IF NOT EXISTS play_history ( - id INTEGER PRIMARY KEY, - audio_file_id INTEGER NOT NULL, - played_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(audio_file_id) REFERENCES audio_files(id) -); -``` - -Update Player to log plays. - ---- - -## Phase 4: UI Customization - -**Goal:** Allow users to arrange and customize the UI layout. - -**Settings to consider for this phase:** -- Active layout preset -- Per-component configurations (each component can define its own settings) -- Theme/appearance settings -- Sidebar default widths -- Visibility toggles for UI elements - -**Important:** This phase heavily integrates with the settings infrastructure. Each registered component should be able to define its own settings schema that appears in the settings window. - -### 4.1 Component Registry System - -#### Task 4.1.1: Define component registry interface - -```typescript -interface RegisteredComponent { - id: string; - name: string; - description: string; - component: typeof LitElement; - defaultSlot: 'main' | 'left-sidebar' | 'right-sidebar' | 'top-bar' | 'bottom-bar'; - allowedSlots: string[]; - defaultConfig: Record; -} -``` - -#### Task 4.1.2: Create component registry - -```typescript -// frontend/src/registry/component-registry.ts -class ComponentRegistry { - private components = new Map(); - - register(component: RegisteredComponent): void; - get(id: string): RegisteredComponent | undefined; - getAll(): RegisteredComponent[]; - getForSlot(slot: string): RegisteredComponent[]; -} - -export const componentRegistry = new ComponentRegistry(); -``` - -#### Task 4.1.3: Register existing components - -```typescript -componentRegistry.register({ - id: 'track-list', - name: 'Track List', - description: 'Display all tracks in a table', - component: TrackList, - defaultSlot: 'main', - allowedSlots: ['main'], - defaultConfig: {}, -}); - -componentRegistry.register({ - id: 'now-playing', - name: 'Now Playing', - description: 'Show current track info', - component: NowPlaying, - defaultSlot: 'bottom-bar', - allowedSlots: ['bottom-bar', 'left-sidebar', 'right-sidebar'], - defaultConfig: {}, -}); -``` - ---- - -### 4.2 Layout Configuration System - -#### Task 4.2.1: Define layout configuration structure - -```typescript -interface LayoutConfig { - 'top-bar': ComponentPlacement[]; - 'bottom-bar': ComponentPlacement[]; - 'left-sidebar': ComponentPlacement[]; - 'right-sidebar': ComponentPlacement[]; - 'main': ComponentPlacement[]; -} - -interface ComponentPlacement { - componentId: string; - config: Record; - order: number; -} -``` - -#### Task 4.2.2: Create LayoutStore - -Store current layout configuration, provide methods to modify. - -#### Task 4.2.3: Create layout persistence - -Save/load layout from config file or localStorage. - -#### Task 4.2.4: Create dynamic slot renderer - -Component that reads layout config and renders appropriate components in each slot. - -```typescript -@customElement('layout-slot') -class LayoutSlot extends LitElement { - @property() slotName: string; - - render() { - const placements = layoutStore.getSlot(this.slotName); - return html` - ${placements.map(p => { - const reg = componentRegistry.get(p.componentId); - const tag = reg.component.tagName; - return html`<${tag} .config=${p.config}>`; - })} - `; - } -} -``` - ---- - -### 4.3 Layout Editor UI - -#### Task 4.3.1: Create layout editor modal - -- Visual representation of slots -- Drag components between slots -- Add/remove components from slots - -#### Task 4.3.2: Create component configurator - -Per-component settings panel for components that support configuration. - -#### Task 4.3.3: Add layout presets - -Default layouts users can choose from: -- "Classic" (sidebar + main + bottom bar) -- "Minimal" (just player controls) -- "Full" (all panels visible) - ---- - -## Phase 5: MusicBrainz Integration - -**Goal:** Enable automatic metadata tagging via MusicBrainz. - -**Settings to consider for this phase:** -- AcoustID API key -- Auto-tag behavior (prompt always, auto-accept high confidence, etc.) -- Minimum confidence threshold for auto-accept -- Which metadata fields to overwrite -- Backup original tags before overwriting - -### 5.1 MusicBrainz API Client - -#### Task 5.1.1: Create MusicBrainz package - -`backend/musicbrainz/client.go`: -- HTTP client with rate limiting (1 req/sec per MB guidelines) -- User-Agent header with app name and contact - -#### Task 5.1.2: Implement recording search - -```go -func (c *Client) SearchRecordings(query string) ([]Recording, error) -func (c *Client) GetRecording(mbid string) (*Recording, error) -``` - -#### Task 5.1.3: Implement release search - -```go -func (c *Client) SearchReleases(query string) ([]Release, error) -func (c *Client) GetRelease(mbid string) (*Release, error) -``` - -#### Task 5.1.4: Implement artist search - -```go -func (c *Client) SearchArtists(query string) ([]Artist, error) -``` - ---- - -### 5.2 AcoustID Integration - -#### Task 5.2.1: Integrate chromaprint for fingerprinting - -Use chromaprint library to generate audio fingerprints. - -#### Task 5.2.2: Create AcoustID client - -```go -func (c *AcoustIDClient) Lookup(fingerprint string, duration int) ([]AcoustIDResult, error) -``` - -#### Task 5.2.3: Map AcoustID results to MusicBrainz - -AcoustID returns MusicBrainz recording IDs; use those to fetch full metadata. - ---- - -### 5.3 Autotag Workflow - -#### Task 5.3.1: Create autotag service - -`backend/autotag/autotag.go`: - -```go -type AutotagResult struct { - FilePath string - MatchConfidence float64 - CurrentMetadata TrackMetadata - SuggestedMetadata TrackMetadata - MBRecordingID string -} - -func (s *Service) AnalyzeTrack(filePath string) (*AutotagResult, error) -func (s *Service) AnalyzeAlbum(tracks []string) ([]AutotagResult, error) -func (s *Service) ApplyTags(result *AutotagResult) error -``` - -#### Task 5.3.2: Create autotag UI component - -- Show current vs suggested metadata side-by-side -- Confidence indicator -- Accept/reject buttons -- Batch operations for albums - -#### Task 5.3.3: Implement tag writing - -Write accepted metadata back to audio files using tag library. - ---- - -### 5.4 MusicBrainz Visual Browser - -#### Task 5.4.1: Create artist browser view - -- Search artists -- View artist discography -- Click release to see tracklist - -#### Task 5.4.2: Create release browser view - -- Album art (from Cover Art Archive) -- Track listing -- Credits and relationships - -#### Task 5.4.3: Link local tracks to MB entities - -Show which local tracks match MB recordings; allow manual linking. - ---- - -## Phase 6: Device Sync - -**Goal:** Sync music to Android devices with optional re-encoding. - -**Settings to consider for this phase:** -- Per-device sync profiles (encoding quality, playlists to sync) -- Encoding cache location and size limit -- Sync behavior (delete removed tracks from device, etc.) -- Custom encoding profiles (advanced users) -- FFmpeg binary path (if not bundled) - -### 6.1 Android Sync (MTP) - -#### Task 6.1.1: Research MTP libraries for Go - -Options: -- libmtp bindings -- gousb for raw USB -- Call external tools (jmtpfs, go-mtpfs) - -#### Task 6.1.2: Create device detection - -Detect connected MTP devices, list storage volumes. - -#### Task 6.1.3: Create file transfer service - -```go -type SyncService struct { - // ... -} - -func (s *SyncService) GetDevices() ([]Device, error) -func (s *SyncService) SyncPlaylist(device Device, playlist Playlist, profile EncodingProfile) error -func (s *SyncService) SyncTracks(device Device, tracks []Track, profile EncodingProfile) error -``` - ---- - -### 6.2 Re-encoding Pipeline - -#### Task 6.2.1: Integrate FFmpeg - -Use FFmpeg for transcoding. Options: -- Call ffmpeg binary -- Use go-ffmpeg bindings - -#### Task 6.2.2: Define encoding profiles - -```go -type EncodingProfile struct { - Name string - Format string // "mp3", "aac", "opus" - Bitrate int // kbps - SampleRate int // Hz -} - -var presets = []EncodingProfile{ - {Name: "High Quality MP3", Format: "mp3", Bitrate: 320, SampleRate: 44100}, - {Name: "Balanced MP3", Format: "mp3", Bitrate: 192, SampleRate: 44100}, - {Name: "Space Saver", Format: "mp3", Bitrate: 128, SampleRate: 44100}, -} -``` - -#### Task 6.2.3: Create encoding cache - -Cache encoded files to avoid re-encoding on every sync: -- Hash source file + profile = cache key -- Store encoded files in cache directory - -#### Task 6.2.4: Create sync progress UI - -- Device selection -- Playlist/track selection -- Encoding profile selection -- Progress bar with current file -- Cancel button - ---- - -## Phase 7: Cross-Platform Polish - -**Goal:** Ensure excellent experience on Windows and macOS. - -**Settings to consider for this phase:** -- System tray behavior (minimize to tray, close to tray) -- Startup behavior (start minimized, start with system) -- Media key handling (enable/disable) -- Notification preferences (track change, etc.) -- File association preferences - -### 7.1 Platform Testing - -#### Task 7.1.1: Set up Windows build environment - -- Windows VM or machine -- Go + Node.js toolchain -- Wails CLI - -#### Task 7.1.2: Set up macOS build environment - -- macOS machine (required for signing) -- Xcode command line tools -- Go + Node.js toolchain - -#### Task 7.1.3: Fix platform-specific issues - -Test and fix: -- File paths (forward vs backslash) -- System directories -- Audio device handling -- Window chrome differences - ---- - -### 7.2 Platform Integration - -#### Task 7.2.1: System media key support - -Respond to keyboard media keys (play/pause, next, previous). - -Research: -- Windows: RegisterHotKey or low-level keyboard hook -- macOS: SPMediaKeyTap or MediaKeySession -- Linux: D-Bus MPRIS - -#### Task 7.2.2: MPRIS integration (Linux) - -Implement MPRIS D-Bus interface for integration with desktop environments. - -#### Task 7.2.3: System tray icon - -Minimize to tray, show playback controls in tray menu. - -#### Task 7.2.4: Native notifications - -Show track change notifications using system notification APIs. - ---- - -### 7.3 Distribution - -#### Task 7.3.1: Create installer for Windows - -- NSIS or WiX installer -- Start menu shortcut -- File associations (.mp3, .flac, etc.) - -#### Task 7.3.2: Create DMG for macOS - -- Signed and notarized app bundle -- Drag-to-Applications installer - -#### Task 7.3.3: Create packages for Linux - -- AppImage (universal) -- .deb (Debian/Ubuntu) -- .rpm (Fedora) -- Flatpak (sandboxed) - -#### Task 7.3.4: Set up CI/CD for releases - -GitHub Actions workflow to: -- Build for all platforms -- Run tests -- Create release artifacts -- Publish to GitHub Releases - ---- - -## Technical Debt Items - -These items should be addressed as time permits, integrated with feature work: - -### Testing - -- [ ] Unit tests for PlayerStore -- [ ] Unit tests for Go Queue package -- [ ] Unit tests for Go Library scanning -- [ ] Integration tests for Wails event flow -- [ ] E2E tests for critical user flows - -### Documentation - -- [ ] User documentation / help pages -- [ ] Developer setup guide -- [ ] Architecture documentation -- [ ] API documentation for plugin authors (future) - -### Code Quality - -- [ ] Consistent error handling patterns in Go -- [ ] Consistent logging throughout -- [ ] Performance profiling and optimization -- [ ] Accessibility audit (keyboard navigation, screen readers) - -### Security - -- [ ] Input validation for all user inputs -- [ ] Safe file path handling -- [ ] Sanitize metadata before display (XSS prevention) - ---- - -## Decision Log - -Key architectural decisions made during planning: - -| Decision | Choice | Rationale | -|----------|--------|-----------| -| Queue model | Queue as special playlist | Unified data structures, queue can be saved as playlist | -| Queue/Playlist location | Go backend | Tighter integration with Player, single source of truth | -| Frontend state | Custom store + Controllers | Production-ready, integrates with Wails events | -| Virtualization | @lit-labs/virtualizer | Native Lit integration, supports both list and grid | -| State management lib | None (custom) | Signals not production-ready, custom gives full control | -| Track per file | Yes (no deduplication) | Practical for real-world music libraries | -| Testing | Deferred | Focus on architecture first, add tests incrementally | -| Settings architecture | Unified, extensible | Each feature adds its own config section; single settings UI | -| Playlists in Phase 1 | Yes | Core feature, needed for queue; smart playlists in Phase 3 | -- 2.54.0