feat(locallibrary): play a track from the Tracks tab (local + offline)

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
This commit is contained in:
vicrodh committed 2026-05-29 00:37:40 -06:00
1 parent 56b3ca790b
commit 5349c947a6
2 files changed
+135 -2

No files matched your search

+16 -2
View File
@@ -4054,11 +4054,25 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}); });
} }
{ {
let runtime = app_runtime.clone();
let weak = window.as_weak();
let handle = tokio_rt.handle().clone();
window window
.global::<LocalLibraryActions>() .global::<LocalLibraryActions>()
.on_track_action(move |id, action| { .on_track_action(move |id, action| {
// TODO(locallibrary): play / queue local tracks (source-aware slice). if action.as_str() == "play" {
log::debug!("[qbz-slint] local track action (playback slice pending): {id} {action}"); if let Ok(row_id) = id.parse::<i64>() {
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}");
}
}); });
} }
+119
View File
@@ -145,6 +145,125 @@ async fn play_audible(runtime: &Runtime, weak: &slint::Weak<AppWindow>, 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<AppWindow>,
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`. /// Resolve the now-playing cover and apply it to `NowPlayingState`.
/// ///
/// Takes a source-aware [`qbz_models::ArtworkRef`] so local-library and Plex /// Takes a source-aware [`qbz_models::ArtworkRef`] so local-library and Plex