what's changed
added
- parallel track downloads via
ThreadPoolExecutorwith 4 concurrent workers by default (closes #34) - thread-safe counter +
_failed_tracksmutation viathreading.Lock - filename collision guard: two tracks that sanitize to the same filename get unique paths instead of racing
- 13 new tests covering worker bounds, lock atomicity, generator thread-safety, cancel-before-pool-start, exception isolation, filename collision handling, parallel-mode UI suppression, aggregate progress monotonicity, and proof the pool actually spawns
MAX_WORKERSconcurrent threads
changed
scrape_playlistmaterializes the track generator upfront before threading (generators are not thread-safe in python)- small playlists (under 3 tracks) stay on the single-worker path to preserve single-track UI feel
- per-track UI widgets are suppressed in parallel mode to prevent label flicker and thumbnail thread spam
- progress bar in parallel mode is driven by aggregate completion count; sequential mode still resets per track
- cancel checks at multiple points (before generator materialization, at worker entry, between future completions)
performance
| workers | 12 tracks (wall time) | success rate |
|---|---|---|
| 1 (old) | ~25s | 12/12 |
| 4 (new default) | ~12s | 12/12 |
| 8 | ~8.6s | 12/12 |
~4x speedup on large playlists. measured on Apple Silicon M4 Max (macOS). the concurrency model is pure python + yt-dlp with no platform-specific code, so Windows and Linux builds ship the same parallel downloader. stress-tested at 24 concurrent YouTube searches with zero rate limiting from Spotify or YouTube.
install via homebrew (macos)
brew tap sunnypatell/sunnify https://github.com/sunnypatell/sunnify-spotify-downloader
brew install --cask sunnify
sudo xattr -cr /Applications/Sunnify.appfull changelog: v2.0.3...v2.0.4