what's changed
a reliability patch: when something goes wrong, Sunnify now tells you what actually went wrong, and the checks that catch upstream breakage run on a schedule instead of waiting for someone to file an issue.
fixed
- a failed single-track download no longer reports success. the error reached the UI but was never recorded, so
run_summarysaidfailed: 0and the CLI exited0with no audio on disk. both failure paths count now, which matters most if you script it and trust the exit code. - region-locked, private, and removed links now say so (closes #94). Spotify serves its own error page for those, and for every link in a country it has withdrawn from. you used to get
Could not find entity in embed page. pageProps keys: [...], which told you nothing. the error page and a real content page are now told apart, and the message names the actual reason. - a YouTube network block is now explained once, in words (closes #95). when YouTube asks a network to prove it isn't a bot it applies that to your whole IP, so a 66-track playlist used to produce 66 unexplained failures. you now get one message saying YouTube is gating the network, that it's your connection rather than Sunnify, and what to try.
added
- the upstream probe runs daily now.
scripts/check_api_status.pyhad been sitting in the repo with nothing invoking it, so every upstream change it was written to catch got found by a user instead. a scheduled workflow runs it and fails loudly. - three staleness guards inside it, each covering a path that had already degraded quietly: a real end-to-end download through the app's own code rather than a copy of it, a check that every YouTube retry client is still one yt-dlp ships, and a check that the unavailable-page detection keeps working without false-positiving on healthy links.
- the YouTube retry clients are validated at runtime. YouTube retires client names and yt-dlp follows; a retired name is now dropped with a warning naming it instead of failing the download.
not changed, on purpose
the matcher. a wrong take, or a file that measures lower than the bitrate you picked, is the source's ceiling rather than a fault: YouTube's best audio for a track is around 128 kbps, so a 320 kbps file is a transcode of that. the strict title/artist/duration selector and the opt-in "use closest result if no match" toggle in Settings stay as they are - tightening further loses tracks that currently work.
verifying this release: every asset is covered by SLSA build L3 provenance - gh attestation verify <asset> --repo sunnypatell/sunnify-spotify-downloader, or check checksums.txt with sha256sum -c.
install via homebrew (macos):
brew tap sunnypatell/sunnify https://github.com/sunnypatell/sunnify-spotify-downloader
brew install --cask sunnifyfull changelog: v2.4.0...v2.4.1
⭐ if sunnify saved you time, star the repo - stars are how people find it.
attestations from this build
direct links to each attestation generated by the workflow run that produced these binaries (sigstore-signed, public-good rekor log entry, SLSA build L3 via the release-build.yml reusable workflow):
- build provenance: Sunnify-Windows.exe
- build provenance: Sunnify-Windows-CLI.exe
- build provenance: Sunnify-Linux
- build provenance: Sunnify-macOS.zip
- build provenance: Sunnify-macOS-Intel.zip
- sbom attestation for the binaries
- build provenance for the source tarball
the *.sigstore.json assets attached above are the same attestations as offline-verifiable bundles. these get auto-refreshed on every release re-build, so the links above always correspond to the binaries currently attached to this release.