perf(slint): server-transcoded Plex thumbs + shared HTTP client for images
Plex cover images were fetched full-res on every surface (Slint built the raw
/library/.../thumb URL), where Tauri requests a server-transcoded thumb sized to
the render target. On a real library this made the views feel heavy and starved
the LAN bandwidth that track playback needs.
- qbz-models: ArtworkRef::PlexThumb gains a size: Option<u32>; new plex_thumb_url()
emits the Tauri-format transcode URL when Some
(/photo/:/transcode?url={enc}&width=N&height=N&minSize=1&X-Plex-Token=), raw
tokenized path when None (album/MPRIS full-res preserved). urlencoding dep added
to match encodeURIComponent. Tests extended.
- qbz-slint: each PlexThumb construction site passes its surface decode size
(grid/detail 264, queue/now-playing-item 96, now-playing bar 160, MPRIS full-res);
fetch_and_decode_ref uses plex_thumb_url.
- artwork.rs: static LazyLock<reqwest::Client> reused in fetch_cached_http instead
of reqwest::get (per-image client) — keep-alive + no fd churn (EMFILE smell).
Audio path untouched; local/Qobuz artwork unchanged.
cc
This commit is contained in:
7 files changed
+140
-16
No files matched your search
Generated
+1
@@ -5337,6 +5337,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"url",
|
"url",
|
||||||
|
"urlencoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -24,3 +24,7 @@ chrono = { workspace = true }
|
|||||||
|
|
||||||
# Correct file:// URI building for media-controls art (ArtworkRef::to_mpris_url)
|
# Correct file:// URI building for media-controls art (ArtworkRef::to_mpris_url)
|
||||||
url = "2"
|
url = "2"
|
||||||
|
# Percent-encoding the Plex transcode `url=`/token params (plex_thumb_url),
|
||||||
|
# matching the Tauri frontend's buildPlexArtworkUrl. Same crate used across
|
||||||
|
# qbz-slint / qbz-media-controls / qbz-integrations.
|
||||||
|
urlencoding = "2"
|
||||||
@@ -41,7 +41,7 @@ pub mod types;
|
|||||||
pub use error::{QbzError, QbzResult};
|
pub use error::{QbzError, QbzResult};
|
||||||
pub use events::CoreEvent;
|
pub use events::CoreEvent;
|
||||||
pub use playback::{PlaybackState, PlaybackStatus, QueueState, QueueTrack, RepeatMode};
|
pub use playback::{PlaybackState, PlaybackStatus, QueueState, QueueTrack, RepeatMode};
|
||||||
pub use source::{ArtworkRef, PlaybackSource, TrackOriginTag};
|
pub use source::{plex_thumb_url, ArtworkRef, PlaybackSource, TrackOriginTag};
|
||||||
pub use traits::{FrontendAdapter, LoggingAdapter, NoOpAdapter};
|
pub use traits::{FrontendAdapter, LoggingAdapter, NoOpAdapter};
|
||||||
pub use types::{
|
pub use types::{
|
||||||
Album,
|
Album,
|
||||||
|
|||||||
+102
-10
@@ -122,10 +122,16 @@ pub enum ArtworkRef {
|
|||||||
LocalFile(String),
|
LocalFile(String),
|
||||||
/// A Plex thumbnail fetched with an auth token appended. `path` is the
|
/// A Plex thumbnail fetched with an auth token appended. `path` is the
|
||||||
/// server-relative thumb path (e.g. `/library/metadata/42/thumb/1`).
|
/// server-relative thumb path (e.g. `/library/metadata/42/thumb/1`).
|
||||||
|
///
|
||||||
|
/// `size` selects the URL form (see [`plex_thumb_url`]): `Some(n)` builds
|
||||||
|
/// a server-side transcode URL downscaled to `n`×`n` (so we download what
|
||||||
|
/// we render, not the full-res original); `None` keeps the raw full-res
|
||||||
|
/// tokenized path (album-detail full covers / fallbacks).
|
||||||
PlexThumb {
|
PlexThumb {
|
||||||
base_url: String,
|
base_url: String,
|
||||||
token: String,
|
token: String,
|
||||||
path: String,
|
path: String,
|
||||||
|
size: Option<u32>,
|
||||||
},
|
},
|
||||||
/// Cover bytes already in memory (e.g. embedded tags).
|
/// Cover bytes already in memory (e.g. embedded tags).
|
||||||
Embedded(Vec<u8>),
|
Embedded(Vec<u8>),
|
||||||
@@ -168,15 +174,39 @@ impl ArtworkRef {
|
|||||||
base_url,
|
base_url,
|
||||||
token,
|
token,
|
||||||
path,
|
path,
|
||||||
} if !path.is_empty() => {
|
size,
|
||||||
let sep = if path.contains('?') { '&' } else { '?' };
|
} if !path.is_empty() => Some(plex_thumb_url(base_url, token, path, *size)),
|
||||||
Some(format!("{base_url}{path}{sep}X-Plex-Token={token}"))
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the fetchable Plex artwork URL.
|
||||||
|
///
|
||||||
|
/// `Some(size)` → a server-side **transcode** URL downscaled to `size`×`size`
|
||||||
|
/// (so the client downloads roughly what it renders instead of the full-res
|
||||||
|
/// original). `None` (or `Some(0)`) → the raw full-res tokenized path.
|
||||||
|
///
|
||||||
|
/// Mirrors the Tauri frontend's `buildPlexArtworkUrl` exactly: the base is
|
||||||
|
/// trailing-slash-stripped, the inner `url=` (the original `/library` path)
|
||||||
|
/// and the token are percent-encoded, and `minSize=1` is appended. The raw
|
||||||
|
/// fallback keeps the non-percent-encoded token to match the historical
|
||||||
|
/// behavior (and the existing regression tests).
|
||||||
|
pub fn plex_thumb_url(base_url: &str, token: &str, path: &str, size: Option<u32>) -> String {
|
||||||
|
let base = base_url.trim_end_matches('/');
|
||||||
|
match size {
|
||||||
|
Some(n) if n > 0 => format!(
|
||||||
|
"{base}/photo/:/transcode?url={}&width={n}&height={n}&minSize=1&X-Plex-Token={}",
|
||||||
|
urlencoding::encode(path),
|
||||||
|
urlencoding::encode(token),
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
let sep = if path.contains('?') { '&' } else { '?' };
|
||||||
|
format!("{base}{path}{sep}X-Plex-Token={token}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl QueueTrack {
|
impl QueueTrack {
|
||||||
/// The track's playback source, parsed from its `source` field.
|
/// The track's playback source, parsed from its `source` field.
|
||||||
pub fn source_kind(&self) -> PlaybackSource {
|
pub fn source_kind(&self) -> PlaybackSource {
|
||||||
@@ -217,7 +247,17 @@ impl QueueTrack {
|
|||||||
/// `{base_url, token}` (from the Slint Plex settings store) and threads them
|
/// `{base_url, token}` (from the Slint Plex settings store) and threads them
|
||||||
/// in. When creds are missing, or the path is not a Plex thumb path, this
|
/// in. When creds are missing, or the path is not a Plex thumb path, this
|
||||||
/// falls back to [`artwork_ref`](Self::artwork_ref).
|
/// falls back to [`artwork_ref`](Self::artwork_ref).
|
||||||
pub fn artwork_ref_with_plex(&self, base_url: &str, token: &str) -> ArtworkRef {
|
///
|
||||||
|
/// `size` is threaded into the resulting [`ArtworkRef::PlexThumb`] so the
|
||||||
|
/// caller picks the render-target size: `Some(n)` → server-side transcode
|
||||||
|
/// at `n`×`n`; `None` → raw full-res (e.g. for MPRIS art that wants a
|
||||||
|
/// larger image). See [`plex_thumb_url`].
|
||||||
|
pub fn artwork_ref_with_plex(
|
||||||
|
&self,
|
||||||
|
base_url: &str,
|
||||||
|
token: &str,
|
||||||
|
size: Option<u32>,
|
||||||
|
) -> ArtworkRef {
|
||||||
let raw = self.artwork_url.as_deref().unwrap_or("");
|
let raw = self.artwork_url.as_deref().unwrap_or("");
|
||||||
let is_plex_path = raw.starts_with("/library/") || raw.starts_with("/photo/");
|
let is_plex_path = raw.starts_with("/library/") || raw.starts_with("/photo/");
|
||||||
if is_plex_path && !base_url.is_empty() && !token.is_empty() {
|
if is_plex_path && !base_url.is_empty() && !token.is_empty() {
|
||||||
@@ -225,6 +265,7 @@ impl QueueTrack {
|
|||||||
base_url: base_url.to_string(),
|
base_url: base_url.to_string(),
|
||||||
token: token.to_string(),
|
token: token.to_string(),
|
||||||
path: raw.to_string(),
|
path: raw.to_string(),
|
||||||
|
size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
self.artwork_ref()
|
self.artwork_ref()
|
||||||
@@ -334,21 +375,34 @@ mod tests {
|
|||||||
// Plex thumb path + creds → PlexThumb (the now-playing/queue/MPRIS path).
|
// Plex thumb path + creds → PlexThumb (the now-playing/queue/MPRIS path).
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
track_with(Some("plex"), Some("/library/metadata/42/thumb/1"))
|
track_with(Some("plex"), Some("/library/metadata/42/thumb/1"))
|
||||||
.artwork_ref_with_plex("http://plex.local:32400", "tok"),
|
.artwork_ref_with_plex("http://plex.local:32400", "tok", None),
|
||||||
ArtworkRef::PlexThumb {
|
ArtworkRef::PlexThumb {
|
||||||
base_url: "http://plex.local:32400".into(),
|
base_url: "http://plex.local:32400".into(),
|
||||||
token: "tok".into(),
|
token: "tok".into(),
|
||||||
path: "/library/metadata/42/thumb/1".into(),
|
path: "/library/metadata/42/thumb/1".into(),
|
||||||
|
size: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// `/photo/...` transcode paths classify the same way.
|
// `/photo/...` transcode paths classify the same way.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
track_with(Some("plex"), Some("/photo/:/transcode?url=x"))
|
track_with(Some("plex"), Some("/photo/:/transcode?url=x"))
|
||||||
.artwork_ref_with_plex("http://plex.local:32400", "tok"),
|
.artwork_ref_with_plex("http://plex.local:32400", "tok", None),
|
||||||
ArtworkRef::PlexThumb {
|
ArtworkRef::PlexThumb {
|
||||||
base_url: "http://plex.local:32400".into(),
|
base_url: "http://plex.local:32400".into(),
|
||||||
token: "tok".into(),
|
token: "tok".into(),
|
||||||
path: "/photo/:/transcode?url=x".into(),
|
path: "/photo/:/transcode?url=x".into(),
|
||||||
|
size: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// A size is threaded straight into the ref (URL form decided later).
|
||||||
|
assert_eq!(
|
||||||
|
track_with(Some("plex"), Some("/library/metadata/42/thumb/1"))
|
||||||
|
.artwork_ref_with_plex("http://plex.local:32400", "tok", Some(264)),
|
||||||
|
ArtworkRef::PlexThumb {
|
||||||
|
base_url: "http://plex.local:32400".into(),
|
||||||
|
token: "tok".into(),
|
||||||
|
path: "/library/metadata/42/thumb/1".into(),
|
||||||
|
size: Some(264),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -358,29 +412,31 @@ mod tests {
|
|||||||
// Missing creds → no PlexThumb; falls back to LocalFile (the raw path).
|
// Missing creds → no PlexThumb; falls back to LocalFile (the raw path).
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
track_with(Some("plex"), Some("/library/metadata/42/thumb/1"))
|
track_with(Some("plex"), Some("/library/metadata/42/thumb/1"))
|
||||||
.artwork_ref_with_plex("", ""),
|
.artwork_ref_with_plex("", "", Some(264)),
|
||||||
ArtworkRef::LocalFile("/library/metadata/42/thumb/1".into())
|
ArtworkRef::LocalFile("/library/metadata/42/thumb/1".into())
|
||||||
);
|
);
|
||||||
// Non-Plex value is untouched: http stays Remote even with creds.
|
// Non-Plex value is untouched: http stays Remote even with creds.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
track_with(Some("qobuz"), Some("https://x/cover.jpg"))
|
track_with(Some("qobuz"), Some("https://x/cover.jpg"))
|
||||||
.artwork_ref_with_plex("http://plex.local:32400", "tok"),
|
.artwork_ref_with_plex("http://plex.local:32400", "tok", Some(264)),
|
||||||
ArtworkRef::Remote("https://x/cover.jpg".into())
|
ArtworkRef::Remote("https://x/cover.jpg".into())
|
||||||
);
|
);
|
||||||
// A local file:// path stays LocalFile, never mistaken for Plex.
|
// A local file:// path stays LocalFile, never mistaken for Plex.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
track_with(Some("local"), Some("file:///home/u/cover.jpg"))
|
track_with(Some("local"), Some("file:///home/u/cover.jpg"))
|
||||||
.artwork_ref_with_plex("http://plex.local:32400", "tok"),
|
.artwork_ref_with_plex("http://plex.local:32400", "tok", Some(264)),
|
||||||
ArtworkRef::LocalFile("/home/u/cover.jpg".into())
|
ArtworkRef::LocalFile("/home/u/cover.jpg".into())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn plex_thumb_to_mpris_url_tokenizes() {
|
fn plex_thumb_to_mpris_url_tokenizes() {
|
||||||
|
// size: None → raw full-res tokenized path (historical behavior).
|
||||||
let no_query = ArtworkRef::PlexThumb {
|
let no_query = ArtworkRef::PlexThumb {
|
||||||
base_url: "http://plex.local:32400".into(),
|
base_url: "http://plex.local:32400".into(),
|
||||||
token: "tok".into(),
|
token: "tok".into(),
|
||||||
path: "/library/metadata/42/thumb/1".into(),
|
path: "/library/metadata/42/thumb/1".into(),
|
||||||
|
size: None,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
no_query.to_mpris_url().as_deref(),
|
no_query.to_mpris_url().as_deref(),
|
||||||
@@ -390,10 +446,46 @@ mod tests {
|
|||||||
base_url: "http://plex.local:32400".into(),
|
base_url: "http://plex.local:32400".into(),
|
||||||
token: "tok".into(),
|
token: "tok".into(),
|
||||||
path: "/photo/:/transcode?url=x".into(),
|
path: "/photo/:/transcode?url=x".into(),
|
||||||
|
size: None,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
with_query.to_mpris_url().as_deref(),
|
with_query.to_mpris_url().as_deref(),
|
||||||
Some("http://plex.local:32400/photo/:/transcode?url=x&X-Plex-Token=tok")
|
Some("http://plex.local:32400/photo/:/transcode?url=x&X-Plex-Token=tok")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plex_thumb_url_size_emits_transcode_form() {
|
||||||
|
// Some(size) → server-side transcode URL, percent-encoded url + token,
|
||||||
|
// trailing-slash-stripped base, minSize=1 (1:1 with Tauri's
|
||||||
|
// buildPlexArtworkUrl). The `/` in the path is encoded to %2F.
|
||||||
|
assert_eq!(
|
||||||
|
plex_thumb_url(
|
||||||
|
"http://plex.local:32400",
|
||||||
|
"tok",
|
||||||
|
"/library/metadata/42/thumb/1",
|
||||||
|
Some(264),
|
||||||
|
),
|
||||||
|
"http://plex.local:32400/photo/:/transcode?url=%2Flibrary%2Fmetadata%2F42%2Fthumb%2F1&width=264&height=264&minSize=1&X-Plex-Token=tok"
|
||||||
|
);
|
||||||
|
// Trailing slashes on the base are stripped before composing.
|
||||||
|
assert_eq!(
|
||||||
|
plex_thumb_url(
|
||||||
|
"http://plex.local:32400/",
|
||||||
|
"t/k",
|
||||||
|
"/library/metadata/7/thumb/9",
|
||||||
|
Some(96),
|
||||||
|
),
|
||||||
|
"http://plex.local:32400/photo/:/transcode?url=%2Flibrary%2Fmetadata%2F7%2Fthumb%2F9&width=96&height=96&minSize=1&X-Plex-Token=t%2Fk"
|
||||||
|
);
|
||||||
|
// None / Some(0) → raw tokenized path (no transcode).
|
||||||
|
assert_eq!(
|
||||||
|
plex_thumb_url("http://plex.local:32400", "tok", "/library/metadata/42/thumb/1", None),
|
||||||
|
"http://plex.local:32400/library/metadata/42/thumb/1?X-Plex-Token=tok"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
plex_thumb_url("http://plex.local:32400", "tok", "/library/metadata/42/thumb/1", Some(0)),
|
||||||
|
"http://plex.local:32400/library/metadata/42/thumb/1?X-Plex-Token=tok"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -186,6 +186,16 @@ pub fn open_cache() -> ImageCache {
|
|||||||
/// resolve cover art. Set once at startup.
|
/// resolve cover art. Set once at startup.
|
||||||
static SHARED_CACHE: std::sync::OnceLock<ImageCache> = std::sync::OnceLock::new();
|
static SHARED_CACHE: std::sync::OnceLock<ImageCache> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
/// Process-wide async HTTP client for artwork downloads. A single client pools
|
||||||
|
/// connections / reuses keep-alive across all concurrent artwork jobs
|
||||||
|
/// (`MAX_CONCURRENT` = 16), instead of `reqwest::get` building a fresh client +
|
||||||
|
/// connection pool on every cache miss (the fd-churn / deferred EMFILE risk).
|
||||||
|
/// rustls per the workspace default; if the builder fails, fall back to the
|
||||||
|
/// default client (equivalent to `Client::new()`), keeping this infallible —
|
||||||
|
/// matching the silent-fallback style of `open_cache`.
|
||||||
|
static HTTP: std::sync::LazyLock<reqwest::Client> =
|
||||||
|
std::sync::LazyLock::new(|| reqwest::Client::builder().build().unwrap_or_default());
|
||||||
|
|
||||||
/// Publish the image cache for `shared_cache()` consumers. Call once.
|
/// Publish the image cache for `shared_cache()` consumers. Call once.
|
||||||
pub fn set_shared_cache(cache: ImageCache) {
|
pub fn set_shared_cache(cache: ImageCache) {
|
||||||
let _ = SHARED_CACHE.set(cache);
|
let _ = SHARED_CACHE.set(cache);
|
||||||
@@ -306,6 +316,10 @@ pub fn spawn_local_or_plex_loads(
|
|||||||
base_url: (*plex_base_url).clone(),
|
base_url: (*plex_base_url).clone(),
|
||||||
token: (*plex_token).clone(),
|
token: (*plex_token).clone(),
|
||||||
path: job.url.clone(),
|
path: job.url.clone(),
|
||||||
|
// Request a server-side transcode at the surface's decode
|
||||||
|
// size (grid cards 264, row thumbs 96, …) instead of the
|
||||||
|
// full-res original.
|
||||||
|
size: Some(decode_size),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ArtworkRef::LocalFile(job.url.clone())
|
ArtworkRef::LocalFile(job.url.clone())
|
||||||
@@ -331,7 +345,7 @@ async fn fetch_cached_http(url: &str, cache: &ImageCache) -> Option<Vec<u8>> {
|
|||||||
match cached_path {
|
match cached_path {
|
||||||
Some(path) => tokio::fs::read(&path).await.ok(),
|
Some(path) => tokio::fs::read(&path).await.ok(),
|
||||||
None => {
|
None => {
|
||||||
let downloaded = reqwest::get(url).await.ok()?.bytes().await.ok()?.to_vec();
|
let downloaded = HTTP.get(url).send().await.ok()?.bytes().await.ok()?.to_vec();
|
||||||
if let Ok(guard) = cache.lock() {
|
if let Ok(guard) = cache.lock() {
|
||||||
if let Some(service) = guard.as_ref() {
|
if let Some(service) = guard.as_ref() {
|
||||||
let _ = service.store(url, &downloaded);
|
let _ = service.store(url, &downloaded);
|
||||||
@@ -375,9 +389,12 @@ pub async fn fetch_and_decode_ref(
|
|||||||
base_url,
|
base_url,
|
||||||
token,
|
token,
|
||||||
path,
|
path,
|
||||||
|
size,
|
||||||
} => {
|
} => {
|
||||||
let sep = if path.contains('?') { '&' } else { '?' };
|
// Shared builder: `Some(size)` → server-side transcode (downscaled),
|
||||||
let url = format!("{base_url}{path}{sep}X-Plex-Token={token}");
|
// `None` → raw full-res. The cache key is this final URL, so each
|
||||||
|
// surface's transcode size caches independently.
|
||||||
|
let url = qbz_models::plex_thumb_url(base_url, token, path, *size);
|
||||||
fetch_cached_http(&url, cache).await?
|
fetch_cached_http(&url, cache).await?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -818,7 +818,14 @@ pub(crate) async fn refresh_now_playing_meta(runtime: &Runtime, weak: &slint::We
|
|||||||
// get the fetchable cover. For non-Plex tracks this is identical to
|
// get the fetchable cover. For non-Plex tracks this is identical to
|
||||||
// `artwork_ref()` (it falls back cleanly).
|
// `artwork_ref()` (it falls back cleanly).
|
||||||
let plex = crate::plex_settings::get();
|
let plex = crate::plex_settings::get();
|
||||||
let artwork = track.artwork_ref_with_plex(&plex.base_url, &plex.token);
|
// Two refs from the same track: MPRIS / desktop-notification art wants a
|
||||||
|
// larger image, so it gets the raw full-res Plex path (`size: None`). The
|
||||||
|
// now-playing bar renders small and decodes to 160 (see
|
||||||
|
// `load_now_playing_artwork`), so it requests a 160px server-side
|
||||||
|
// transcode — downloading ~what it renders instead of the full original.
|
||||||
|
// For non-Plex tracks both collapse to the same `artwork_ref()`.
|
||||||
|
let artwork = track.artwork_ref_with_plex(&plex.base_url, &plex.token, None);
|
||||||
|
let bar_artwork = track.artwork_ref_with_plex(&plex.base_url, &plex.token, Some(160));
|
||||||
// Quality badge: tier from bit depth (24-bit+ = Hi-Res), exact detail line
|
// Quality badge: tier from bit depth (24-bit+ = Hi-Res), exact detail line
|
||||||
// reused from the shared formatter so it matches the track-row badges.
|
// reused from the shared formatter so it matches the track-row badges.
|
||||||
let quality_tier = match track.bit_depth {
|
let quality_tier = match track.bit_depth {
|
||||||
@@ -902,7 +909,7 @@ pub(crate) async fn refresh_now_playing_meta(runtime: &Runtime, weak: &slint::We
|
|||||||
np.set_artwork(slint::Image::default());
|
np.set_artwork(slint::Image::default());
|
||||||
});
|
});
|
||||||
|
|
||||||
load_now_playing_artwork(weak.clone(), artwork);
|
load_now_playing_artwork(weak.clone(), bar_artwork);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record the playback CONTEXT — the source the queue was launched from — on
|
/// Record the playback CONTEXT — the source the queue was launched from — on
|
||||||
|
|||||||
@@ -788,6 +788,9 @@ fn load_artwork(
|
|||||||
base_url: plex_base_url.clone(),
|
base_url: plex_base_url.clone(),
|
||||||
token: plex_token.clone(),
|
token: plex_token.clone(),
|
||||||
path: url,
|
path: url,
|
||||||
|
// Queue rows + now-playing item render small; request a
|
||||||
|
// 96px server-side transcode (the decode size used below).
|
||||||
|
size: Some(96),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
qbz_models::ArtworkRef::LocalFile(url)
|
qbz_models::ArtworkRef::LocalFile(url)
|
||||||
|
|||||||
Reference in new issue
Block a user