feat(slint): MPRIS album art (mpris:artUrl) — Qobuz pass-through + local file://

Port the Tauri normalizeCoverUrlForMetadata workaround as ArtworkRef::to_mpris_url
(qbz-models): remote Qobuz covers pass through as-is, local-file covers become a
percent-encoded file:// URI (MPRIS clients cannot read bare paths or asset://),
Plex thumbs become a tokenized HTTP URL. Wired into the Slint now-playing push so
the media widget shows album art. The app icon was already fixed via DesktopEntry.

cc
This commit is contained in:
vicrodh committed 2026-06-06 11:58:17 -06:00
1 parent ee8700db1a
commit 2a2328e6c6
4 files changed
+39 -3

No files matched your search

+1
View File
@@ -5700,6 +5700,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror 2.0.18",
"url",
]
[[package]]
+3
View File
@@ -21,3 +21,6 @@ thiserror = { workspace = true }
# Time
chrono = { workspace = true }
# Correct file:// URI building for media-controls art (ArtworkRef::to_mpris_url)
url = "2"
+31
View File
@@ -144,6 +144,37 @@ impl ArtworkRef {
ArtworkRef::PlexThumb { path, .. } => path.is_empty(),
}
}
/// A URL suitable for the MPRIS `mpris:artUrl` property (and other OS media
/// controls). Mirrors the Tauri frontend's `normalizeCoverUrlForMetadata`:
/// - **Remote** HTTP(S) covers (Qobuz) pass through — clients fetch them.
/// - **LocalFile** bare paths become a proper percent-encoded `file://`
/// URI (MPRIS clients cannot read a bare path or an `asset://` URL).
/// - **PlexThumb** becomes the tokenized HTTP URL the client can fetch.
/// - **Embedded** bytes / **None** have no URL (`None`).
pub fn to_mpris_url(&self) -> Option<String> {
match self {
ArtworkRef::Remote(s) if !s.is_empty() => Some(s.clone()),
ArtworkRef::LocalFile(p) if !p.is_empty() => {
// Already a file URL? keep it. Otherwise build one (absolute
// paths only — `from_file_path` rejects relative, → None).
if p.starts_with("file://") {
Some(p.clone())
} else {
url::Url::from_file_path(p).ok().map(|u| u.to_string())
}
}
ArtworkRef::PlexThumb {
base_url,
token,
path,
} if !path.is_empty() => {
let sep = if path.contains('?') { '&' } else { '?' };
Some(format!("{base_url}{path}{sep}X-Plex-Token={token}"))
}
_ => None,
}
}
}
impl QueueTrack {
+4 -3
View File
@@ -562,15 +562,16 @@ pub(crate) async fn refresh_now_playing_meta(runtime: &Runtime, weak: &slint::We
}
// Push to the OS media controls (MPRIS / SMTC / MediaRemote). The app icon
// GNOME shows comes from the MPRIS DesktopEntry, not from album art; art_url
// (album art) is a later refinement.
// GNOME shows comes from the MPRIS DesktopEntry; `art_url` is the album art
// (`mpris:artUrl`) — remote Qobuz covers pass through, local covers become a
// file:// URI, matching the Tauri `normalizeCoverUrlForMetadata` workaround.
if let Some(mc) = crate::media_controls::handle() {
mc.set_metadata(&qbz_media_controls::TrackMeta {
title: title.clone(),
artist: artist.clone(),
album: album.clone(),
duration: (duration > 0).then(|| std::time::Duration::from_secs(duration as u64)),
art_url: None,
art_url: artwork.to_mpris_url(),
});
mc.set_playback(
qbz_media_controls::PlaybackStatus::Playing,