knowledge.md 10 KB

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 <token> 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