feat(slint): Plex track playback (port slice 5)

Plex tracks produced no audio: local_queue_track had no "plex" arm so they were
enqueued as source="local", and there was no Plex resolve+play path.

- local_queue_track tags source="plex" and carries the string rating_key in
  source_item_id_hint (the numeric queue id is a hashed/parsed form; the resolve
  needs the original key); persisted in the session queue store.
- play_audible gains a "plex" arm -> play_plex_audible.
- play_plex_audible: qbz_plex::plex_resolve_track_media(base, token, rating_key)
  -> player.play_data(bytes, id), mirroring the local-file seam. Bit-perfect:
  play_data drives device init from the DECODED bytes; Plex metadata is
  display-only. Covers album play, single track, and auto-advance.

cc
This commit is contained in:
vicrodh committed 2026-06-07 22:39:19 -06:00
1 parent a45840d178
commit 06b2bea932
1 file changed
+61 -6
+61 -6
View File
@@ -151,11 +151,25 @@ async fn play_audible(runtime: &Runtime, weak: &slint::Weak<AppWindow>, track_id
// current track and `track_id` momentarily disagree. Auto-advance, skip and
// play-all all flow through here, so they become source-aware for free.
if let Some(qt) = runtime.core().current_track().await {
if qt.id == track_id
&& matches!(qt.source.as_deref(), Some("local") | Some("ephemeral"))
{
play_local_file_audible(runtime, track_id).await;
return;
if qt.id == track_id {
match qt.source.as_deref() {
Some("local") | Some("ephemeral") => {
play_local_file_audible(runtime, track_id).await;
return;
}
Some("plex") => {
// The string rating_key rides in `source_item_id_hint`;
// fall back to the numeric id (= rating_key for the common
// numeric-key case) if the hint is absent.
let rating_key = qt
.source_item_id_hint
.clone()
.unwrap_or_else(|| track_id.to_string());
play_plex_audible(runtime, track_id, rating_key).await;
return;
}
_ => {}
}
}
}
// Offline-cached copy (preferred, decrypted to FLAC + played via play_data)
@@ -242,6 +256,38 @@ async fn play_local_file_audible(runtime: &Runtime, row_id: u64) {
}
}
/// Audible step for a Plex track: resolve the original part bytes from the
/// user's LAN Plex server and hand them to the player's `play_data` seam —
/// exactly like the local-file path, but the bytes come from the network
/// instead of disk. `play_data` extracts the sample rate and drives the
/// PROTECTED device init from the DECODED bytes (bit-perfect), so the Plex
/// `sampling_rate_hz`/`bit_depth` are display-only and never touched here.
/// `play_id` is the queue id (numeric rating key); `rating_key` is the string
/// key the resolve needs.
async fn play_plex_audible(runtime: &Runtime, play_id: u64, rating_key: String) {
let cfg = crate::plex_settings::get();
if cfg.base_url.is_empty() || cfg.token.is_empty() {
log::error!("[qbz-slint] plex play: no Plex credentials configured");
return;
}
let resolved = match qbz_plex::plex_resolve_track_media(
cfg.base_url,
cfg.token,
rating_key.clone(),
)
.await
{
Ok(r) => r,
Err(e) => {
log::error!("[qbz-slint] plex play: resolve {rating_key} failed: {e}");
return;
}
};
if let Err(e) = runtime.core().player().play_data(resolved.bytes, play_id) {
log::error!("[qbz-slint] plex play: play_data {play_id} failed: {e}");
}
}
/// Set a Local Library queue and start playback at `start`. Source-aware
/// `play_audible` routes each track (local file vs offline vs Qobuz) and
/// auto-advance flows through the same path, so a mixed-source album/list
@@ -600,9 +646,11 @@ fn local_queue_track(track: &qbz_library::LocalTrack) -> QueueTrack {
let src = match track.source.as_deref() {
Some("qobuz_download") => "qobuz_download",
Some("ephemeral") => "ephemeral",
Some("plex") => "plex",
_ => "local",
};
let is_offline = src == "qobuz_download";
let is_plex = src == "plex";
QueueTrack {
id: if is_offline {
track.qobuz_track_id.unwrap_or(track.id) as u64
@@ -624,7 +672,14 @@ fn local_queue_track(track: &qbz_library::LocalTrack) -> QueueTrack {
streamable: true,
source: Some(src.to_string()),
parental_warning: false,
source_item_id_hint: None,
// For Plex, carry the string rating_key (the numeric queue `id` is a
// hashed/parsed form; the resolve needs the original key). Persisted in
// the session queue store so playback survives a restart.
source_item_id_hint: if is_plex {
Some(track.file_path.clone())
} else {
None
},
}
}