fix(qbz-qt): decode local artwork file URIs

cx
This commit is contained in:
vicrodh committed 2026-08-25 22:27:27 -06:00
1 parent 2d31244a7a
commit 24d778e9cf
3 files changed
+42 -5

No files matched your search

+25 -2
View File
@@ -228,8 +228,17 @@ impl QueueTrack {
}
if raw.starts_with("http://") || raw.starts_with("https://") {
ArtworkRef::Remote(raw.to_string())
} else if let Some(path) = raw.strip_prefix("file://") {
ArtworkRef::LocalFile(path.to_string())
} else if raw.starts_with("file://") {
// Queue builders hand this layer a URI, not a filesystem path.
// Decode it before `to_mpris_url` canonicalises it again; merely
// stripping the scheme turns `%23` into a literal filename and
// the second encode publishes `%2523` to MPRIS.
let path = url::Url::parse(raw)
.ok()
.and_then(|url| url.to_file_path().ok())
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_else(|| raw.trim_start_matches("file://").to_string());
ArtworkRef::LocalFile(path)
} else {
ArtworkRef::LocalFile(raw.to_string())
}
@@ -373,6 +382,20 @@ mod tests {
);
}
#[test]
fn local_file_uri_round_trips_special_path_chars_for_mpris() {
let path = "/mnt/music/Disc 01 - Soundtrack #01/100%? cover.jpg";
let queue_uri = "file:///mnt/music/Disc 01 - Soundtrack %2301/100%25%3F cover.jpg";
let artwork = track_with(Some("local"), Some(queue_uri)).artwork_ref();
assert_eq!(artwork, ArtworkRef::LocalFile(path.into()));
assert_eq!(
artwork.to_mpris_url(),
url::Url::from_file_path(path)
.ok()
.map(|url| url.to_string())
);
}
#[test]
fn artwork_ref_with_plex_resolves_library_path() {
// Plex thumb path + creds → PlexThumb (the now-playing/queue/MPRIS path).
+8 -2
View File
@@ -162,8 +162,12 @@ pub fn classify(url: &str) -> ArtUrl {
}
// A `file://` url is ALREADY on disk — reqwest cannot even build a
// request for it ("builder error for url"), so it must never be fetched.
if let Some(path) = url.strip_prefix("file://") {
return ArtUrl::LocalFile(path.to_string());
if url.starts_with("file://") {
// `local_queue_track` escapes `%`, `#` and `?` before this value
// reaches QML. The filesystem probe needs the inverse, otherwise a
// real `Disc 01 - Soundtrack #01/cover.jpg` is stat'ed as a literal
// `...%2301/cover.jpg` and every queue-owned surface gets a miss.
return ArtUrl::LocalFile(local_path(url));
}
// A SOURCE-TAGGED token (`jellyfin:<albumId>/<tag>`,
// `subsonic:al-<id>_<hash>`), stamped by `local_queue_track` for a media
@@ -892,6 +896,8 @@ mod tests {
classify("/home/u/Music/Album/cover.jpg"),
ArtUrl::LocalFile("/home/u/Music/Album/cover.jpg".into())
);
let raw = "/mnt/music/Disc 01 - Soundtrack #01/100%? cover.jpg";
assert_eq!(classify(&file_url(raw)), ArtUrl::LocalFile(raw.into()));
}
#[test]
+9 -1
View File
@@ -966,7 +966,10 @@ mod tests {
.as_nanos();
let box_dir =
std::env::temp_dir().join(format!("qbz-queue-disc-art-{}-{nonce}", std::process::id()));
let disc_one = box_dir.join("Disc 01 - Sanctuary");
// `#` is the exact seam this fixture protects: QueueTrack carries a
// file URI (`%23`), while the NPB/queue resolver must stat the decoded
// filesystem path and MPRIS must not publish a double-escaped `%2523`.
let disc_one = box_dir.join("Disc 01 - TV Series Soundtrack #01");
let disc_five = box_dir.join("Disc 05 - Movie OST");
std::fs::create_dir_all(&disc_one).unwrap();
std::fs::create_dir_all(&disc_five).unwrap();
@@ -998,6 +1001,11 @@ mod tests {
assert_ne!(one.artwork_url, five.artwork_url);
assert!(one.artwork_url.as_deref().unwrap().contains("Disc 01"));
assert!(five.artwork_url.as_deref().unwrap().contains("Disc 05"));
let one_resolved = crate::artwork_qt::cached_path(one.artwork_url.as_deref().unwrap());
assert_eq!(
crate::artwork_qt::local_path(&one_resolved),
disc_one.join("cover.jpg").to_string_lossy()
);
std::fs::remove_dir_all(&box_dir).unwrap();
}