diff --git a/src/infra/queue/dispatch.rs b/src/infra/queue/dispatch.rs index d47b76d7..097e5005 100644 --- a/src/infra/queue/dispatch.rs +++ b/src/infra/queue/dispatch.rs @@ -349,15 +349,17 @@ async fn play_queued_subsonic(app: &Arc>, track: &TrackInfo, uri: &st let fetch_id = publish_pending_decoded(app, &player, track).await; // Fetch off the IoEvent pump: awaiting the download here would freeze every // other event (skips included, for every source) for its whole duration. - let app = Arc::clone(app); + let app_for_spawn = Arc::clone(app); let uri = uri.to_string(); let name = track.name.clone(); - tokio::spawn(async move { + let handle = tokio::spawn(async move { let result = crate::infra::subsonic::dispatch::download_for_queue(&source, &uri) .await .map(|tmp| (tmp, None)); - finish_decoded_fetch(&app, fetch_id, result, &name).await; + finish_decoded_fetch(&app_for_spawn, fetch_id, result, &name).await; }); + + attach_abort_handle(app, fetch_id, handle.abort_handle()).await; true } @@ -373,15 +375,17 @@ async fn play_queued_qobuz(app: &Arc>, track: &TrackInfo, uri: &str) let fetch_id = publish_pending_decoded(app, &player, track).await; let quality = app.lock().await.user_config.behavior.qobuz_quality; // Fetch off the IoEvent pump, like Subsonic: a Qobuz track is a long download. - let app = Arc::clone(app); + let app_for_spawn = Arc::clone(app); let uri = uri.to_string(); let name = track.name.clone(); - tokio::spawn(async move { + let handle = tokio::spawn(async move { let result = crate::infra::qobuz::dispatch::download_for_queue(&source, &uri, quality) .await .map(|(tmp, label)| (tmp, Some(label))); - finish_decoded_fetch(&app, fetch_id, result, &name).await; + finish_decoded_fetch(&app_for_spawn, fetch_id, result, &name).await; }); + + attach_abort_handle(app, fetch_id, handle.abort_handle()).await; true } @@ -399,15 +403,17 @@ async fn play_queued_youtube(app: &Arc>, track: &TrackInfo, uri: &str let source = crate::infra::youtube::dispatch::build_source(app).await; // Fetch off the IoEvent pump: awaiting yt-dlp here would freeze every other // event (skips included, for every source) for its whole duration. - let app = Arc::clone(app); + let app_for_spawn = Arc::clone(app); let uri = uri.to_string(); let name = track.name.clone(); - tokio::spawn(async move { + let handle = tokio::spawn(async move { let result = crate::infra::youtube::dispatch::download_for_queue(&source, &uri) .await .map(|tmp| (tmp, None)); - finish_decoded_fetch(&app, fetch_id, result, &name).await; + finish_decoded_fetch(&app_for_spawn, fetch_id, result, &name).await; }); + + attach_abort_handle(app, fetch_id, handle.abort_handle()).await; true } @@ -599,6 +605,8 @@ async fn publish_pending_decoded( fetch_id, #[cfg(feature = "queue-download")] tempfile: None, + #[cfg(feature = "queue-download")] + abort_handle: None, quality: None, })); fetch_id @@ -691,6 +699,8 @@ async fn publish_decoded( fetch_id: next_fetch_id(), #[cfg(feature = "queue-download")] tempfile, + #[cfg(feature = "queue-download")] + abort_handle: None, quality: None, })); guard.set_status_message(format!("\u{266a} {name} (queue)"), 4); @@ -758,6 +768,22 @@ async fn suspended_context_player(app: &Arc>) -> Option>, + fetch_id: u64, + abort_handle: tokio::task::AbortHandle, +) { + let mut guard = app.lock().await; + if let Some(crate::infra::queue::QueueNowPlaying::Decoded(ref mut d)) = guard.queue_now { + if d.fetch_id == fetch_id { + d.abort_handle = Some(crate::infra::queue::DownloadAbortHandle(abort_handle)); + return; + } + } + abort_handle.abort(); +} + /// Hand the sink to `source` before a decoded queue item takes over: claim it, /// drop a Spotify slot that is being skipped mid-play, then pause librespot and /// its play intent so no rebuild resumes Spotify under the queued track. @@ -1253,6 +1279,7 @@ mod tests { use std::time::SystemTime; #[cfg(any( + feature = "queue-download", feature = "streaming", not(all(feature = "qobuz", feature = "subsonic")) ))] diff --git a/src/infra/queue/mod.rs b/src/infra/queue/mod.rs index a8b327fe..0ba91b7d 100644 --- a/src/infra/queue/mod.rs +++ b/src/infra/queue/mod.rs @@ -418,6 +418,29 @@ pub fn restage( Ok(()) } +/// Aborts a background queue download when dropped. +#[cfg(feature = "queue-download")] +pub struct DownloadAbortHandle(pub tokio::task::AbortHandle); + +#[cfg(feature = "queue-download")] +impl Drop for DownloadAbortHandle { + fn drop(&mut self) { + self.0.abort(); + } +} + +#[cfg(all(test, feature = "queue-download"))] +#[tokio::test] +async fn dropping_the_abort_handle_cancels_the_download_task() { + let handle = tokio::spawn(async { + tokio::time::sleep(std::time::Duration::from_secs(3600)).await; + }); + let abort_handle = DownloadAbortHandle(handle.abort_handle()); + drop(abort_handle); + let res = handle.await; + assert!(res.unwrap_err().is_cancelled()); +} + /// A queued *decoded* track playing through the shared [`LocalPlayer`] sink /// (local file, Subsonic, or YouTube). Kept separate from the per-source /// `*_playback` structs so the underlying context is preserved for resume. @@ -452,6 +475,11 @@ pub struct DecodedQueuePlayback { #[cfg(feature = "queue-download")] #[allow(dead_code)] pub tempfile: Option, + /// The handle to abort the background download task if the slot is cleared + /// or replaced before the download completes. + #[cfg(feature = "queue-download")] + #[allow(dead_code)] + pub abort_handle: Option, /// The delivered audio format of a downloaded track (Qobuz, e.g. /// `FLAC 24/96`), shown after the artists in the playbar. #[cfg_attr(not(feature = "tui"), allow(dead_code))] diff --git a/tools/gates.count b/tools/gates.count index 9ef489a2..28e9d66f 100644 --- a/tools/gates.count +++ b/tools/gates.count @@ -15,4 +15,4 @@ view_writes_outside_tui = 12 # target 0 (producers outside tui/ and co pub_fields_on_app = 117 # target 1 (App.view stays public for the frontend; the rest go through App methods) direct_playback_context_reads = 88 # target 0 (readers of App::current_playback_context outside the ownership resolver and the snapshot builder, which are excluded) action_refs_in_tui_handlers = 189 # adoption: may only rise -test_attribute_total = 2286 # adoption: may only rise +test_attribute_total = 2287 # adoption: may only rise