Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [v0.39.1] 2026-06-12

### Fixed

- **Self-update silently failing since v0.38.5**: Release-asset downloads during update checksum verification now send a `User-Agent` header (GitHub rejects requests without one with `403 Forbidden`) and `Accept: application/octet-stream` (without it the asset API URL returns JSON metadata instead of the file), so auto-update and `spotatui update --install` work again. Auto-update failures are now logged instead of silently discarded, and `spotatui update` runs on a blocking thread so it can no longer panic the async runtime. Clients on v0.38.5 through v0.39.0 carry the broken verification in their own binaries and need one manual update to reach this release ([#303](https://github.com/LargeModGames/spotatui/pull/303)).

## [v0.39.0] 2026-06-12

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ documentation = "https://github.com/LargeModGames/spotatui"
repository = "https://github.com/LargeModGames/spotatui"
keywords = ["spotify", "tui", "cli", "terminal"]
categories = ["command-line-utilities"]
version = "0.39.0"
version = "0.39.1"
authors = ["LargeModGames <LargeModGames@gmail.com>"]
edition = "2021"
license = "MIT"
Expand Down
9 changes: 9 additions & 0 deletions announcements.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"version": 1,
"announcements": [
{
"id": "2026-06-12-self-update-broken",
"title": "Action needed: auto-update is broken in v0.38.5 to v0.39.0",
"body": "A bug in update checksum verification means spotatui v0.38.5 through v0.39.0 cannot update themselves: the update check fails silently on every launch. The fix ships in v0.39.1, but affected versions cannot reach it on their own. Please update once manually (winget upgrade, brew upgrade, your package manager, or the GitHub releases page). Auto-update works again from v0.39.1 onward.",
"level": "warning",
"url": "https://github.com/LargeModGames/spotatui/pull/303",
"starts_at": "2026-06-12T00:00:00Z",
"ends_at": null
},
{
"id": "2026-06-12-lua-scripting",
"title": "New: Lua plugin scripting",
Expand Down
5 changes: 5 additions & 0 deletions src/cli/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,16 @@ fn verify_release_checksum(release: &self_update::update::Release) -> Result<()>
)
})?;

// GitHub's API rejects requests without a User-Agent with 403, and asset URLs
// return JSON metadata instead of the file unless Accept is application/octet-stream.
let client = reqwest::blocking::Client::builder()
.user_agent(concat!("spotatui/", env!("CARGO_PKG_VERSION")))
.timeout(std::time::Duration::from_secs(60))
.build()?;

let checksum_text = client
.get(&checksum_asset.download_url)
.header(reqwest::header::ACCEPT, "application/octet-stream")
.send()?
.error_for_status()?
.text()?;
Expand All @@ -138,6 +142,7 @@ fn verify_release_checksum(release: &self_update::update::Release) -> Result<()>

let binary_bytes = client
.get(&asset.download_url)
.header(reqwest::header::ACCEPT, "application/octet-stream")
.send()?
.error_for_status()?
.bytes()?;
Expand Down
26 changes: 18 additions & 8 deletions src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -482,18 +482,20 @@ fn add_self_update_cli(clap_app: ClapApp) -> ClapApp {
}

#[cfg(feature = "self-update")]
fn handle_self_update_command(matches: &ArgMatches) -> Result<bool> {
async fn handle_self_update_command(matches: &ArgMatches) -> Result<bool> {
if let Some(update_matches) = matches.subcommand_matches("update") {
let do_install = update_matches.get_flag("install");
cli::check_for_update(do_install)?;
// Must use spawn_blocking because self_update uses reqwest::blocking internally,
// which creates its own tokio runtime and panics if called from an async context.
tokio::task::spawn_blocking(move || cli::check_for_update(do_install)).await??;
return Ok(true);
}

Ok(false)
}

#[cfg(not(feature = "self-update"))]
fn handle_self_update_command(_matches: &ArgMatches) -> Result<bool> {
async fn handle_self_update_command(_matches: &ArgMatches) -> Result<bool> {
Ok(false)
}

Expand All @@ -513,10 +515,18 @@ async fn run_auto_update(matches: &ArgMatches, user_config: &UserConfig) {
let delay_secs =
crate::core::user_config::parse_update_delay_secs(&user_config.behavior.auto_update_delay)
.unwrap_or(0);
let update_result = tokio::task::spawn_blocking(move || cli::install_update_silent(delay_secs))
.await
.ok()
.and_then(|r| r.ok());
let update_result =
match tokio::task::spawn_blocking(move || cli::install_update_silent(delay_secs)).await {
Ok(Ok(outcome)) => Some(outcome),
Ok(Err(e)) => {
log::warn!("auto-update failed: {:#}", e);
None
}
Err(e) => {
log::warn!("auto-update task panicked: {}", e);
None
}
};

match update_result {
Some(cli::UpdateOutcome::Installed(new_version)) => {
Expand Down Expand Up @@ -633,7 +643,7 @@ screens more often and cost more CPU. Animation-heavy views keep their separate
}

// Handle self-update command (doesn't need Spotify auth)
if handle_self_update_command(&matches)? {
if handle_self_update_command(&matches).await? {
return Ok(());
}

Expand Down
Loading