From 3c87df308badb53c4091b8b1e1b4629dc625f6af Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:06:59 -0600 Subject: [PATCH] fix(media-controls): decode notification artwork by content, not extension image::open resolves the format from the path extension only (no content sniffing in image 0.25), so every notification artwork served from the shared image cache - files named {md5}.img since slice 3b/B11 - failed as Unsupported and the toast fired icon-less on online Qobuz plays. Decode via fs::read + image::load_from_memory (magic bytes, extension-agnostic), which also heals mislabeled downloads in the md5 cache. file:// paths now percent-decode (cached_file_url_for builds them via Url::from_file_path), fixing the latent spaces/non-ASCII fall-through to an HTTP GET of a file:// URL. Offline semantics unchanged. cc --- crates/qbz-media-controls/src/notify.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/qbz-media-controls/src/notify.rs b/crates/qbz-media-controls/src/notify.rs index 1ed532723..99363e621 100644 --- a/crates/qbz-media-controls/src/notify.rs +++ b/crates/qbz-media-controls/src/notify.rs @@ -102,7 +102,14 @@ fn artwork_cache_dir() -> Result { #[cfg(any(target_os = "linux", target_os = "macos"))] fn resolve_local_artwork(url: &str) -> Option { if let Some(path) = url.strip_prefix("file://") { - return Some(PathBuf::from(path)); + // file:// URLs built with url::Url::from_file_path (e.g. the shared + // disk-image cache hits handed over by playback) are percent-encoded; + // decode so paths with spaces/non-ASCII resolve. Fall back to the raw + // string on invalid UTF-8 escapes (a plain unencoded path). + let decoded = urlencoding::decode(path) + .map(|c| c.into_owned()) + .unwrap_or_else(|_| path.to_string()); + return Some(PathBuf::from(decoded)); } if let Some(path) = url.strip_prefix("asset://localhost/") { let decoded = urlencoding::decode(path).ok()?; @@ -184,11 +191,19 @@ const PORTAL_ICON_MAX_BYTES: usize = 4 * 1024 * 1024; /// Center-crop to a square, downscale to <=512px, re-encode PNG. Mirrors the /// Tauri `v2_prepare_notification_icon_bytes`. +/// +/// Decodes by CONTENT, never by extension: the shared disk-image cache names +/// files `{md5}.img` (no real image extension), and `image::open` resolves the +/// format from the path extension only — it returned `Unsupported` for every +/// cache hit, which is exactly the common online case. #[cfg(target_os = "linux")] fn prepare_icon_bytes(path: &std::path::Path) -> Result, String> { use std::io::Cursor; - let source = image::open(path).map_err(|e| format!("Failed to decode artwork {path:?}: {e}"))?; + let bytes = + std::fs::read(path).map_err(|e| format!("Failed to read artwork {path:?}: {e}"))?; + let source = image::load_from_memory(&bytes) + .map_err(|e| format!("Failed to decode artwork {path:?}: {e}"))?; let (w, h) = (source.width(), source.height()); let square = if w == h { source