diff --git a/crates/qbz-player/src/player/playback_engine.rs b/crates/qbz-player/src/player/playback_engine.rs index cae2a8cd5..8be0b495a 100644 --- a/crates/qbz-player/src/player/playback_engine.rs +++ b/crates/qbz-player/src/player/playback_engine.rs @@ -199,8 +199,15 @@ impl PlaybackEngine { } } - /// Stop - pub fn stop(self) { + /// Stop playback and release resources. + /// For ALSA Direct, signals the writer thread and waits for it to exit. + /// The Drop impl handles the same cleanup if stop() is not called explicitly. + pub fn stop(mut self) { + self.stop_inner(); + } + + /// Internal stop logic shared by stop() and Drop + fn stop_inner(&mut self) { match self { Self::Rodio { sink } => { sink.stop(); @@ -212,11 +219,14 @@ impl PlaybackEngine { playback_thread, .. } => { + if should_stop.load(Ordering::SeqCst) { + return; // Already stopped + } log::info!("[ALSA Direct Engine] Stop requested"); should_stop.store(true, Ordering::SeqCst); is_playing.store(false, Ordering::SeqCst); - if let Some(handle) = playback_thread { + if let Some(handle) = playback_thread.take() { let _ = handle.join(); } @@ -424,3 +434,9 @@ fn alsa_writer_thread( is_playing.store(false, Ordering::SeqCst); log::info!("[ALSA Direct Engine] Writer thread finished"); } + +impl Drop for PlaybackEngine { + fn drop(&mut self) { + self.stop_inner(); + } +} diff --git a/src-tauri/src/commands_v2.rs b/src-tauri/src/commands_v2.rs index 6e77aa149..f81ea081e 100644 --- a/src-tauri/src/commands_v2.rs +++ b/src-tauri/src/commands_v2.rs @@ -322,9 +322,8 @@ async fn download_audio(url: &str) -> Result, String> { let client = reqwest::Client::builder() .connect_timeout(Duration::from_secs(10)) - .no_gzip() - .no_brotli() - .no_deflate() + .timeout(Duration::from_secs(120)) + .http1_only() .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; @@ -332,7 +331,6 @@ async fn download_audio(url: &str) -> Result, String> { let response = client .get(url) - .header("User-Agent", "Mozilla/5.0") .send() .await .map_err(|e| format!("Failed to fetch audio: {}", e))?; @@ -349,7 +347,18 @@ async fn download_audio(url: &str) -> Result, String> { let bytes = response .bytes() .await - .map_err(|e| format!("Failed to read audio bytes: {}", e))?; + .map_err(|e| { + // Log detailed error chain for debugging + use std::error::Error as _; + let mut msg = format!("Failed to read audio bytes: {}", e); + let mut source = e.source(); + while let Some(cause) = source { + msg.push_str(&format!(" | caused by: {}", cause)); + source = cause.source(); + } + log::error!("[V2] Download error details: {}", msg); + msg + })?; log::info!("[V2] Downloaded {} bytes", bytes.len()); Ok(bytes.to_vec()) @@ -371,9 +380,7 @@ async fn v2_get_stream_info(url: &str) -> Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(30)) .connect_timeout(Duration::from_secs(10)) - .no_gzip() - .no_brotli() - .no_deflate() + .http1_only() .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; @@ -463,9 +470,8 @@ async fn v2_download_and_stream( let client = reqwest::Client::builder() .connect_timeout(Duration::from_secs(10)) - .no_gzip() - .no_brotli() - .no_deflate() + .timeout(Duration::from_secs(300)) + .http1_only() .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; @@ -493,7 +499,16 @@ async fn v2_download_and_stream( let mut last_log_time = Instant::now(); while let Some(chunk_result) = stream.next().await { - let chunk = chunk_result.map_err(|e| format!("Stream chunk error: {}", e))?; + let chunk = chunk_result.map_err(|e| { + let mut msg = format!("Stream chunk error: {}", e); + let mut source = std::error::Error::source(&e); + while let Some(cause) = source { + msg.push_str(&format!(" | caused by: {}", cause)); + source = std::error::Error::source(cause); + } + log::error!("[V2/STREAMING] Chunk error details: {}", msg); + msg + })?; bytes_received += chunk.len() as u64; all_data.extend_from_slice(&chunk);