You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
218 lines
7.6 KiB
218 lines
7.6 KiB
# 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
|
|
|