cloud-download-v1.md 4.5 KB

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

  • Exporting a playlist with cloud tracks downloads them and includes them in the session file
  • Download progress is visible (separate from export progress) — e.g., "Downloading 3 cloud tracks... (2/3)"
  • Pre-flight dialog before export: "N cloud tracks need to be downloaded. Continue?"
  • If some downloads fail, export completes with available tracks and reports which ones are missing
  • User can cancel during the download phase
  • Downloaded files are cleaned up after export (temp directory, not persistent cache)
  • Auth headers are included on all download requests (even though file-server is currently unprotected)

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)