# 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