Cloud Download for Export v1 — Shaped Brief
Approved: 2026-03-18
Deliberation: 3 models (Codex scope, Gemini challenge, Claude requirements)
Problem
Cloud tracks from Chad Music are skipped during DAW export. The export pipeline
(MixExporter.copyAudioFiles) skips tracks where hasLocalFile == false. Users
who mix local and cloud tracks in playlists get incomplete Audition/Bitwig/REAPER
sessions.
Goal
When exporting a playlist that contains cloud tracks, MixBoard downloads those
tracks and includes them in the export — same result as if the files were local.
Non-goals (v1)
- Standalone "Download" button / right-click → Download (separate feature)
- Offline mode / persistent cache management
- Resumable downloads
- Background pre-caching when tracks are added to playlists
- Download queue UI (download is part of the export flow, not a separate surface)
Acceptance Criteria
Appetite
Small:
- 1 new service file (
DownloadService.swift, ~150 lines) — mirrors UploadService
pattern
- Modify
MixExporter.copyAudioFiles to become async + call DownloadService for
cloud tracks
- Update
ExportSheet UI for download progress / pre-flight dialog
- ~3-4 files changed total, no new dependencies, no model schema changes
Architecture
Critical prerequisite — VERIFIED
When cloud tracks are added to playlists, Track.fromCloud() creates a SwiftData
@Model with isCloud=true, cloudStreamPath set, and filePath="". The export
pipeline already has Track objects — they're just skipped by guard track.hasLocalFile.
No ChadTrack → Track boundary problem.
DownloadService API
func download(track: Track, apiClient: ChadMusicAPIClient, to directory: URL) async throws -> URL
- Accepts
Track (not ChadTrack) since the export pipeline works with Track @Model
- Downloads to an export-scoped temp directory — cleaned up after export
- Uses
URLSession.download(for:) with progress
- File extension derived from
cloudStreamPath (paths like /music/Artist/Album/track.flac)
- Falls back to
Content-Type response header if path extension is missing
- Validates downloaded file exists and has non-zero size before returning
Export pipeline changes
MixExporter.copyAudioFiles becomes async
- Before copying, partition tracks into local vs cloud
- Download cloud tracks with bounded concurrency (
TaskGroup, max 3-4 simultaneous)
- On download failure: skip track, collect into "missing tracks" report
- After export: delete temp download directory
Pre-flight UX
- Export detects cloud tracks → shows confirmation dialog with count
- Two-phase progress: download phase (track-by-track), then export phase
Technical Constraints
cloudStreamPath stores URL paths like /music/Artist/Album/track.flac —
extension is reliably present
- Stream URLs are direct HTTP (Nginx, verified 206 Range support) — same GET as
streaming
ChadMusicAPIClient.streamURL(for:) and .authHeaders already exist
- No backend changes needed — server already serves full files at stream URLs
Dependencies & Blockers
None. All prerequisites exist.
Risks
- Large playlists: 50 cloud tracks × 50MB = 2.5GB download before export.
Bounded concurrency helps but user needs patience. Pre-flight dialog makes this
visible.
- Network interruption: Partial failure handled by "export what you can" policy.
- Disk space: No explicit check in v1 (URLSession will fail naturally if disk
is full).
- OGG Vorbis: Files might exist in cloud library. They'll download fine but may
not be usable in all DAWs. Not our problem — include as-is.
Not Now (Deferred)
- Standalone download feature (right-click → Download, toolbar button)
- Persistent local cache with cleanup policy
- Track model
localCachePath field for cached state
- Pre-flight disk space estimation
- Download without export (for offline listening)