From 09921132695ded02ea537bb8a508e4ebcbef1278 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Sat, 9 May 2026 01:48:38 +0200 Subject: [PATCH] fix(audio): suppress audio:error toast for superseded plays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rapid skipping while a track is in initial preparation produced a misleading "Couldn't play track — skipping" toast for the abandoned track. Sequence: audio_play(A) reaches build_source_from_play_input; user skips → audio_play(B) bumps gen; A's RangedHttpSource::read sees the gen mismatch and returns Ok(0); Symphonia's probe interprets Ok(0) as EOF → "ranged-stream: format probe failed: end of stream"; audio_play(A)'s map_err emits audio:error unconditionally → toast appears even though playback already moved on. Gate the emit on a generation check: if the global generation has already moved past this play's gen, the failure is supersedion, not a real codec error — log it but do not surface the toast. Pre-existing bug, identical code path on main (commands.rs:600). Noticed on refactor/backend-pilot during cucadmuh's skip-test session because the live-test pattern hit the narrow probe-window race repeatedly. The diagnostic logs added in the previous commit should make the next occurrence's cause visible regardless of whether the toast fires. Frontend impact: - handleAudioError no longer fires on supersedion → no toast, no setState({ isPlaying: false }), no queued next() call. - The audio_play IPC promise still rejects with the same error string (preserved as the .map_err return value); the JS .catch is already generation-guarded in playerStore so this is a no-op there. --- src-tauri/src/audio/commands.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/audio/commands.rs b/src-tauri/src/audio/commands.rs index 2eae3de3..dc813710 100644 --- a/src-tauri/src/audio/commands.rs +++ b/src-tauri/src/audio/commands.rs @@ -240,7 +240,19 @@ pub async fn audio_play( ) .await .map_err(|e| { - app.emit("audio:error", &e).ok(); + // Suppress the audio:error toast when this play was already superseded + // by a newer audio_play (rapid skip): the failure is the inevitable + // Ok(0)/EOF from RangedHttpSource after gen-bump, not a real codec + // problem. The frontend would otherwise show "Couldn't play track" for + // the abandoned URL while a new track is already loading. + if state.generation.load(Ordering::SeqCst) == gen { + app.emit("audio:error", &e).ok(); + } else { + crate::app_deprintln!( + "[audio] suppressed audio:error for superseded play (gen={} cur={}): {}", + gen, state.generation.load(Ordering::SeqCst), e + ); + } e })?; state.current_is_seekable.store(playback_source.is_seekable, Ordering::SeqCst);