chore: track docs/superpowers specs and plans, fix gitignore

feat/music-streaming
Laurent 1 month ago
parent 10e147b447
commit 1aac6823fa
  1. 1
      .gitignore
  2. 2092
      docs/superpowers/plans/2026-05-23-music-player.md
  3. 218
      docs/superpowers/specs/2026-05-23-music-player-design.md

1
.gitignore vendored

@ -1,4 +1,3 @@
.superpowers/
docs/superpowers/
.DS_Store
xcuserdata/

File diff suppressed because it is too large Load Diff

@ -0,0 +1,218 @@
# Music Player — Design Spec
A high-performance macOS music player built with SwiftUI + AppKit. Reads a local music collection from disk, indexes metadata into SQLite for fast search/sort/filter, and plays audio via AVPlayer.
## Constraints
- Library size: 10,000+ tracks, 100GB+, growing
- All search, sort, and filter operations must feel instant (<50ms)
- macOS 15.6+ (current project target)
- No external music service integration — local files only
## Data Model
SQLite database via GRDB, stored at `~/Library/Application Support/Music/db.sqlite`.
### `tracks` table
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER PRIMARY KEY | Auto-increment |
| fileURL | TEXT, UNIQUE | Absolute path to audio file |
| title | TEXT | From metadata, fallback to filename |
| artist | TEXT | |
| albumArtist | TEXT | |
| album | TEXT | |
| genre | TEXT | |
| year | INTEGER | |
| trackNumber | INTEGER | |
| discNumber | INTEGER | |
| duration | DOUBLE | Seconds |
| bpm | INTEGER | |
| composer | TEXT | |
| fileFormat | TEXT | mp3, m4a, flac, etc. |
| bitrate | INTEGER | kbps |
| sampleRate | INTEGER | Hz |
| fileSize | INTEGER | Bytes |
| artworkData | BLOB, nullable | Embedded cover art |
| playCount | INTEGER DEFAULT 0 | Tracked by app |
| lastPlayedAt | DATETIME, nullable | Tracked by app |
| rating | INTEGER DEFAULT 0 | 0–5, tracked by app |
| dateAdded | DATETIME | When imported |
| dateModified | DATETIME | File modification date |
| fileHash | TEXT | fileSize + modificationDate for change detection |
### Indexes
- Individual: `artist`, `album`, `genre`, `year`
- Compound: `(albumArtist, album, discNumber, trackNumber)` for album browsing order
### FTS5 virtual table
- Indexed columns: `title`, `artist`, `albumArtist`, `album`, `genre`, `composer`
- Kept in sync via GRDB FTS5 triggers
## Architecture
Four layers, strict downward dependency only.
### UI Layer
- `NSTableView` wrapped in `NSViewRepresentable` for the track list
- SwiftUI for search bar and player controls
- No business logic in views
### ViewModel Layer
**LibraryViewModel** (ObservableObject):
- Holds current query state: search text, sort column, sort order
- Subscribes to GRDB `ValueObservation` — receives `[Track]` on every query change
- Exposes results to the NSTableView data source
**PlayerViewModel** (ObservableObject):
- Current track, playback state (playing/paused/stopped), current time, duration
- Queue management: current queue (array of tracks), current index
- Shuffle state and shuffled queue
### Service Layer
**DatabaseService**:
- Owns the GRDB `DatabasePool`
- Schema migrations
- Query builders for search (FTS5 MATCH), sort (ORDER BY indexed columns), filter
**ScannerService**:
- Background folder walking with `FileManager`
- Metadata extraction via `AVAsset.metadata` (async)
- Artwork extraction from embedded tags
- Incremental rescan via `fileHash` comparison
- Progress reporting
**AudioService**:
- `AVPlayer` wrapper
- Play, pause, stop, seek, next, previous, volume
- Periodic time observation for progress bar
- Updates `playCount`/`lastPlayedAt` in DB when track finishes or passes 50% played
### Data Layer
- SQLite + FTS5 via GRDB
- Database file at `~/Library/Application Support/Music/db.sqlite`
### Data Flows
**Scan**: User picks folder → `ScannerService` walks directory on background thread → extracts metadata with `AVAsset` → inserts into DB → `ValueObservation` fires → `LibraryViewModel` updates → `NSTableView` reloads
**Search**: User types in search bar → `LibraryViewModel` updates query string → new SQL query with FTS5 MATCH → `ValueObservation` emits new results → table reloads
**Sort**: User clicks column header → `LibraryViewModel` updates sort column/order → new SQL ORDER BY → same reactive flow
**Play**: User double-clicks row → `PlayerViewModel` receives track → `AudioService` loads file URL → playback starts → controls update
## UI Layout
Single window, three vertical zones.
### Zone 1: Search Bar (top)
- Full-width text field with placeholder: "Search by title, artist, album, genre..."
- Track count label on the right (e.g., "10,342 tracks")
- SwiftUI `TextField`
### Zone 2: Track List (middle, fills available space)
- `NSTableView` via `NSViewRepresentable`
- Columns: Title, Artist, Album, Genre, Duration
- Sortable by clicking column headers (ascending/descending toggle)
- Currently playing track: blue left accent bar + play indicator in title cell
- Alternating row colors (white / light gray) for readability
- Double-click to play, single-click to select
- Light theme: white backgrounds, Apple system gray text hierarchy
### Zone 3: Player Controls (bottom)
Three sections in a horizontal grid:
**Left — Now Playing**:
- Album artwork thumbnail (44×44, rounded corners)
- Track title (bold) + "Artist — Album" subtitle
**Center — Transport**:
- Shuffle toggle (dimmed when off, highlighted when on)
- Previous, Play/Pause, Next buttons
- Progress bar with current time / total time
**Right — Volume**:
- Speaker icon + volume slider
## Scanner & Import
### Folder scanning (primary)
- User selects root folder via `NSOpenPanel`
- Recursive directory walk on background thread
- Supported extensions: `.mp3`, `.m4a`, `.aac`, `.wav`, `.aiff`, `.alac`, `.flac`
- Metadata extraction via `AVAsset.metadata`
- Artwork from embedded tags → stored as BLOB
- `fileHash` = file size + modification date (fast, sufficient for change detection)
- Progress reported: "Scanning... 4,231 / 10,342 tracks"
- Watched folder path stored in `UserDefaults`
### Drag and drop (secondary)
- Files/folders dropped onto the window
- Folders recursively scanned same as above
- Duplicates detected by `fileURL` uniqueness constraint — silently skipped
### Incremental rescan
- Triggered on app launch or manually
- New files → insert
- Changed files (different `fileHash`) → re-extract metadata, update row
- Missing files → remove from DB
## Playback & Queue
### Queue model
- Current track list (full library or filtered/searched results) acts as the play queue
- Double-click a track → starts playing from that position in the current queue
- Next/Previous navigate within the queue
- Searching/filtering while playing does not interrupt playback — queue only changes on explicit play action
### Shuffle
- Toggle on → build shuffled copy of current queue, starting from current track
- Toggle off → return to original queue position
- New search/filter while shuffle is on → re-shuffle new result set
### Play tracking
- `playCount` incremented and `lastPlayedAt` updated when a track finishes or passes 50% played
### Not in v1
- Repeat modes (repeat-one, repeat-all)
- Crossfade
- Equalizer
- Playlists
## Error Handling
- **Unreadable audio file** (corrupt, DRM, unsupported) → skip during scan, log to console
- **File moved/deleted after import** → AVPlayer fails to load → inline error in player controls ("File not found"), auto-advance to next track
- **Metadata extraction fails** → use filename as title, "Unknown" for artist/album, still import
- **Folder permission denied** → system alert, prompt to grant access in System Settings
- **Database migration failure** → surface error, do not launch with broken schema
## Dependencies
- **GRDB** (Swift Package) — SQLite wrapper with reactive observation
- **AVFoundation** (system) — audio playback and metadata extraction
- **AppKit** (system) — NSTableView for high-performance track list
- **SwiftUI** (system) — search bar, player controls, app structure
## Audio Format Support
Natively handled by AVFoundation on macOS:
- MP3, AAC/M4A, WAV, AIFF, ALAC, FLAC
Loading…
Cancel
Save