# MixBoard — Knowledge Base > Accumulated findings from investigations, deliberations, and development sessions. > Updated automatically after each significant investigation. --- ## Chad Music Integration (2026-03-13) ### ADR: API Strategy — Custom REST over Subsonic (2026-03-13) - Decision: Keep chad-music's custom REST API, no Subsonic implementation - Rationale: MixBoard is the only client. Subsonic = 70+ endpoints for zero benefit. All three reviewer models agreed. - Status: Active - Key entities: ChadMusicAPIClient, /api/cat/:category, /api/album/:id/tracks ### ADR: Streaming — Direct HTTP + Range Requests (2026-03-13) - Decision: Direct HTTP file serving with Range requests (206 Partial Content). No HLS in early phases. - Rationale: HLS breaks gapless playback on concept albums. Single user = no need for adaptive bitrate. AVPlayer handles direct HTTP natively. - Status: Active - Key entities: AVPlayer, AVURLAsset, AVURLAssetHTTPHeaderFieldsKey ### ADR: Playback Engine — AVPlayer First (2026-03-13) - Decision: AVPlayer for ALL playback in Phase 1 (both local file:// and cloud https:// URLs). AVAudioEngine path added in Phase 3 for cached tracks needing EQ/BPM. - Rationale: AVPlayer handles buffering, seeking, gapless (AVQueuePlayer), and AirPlay out of the box. Dual engines add complexity. - Status: Active ### ADR: Auth — API Key via HTTP Header (2026-03-13) - Decision: API key in macOS Keychain, sent via Authorization: Bearer header. Use AVURLAssetHTTPHeaderFieldsKey for stream URLs (undocumented but working Apple API). - Rationale: Simplest auth for personal use. Telegram login deferred to Phase 4. - Status: Active - Risk: AVURLAssetHTTPHeaderFieldsKey is undocumented — may break in future macOS versions ### Key Risks Identified 1. Woo/Clack Range request support — MUST verify with curl before coding 2. Static file auth — file-server routes may bypass with-user macro 3. TLS — AVPlayer rejects self-signed certs, need Tailscale or Caddy 4. OGG Vorbis — NOT supported by AVPlayer on macOS ### Phase Plan - Phase 1 (Stream & Browse): ~20-35 hours — zero backend changes needed - Phase 2 (Upload & Sync): ~20-30 hours — new server endpoints - Phase 3 (Native Polish): ~15-20 hours — cache, loudness, Spotlight - Phase 4 (Social & Advanced): ~10-20 hours — Telegram, Shortcuts ### Brainstorm Spec Full spec at: Work vault → .orchestra/mixboard-chadmusic/spec.md ### Phase 1 Implementation (2026-03-13) — COMPLETE - **Files created**: ChadMusic.swift (models), KeychainService.swift, ChadMusicAPIClient.swift, StreamingPlayer.swift, CloudBrowserView.swift - **Files modified**: PlayerViewModel.swift (dual engine routing), SettingsView.swift (Chad Music tab), SidebarView.swift (cloud section), ContentView.swift (cloud browser routing) - **Architecture**: StreamingPlayer (AVPlayer) for cloud, AudioEngine (AVAudioEngine) for local — routed via isCloudPlayback flag in PlayerViewModel - **Cloud tracks are transient** — ChadTrack is Codable struct, not SwiftData. No DB persistence until Phase 2 sync. - **Build note**: Must run `xcodegen generate` after adding new source files - **Codex review finding (fixed)**: URL path composition changed from `.appending(path:)` to `URLComponents` for proper encoding - **Gemini review**: False positive — applied D365 rubric to Swift project (review extension prompt template still has D365 residue for this workspace) - **Tests**: 143 pass (38 new ChadMusic tests), 0 failures, no regressions ### Pre-flight Verification Results (2026-03-13) #### 1. TLS / Network - Server is **publicly reachable** — no Tailscale needed for connectivity - **URL: `https://music.chad-partners.com`** (confirmed via curl 2026-03-14) - Behind **Nginx 1.27.0** reverse proxy with HTTPS (HSTS enabled, max-age=31536000) - Gogs domain: `gogs.chad-partners.com` - **Default port is 5000** (from `main` function: `&key (port 5000)`) — Nginx proxies to it - `file-server` is only enabled when `serve-files` flag is passed to `main` #### 1b. Range Request Support — VERIFIED ✅ - `curl -sI -H 'Range: bytes=0-1000' https://music.chad-partners.com/` → **HTTP 206**, Content-Range: bytes 0-1000/2998 - Nginx handles Range natively — no server-side changes needed - AVPlayer streaming will work out of the box #### 2. Server Architecture (from server.lisp, verified 2026-03-14) - **Woo HTTP server** (libev-based) via Clack - Default port: 5000 - Auth: Telegram-based login flow → generates random 16-byte hex token → stored in server-side hash table - `with-user` macro checks `Authorization: Bearer ` header - CLI bypass: if no `x-real-ip` header AND no valid token → auto-auth as admin (for local dev/REPL) - File serving: only active when `(main :serve-files t)` is passed - Path mapping: config.lisp maps filesystem paths to URL prefixes (e.g. `/media/music/` → `/music/`) - Routes: `/api/cat/:category`, `/api/cat/:category/size`, `/api/album/:id/tracks`, `/api/stats`, `/api/rescan` (POST), `/api/login` (POST), `/api/user` #### 3. API Response Format (from %to-json methods) - Track JSON keys: `id`, `artist`, `album_artist`, `album`, `year`, `no` (track number), `title`, `bit_rate`, `duration`, `url`, `cover` - Album JSON keys: `id`, `artist`, `year`, `album`, `original_date`, `publisher`, `country`, `genre`, `type`, `status`, `mb_id`, `track_count`, `total_duration`, `cover` - Category results: array of `{"item": "name", "count": N}` - **NOTE**: Track number key is `no`, not `track_number`. Album title key is `album`, not `title`. Must update ChadMusic.swift CodingKeys! #### 4. File-Server Auth — FILES ARE UNPROTECTED (when enabled) - `file-server` function does NOT use `with-user` macro - All API routes ARE auth-protected - Acceptable for personal server — file URLs are opaque (need API to discover them) - Fix in Phase 2: wrap file-server in with-user ### Cloud Upload — Phase 2 Implementation (2026-03-18) #### ADR: Upload Protocol — Raw Body PUT (2026-03-18) - Decision: Raw body PUT with X-Filename header (not multipart form upload) - Rationale: Simpler server-side, matches existing chad-music pattern. URLSession upload task handles progress natively. - Status: Active #### ADR: Upload Storage — Atomic Write Pattern (2026-03-18) - Decision: Write to temp file (.tmp suffix), atomic rename on success, delete on error - Rationale: Codex + Gemini flagged partial file corruption risk on interrupted uploads - Status: Active - Key entities: upload-file (server.lisp), UploadService.swift #### Key Technical Findings - `UTType.flac` does NOT exist in Xcode 16 / macOS 14 SDK — use `UTType(filenameExtension: "flac")` - Woo/Clack `:raw-body` is a binary stream — safe for chunked reads - `get-universal-time` has 1-second resolution — use timestamp + random suffix for collision avoidance - Content-Type headers may include parameters (e.g., `audio/mpeg; charset=binary`) — strip before matching - `*rescan-lock*` is mutex for DB rescans — upload endpoint must acquire it #### Files Created/Modified - **Created**: `Sources/Services/UploadService.swift` - **Modified**: `Sources/Views/CloudBrowserView.swift` — upload button, progress, success/error - **Artifacts**: `.orchestra/cloud-upload-v1/server-patch.lisp`, `nginx-and-cron.conf` - **Server PR**: `feature/upload-endpoint` branch on chad-music — **merged & deployed by Enikesha (2026-03-18)** ### Offline Download v1 — Implementation (2026-03-18) #### ADR: Download Storage — Application Support (2026-03-18) - Decision: Store persistent downloads in `~/Library/Application Support/MixBoard/CloudTracks/{cloudTrackId}.{ext}` - Rationale: All 3 reviewers agreed ~/Library/Caches/ is wrong for user-initiated downloads (macOS can purge). Application Support survives OS cleanup and Time Machine backup. - Status: Active - Key entities: DownloadService.persistentStorageDirectory, Track.localCachePath #### ADR: Separate localCachePath from filePath (2026-03-18) - Decision: New `localCachePath: String?` field on Track model, separate from `filePath`. `hasLocalFile` unchanged. - Rationale: `hasLocalFile` is used by export pipeline and many other consumers. Changing it would break existing functionality. New `hasPlayableLocalFile` computed property checks both paths. - Status: Active - Key entities: Track.localCachePath, Track.hasPlayableLocalFile, Track.playableFileURL #### ADR: Playback Routing — hasPlayableLocalFile First (2026-03-18) - Decision: Check `track.hasPlayableLocalFile` first → AudioEngine. Fallback to StreamingPlayer if AudioEngine fails (format incompatibility). Only stream if no local file. - Rationale: Downloaded cloud tracks should play via AudioEngine for EQ/BPM/waveform. OGG format can't be opened by AVAudioFile but stb_vorbis decoder handles it via AudioEngine's OGG path. - Status: Active - Key entities: PlayerViewModel.loadAndPlay, PlayerViewModel.loadAndPlayDirect, PlayerViewModel.playViaStreamingPlayer #### Files Created/Modified - **Created**: `Sources/Services/DownloadManager.swift` — observable singleton for tracking active downloads - **Created**: `Sources/Views/DownloadIndicator.swift` — four-state inline download button (20pt) - **Created**: `Sources/Views/AlbumDownloadButton.swift` — album header aggregate download button - **Created**: `Sources/Views/PlaylistDownloadButton.swift` — playlist header download count button - **Modified**: `Sources/Models/Track.swift` — added DownloadState enum, localCachePath, downloadStateRaw, hasPlayableLocalFile, playableFileURL - **Modified**: `Sources/Services/DownloadService.swift` — added downloadPersistent(), removeDownload(), persistentStorageDirectory - **Modified**: `Sources/Services/AudioEngine.swift` — added optional fileURL parameter to loadTrack() - **Modified**: `Sources/ViewModels/PlayerViewModel.swift` — routing change + playViaStreamingPlayer helper - **Modified**: `Sources/Views/CloudBrowserView.swift` — download indicator in CloudTrackRow, album download button, download context menus, persisted track lookup - **Modified**: `Sources/Views/TrackRow.swift` — downloaded cloud tracks show green arrow.down.circle.fill instead of cloud.fill - **Modified**: `Sources/Views/PlaylistView.swift` — download context menus for cloud tracks, PlaylistDownloadButton in header - **Tests**: 172 pass (1 pre-existing failure in testAllFormatsExport — unrelated E2E test), 0 new failures