Cloud Upload v1 — Shaped Brief
Approved: 2026-03-18
Deliberation: 3 models (Codex scope, Gemini challenge, Claude requirements)
Problem
MixBoard can browse and stream the chad-music cloud library but can't add music.
Adding music currently requires SSH + torrents/slskd + beets import — no path from
the macOS app.
Goal
Upload a local audio file from MixBoard to the chad-music server, where it becomes
immediately available for streaming (via taglib rescan), and is later enriched by
beets running as a background cron job.
Non-goals (v1)
- Multi-file upload (one at a time is fine)
- Dedup (beets background handles it implicitly; no server-side dedup today)
- Resumable uploads
- Auto-upload on play
- Upload queue persistence across app restarts
- Drag-and-drop from Finder
- Batch album upload with metadata editing UI
Acceptance Criteria
Appetite
Small-Medium:
- Server: 1 new endpoint (PUT /api/upload), ~3 functions in server.lisp,
nginx config change (client_max_body_size), cron job for beets
- Client: 1 new service file (UploadService.swift), toolbar button + progress
indicator in existing views
- No new dependencies on either side
Architecture
Upload protocol: Raw body PUT (not multipart)
PUT /api/upload
Headers:
Authorization: Bearer <token>
X-Filename: "05 - Echoes.flac"
Content-Type: audio/flac (or audio/mpeg, audio/mp4, etc.)
Content-Length: <size>
Body: raw audio file bytes
Response (synchronous):
200 {"status": "imported", "tracks_added": 1, "albums_updated": 1}
409 {"status": "error", "message": "File already exists"}
413 {"status": "error", "message": "File too large"}
400 {"status": "error", "message": "Unsupported format"}
Server-side flow (immediate)
with-user auth check
- Validate Content-Type (allow mp3, flac, m4a, wav, aiff, ogg)
- Read raw body → stream to disk in upload directory
(e.g.,
/data/upload/mixboard/<timestamp>-<filename>)
- Trigger synchronous rescan of the upload directory
- Return result with added/updated counts
Server-side flow (background — beets)
- Cron job (e.g., every 5 min):
beet import --move --quiet /data/upload/mixboard/
then curl -X POST http://localhost:5000/api/rescan
- This is the same pattern chatikbot already uses (run-import → request-chad-music-rescan)
- Zero server.lisp changes needed for the beets part — just a cron entry
What beets does to uploaded files
- Recognized files: beets moves from upload dir to organized library path
(Artist/Album/Track.ext), enriches tags with MusicBrainz data, downloads cover art.
On next rescan: old path entry disappears, new enriched entry appears.
- Unrecognized files: beets skips them (--quiet). They stay in upload dir,
remain visible in library with their original embedded tags. No data loss.
Client-side flow
- Toolbar button → NSOpenPanel (single file, audio UTTypes)
- URLSession upload task with PUT, progress via delegate
- Progress indicator (linear bar or circular) near toolbar
- On success: "Track uploaded!" toast + auto-refresh cloud browser
- On failure: error alert with server message
- Cancel via URLSession task cancellation
Technical Constraints
- Nginx: set
client_max_body_size 200m in server block
- Server: file must be written by the same user that runs chad-music
(so rescan can read it). No sudo needed for the upload endpoint.
- Upload directory must be under a path that config.lisp maps to a URL prefix
(so file-server can serve it for streaming)
- Beets cron runs as user
uploader (existing pattern from chatikbot)
- Accepted formats: mp3, flac, m4a, aac, wav, aiff, ogg
Dependencies & Blockers
- Verify Woo/SBCL can stream request body to disk without loading into
memory (critical for 100MB+ FLAC files — test with curl upload first)
- Nginx
client_max_body_size config change
- Enikesha needs to review/merge server-side changes (PR workflow)
- Beets cron job needs to be added on the server
Risks
- Woo body streaming: if Woo buffers entire body in memory, 100MB FLAC = OOM.
Mitigation: test early with curl. Fallback: nginx upload module to buffer
to disk before proxying.
- File organization: without beets (before cron runs), uploaded files land
in a flat upload directory. Not terrible — they're playable immediately.
Beets organizes them on its next run.
- OGG files: chad-music's taglib parses OGG, but MixBoard's AVPlayer can't
play OGG streams. Upload works, playback doesn't. Document this limitation.
- Two-state tracks: after upload, track appears with original tags. After
beets runs, it "moves" (new path = new track ID). For single user, acceptable.
Deferred to v2
- Multi-file / album batch upload
- Server-side dedup (chromaprint/acoustid)
- Drag-and-drop from Finder to cloud section
- Upload from "now playing" context menu
- Auto-upload toggle
- Resumable uploads
- Upload history view
- Beets on the synchronous upload path (instead of background cron)