cloud-upload-v1.md 5.4 KB

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

  • User can select a local audio file via toolbar button → file picker and upload it to chad-music
  • Server saves the file to a designated upload directory, then triggers rescan
  • Upload progress (bytes sent) is visible in the UI as a progress indicator
  • After rescan completes, server returns success and the track is browsable in cloud library
  • If upload or processing fails, user sees a clear error message
  • User can cancel an in-progress upload

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)

  1. with-user auth check
  2. Validate Content-Type (allow mp3, flac, m4a, wav, aiff, ogg)
  3. Read raw body → stream to disk in upload directory (e.g., /data/upload/mixboard/<timestamp>-<filename>)
  4. Trigger synchronous rescan of the upload directory
  5. 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

  1. Toolbar button → NSOpenPanel (single file, audio UTTypes)
  2. URLSession upload task with PUT, progress via delegate
  3. Progress indicator (linear bar or circular) near toolbar
  4. On success: "Track uploaded!" toast + auto-refresh cloud browser
  5. On failure: error alert with server message
  6. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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)