From 5349c947a690ca4e68bd5d5b51f05d83d798b08a Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Fri, 29 May 2026 00:37:40 -0600 Subject: [PATCH] feat(locallibrary): play a track from the Tracks tab (local + offline) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First source-aware playback stage. Clicking a track in the Local Library Tracks tab now plays it, mirroring Tauri's v2_library_play_track routing: - offline-cached (qobuz_download) tracks reuse the shared tier-walk (after_track_change -> play_track_resolved -> offline resolve -> play_data), keyed by the Qobuz id; - local user files are read from disk and handed to the player's play_data seam (which extracts the sample rate + drives the PROTECTED device init, untouched), with a CUE seek for virtual tracks. Additive: play_local_library_track + local_queue_track (mirrors Tauri's local_track_to_queue_track) are new; the shared Qobuz play_audible path is unchanged (no regression risk to streaming). Single-track play (a 1-track queue) — album/queue + auto-advance need a source-aware advancement handler and land in the next stage. Built green (cargo check -p qbz-slint); runtime smoke-test pending (a local FLAC + an offline track should play). cc --- crates/qbz-slint/src/main.rs | 18 ++++- crates/qbz-slint/src/playback.rs | 119 +++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/crates/qbz-slint/src/main.rs b/crates/qbz-slint/src/main.rs index 3a35f3a22..1223c2ed9 100644 --- a/crates/qbz-slint/src/main.rs +++ b/crates/qbz-slint/src/main.rs @@ -4054,11 +4054,25 @@ fn main() -> Result<(), Box> { }); } { + let runtime = app_runtime.clone(); + let weak = window.as_weak(); + let handle = tokio_rt.handle().clone(); window .global::() .on_track_action(move |id, action| { - // TODO(locallibrary): play / queue local tracks (source-aware slice). - log::debug!("[qbz-slint] local track action (playback slice pending): {id} {action}"); + if action.as_str() == "play" { + if let Ok(row_id) = id.parse::() { + playback::play_local_library_track( + runtime.clone(), + weak.clone(), + handle.clone(), + row_id, + ); + } + } else { + // queue / play-next land with the queue-advancement slice. + log::debug!("[qbz-slint] local track action (queue slice pending): {id} {action}"); + } }); } diff --git a/crates/qbz-slint/src/playback.rs b/crates/qbz-slint/src/playback.rs index 870c958b1..92cd569ec 100644 --- a/crates/qbz-slint/src/playback.rs +++ b/crates/qbz-slint/src/playback.rs @@ -145,6 +145,125 @@ async fn play_audible(runtime: &Runtime, weak: &slint::Weak, track_id } } +/// Play a single Local Library track (Tracks-tab rows). Mirrors Tauri's +/// `v2_library_play_track` routing: offline-cached (`qobuz_download`) tracks +/// reuse the shared tier-walk (`after_track_change` -> `play_track_resolved` +/// -> offline resolve -> `play_data`), keyed by the Qobuz id; local user +/// files are read from disk and handed to the player's `play_data` seam +/// (which extracts the sample rate + drives the PROTECTED device init, +/// untouched here). Single-track play (a 1-track queue) — album/queue + +/// auto-advance land with the queue-advancement slice. Additive: the shared +/// Qobuz `play_audible` path is unchanged. +pub fn play_local_library_track( + runtime: Runtime, + weak: slint::Weak, + handle: tokio::runtime::Handle, + row_id: i64, +) { + handle.spawn(async move { + let track = tokio::task::spawn_blocking(move || { + crate::library_db::with_db(|db| db.get_track(row_id)) + }) + .await + .ok() + .flatten() + .flatten(); + let Some(track) = track else { + log::error!("[qbz-slint] local play: track {row_id} not found"); + return; + }; + let is_offline = track.source.as_deref() == Some("qobuz_download"); + runtime + .core() + .set_queue(vec![local_queue_track(&track)], Some(0)) + .await; + + if is_offline { + // Offline copy: reuse the shared tier-walk, keyed by the Qobuz id. + match track.qobuz_track_id { + Some(qid) => after_track_change(&runtime, &weak, qid as u64).await, + None => log::error!( + "[qbz-slint] local play: qobuz_download row {row_id} has no qobuz_track_id" + ), + } + return; + } + + // Local user file: refresh now-playing, then read the file off-thread + // and hand the bytes to the player (no Qobuz client, no network). + refresh_now_playing_meta(&runtime, &weak).await; + record_recent(&runtime).await; + let path = track.file_path.clone(); + let bytes = tokio::task::spawn_blocking(move || std::fs::read(&path)) + .await + .ok() + .and_then(Result::ok); + let Some(bytes) = bytes else { + log::error!("[qbz-slint] local play: failed to read {}", track.file_path); + return; + }; + if let Err(e) = runtime.core().player().play_data(bytes, row_id as u64) { + log::error!("[qbz-slint] local play: play_data {row_id} failed: {e}"); + return; + } + // CUE virtual tracks share one audio file — seek to the track start. + if let Some(start) = track.cue_start_secs { + if start > 0.0 { + tokio::time::sleep(std::time::Duration::from_millis(120)).await; + let _ = runtime.core().player().seek(start as u64); + } + } + }); +} + +/// Build a `QueueTrack` from a local-library row. Mirrors Tauri's +/// `local_track_to_queue_track`: `file://` artwork, kHz sample rate, the real +/// source. Offline copies carry the Qobuz id (so the shared resolver finds +/// them) + `source = "qobuz_download"`; user files carry the library row id + +/// `source = "local"`. +fn local_queue_track(track: &qbz_library::LocalTrack) -> QueueTrack { + let artwork_url = track.artwork_path.as_ref().map(|p| { + if p.starts_with("file://") { + p.clone() + } else { + format!("file://{p}") + } + }); + let sample_rate_khz = if track.sample_rate >= 1000.0 { + track.sample_rate / 1000.0 + } else { + track.sample_rate + }; + let is_offline = track.source.as_deref() == Some("qobuz_download"); + QueueTrack { + id: if is_offline { + track.qobuz_track_id.unwrap_or(track.id) as u64 + } else { + track.id as u64 + }, + title: track.title.clone(), + version: None, + artist: track.artist.clone(), + album: track.album_group_title.clone(), + duration_secs: track.duration_secs, + artwork_url, + hires: track.bit_depth.map(|d| d > 16).unwrap_or(false), + bit_depth: track.bit_depth, + sample_rate: Some(sample_rate_khz), + is_local: true, + album_id: Some(track.album_group_key.clone()), + artist_id: None, + streamable: true, + source: Some(if is_offline { + "qobuz_download".to_string() + } else { + "local".to_string() + }), + parental_warning: false, + source_item_id_hint: None, + } +} + /// Resolve the now-playing cover and apply it to `NowPlayingState`. /// /// Takes a source-aware [`qbz_models::ArtworkRef`] so local-library and Plex