use std::io::Cursor;
use anyhow::{Context, Result, bail};
use image::{DynamicImage, ImageReader, Limits};
const TARGET_WIDTH: u32 = 300;
const MAX_IMAGE_DIM: u32 = 4096;
pub fn is_allowed_art_url(url: &str) -> bool {
match reqwest::Url::parse(url) {
Ok(u) => {
u.scheme() == "https"
&& u.host_str()
.is_some_and(|h| h == "scdn.co" || h.ends_with(".scdn.co"))
}
Err(_) => false,
}
}
pub fn pick_image_url(images: &[(String, u32, u32)]) -> Option<String> {
images
.iter()
.min_by_key(|(_, w, _)| w.abs_diff(TARGET_WIDTH))
.map(|(url, _, _)| url.clone())
}
pub async fn fetch_decode(client: &reqwest::Client, url: &str) -> Result<DynamicImage> {
if !is_allowed_art_url(url) {
bail!("cover-art URL is not allowed");
}
let bytes = client
.get(url)
.send()
.await
.context("failed to fetch the cover art")?
.error_for_status()
.context("failed to fetch the cover art")?
.bytes()
.await
.context("failed to read the cover-art body")?;
let img = tokio::task::spawn_blocking(move || -> Result<DynamicImage> {
let mut reader = ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.context("failed to detect the image format")?;
let mut limits = Limits::default();
limits.max_image_width = Some(MAX_IMAGE_DIM);
limits.max_image_height = Some(MAX_IMAGE_DIM);
reader.limits(limits);
reader.decode().context("failed to decode the image")
})
.await
.context("failed to run the image-decode task")??;
Ok(img)
}
#[cfg(test)]
mod tests {
use super::*;
fn img(url: &str, w: u32) -> (String, u32, u32) {
(url.to_string(), w, w)
}
#[test]
fn picks_width_closest_to_target() {
let imgs = [img("a640", 640), img("b300", 300), img("c64", 64)];
assert_eq!(pick_image_url(&imgs).as_deref(), Some("b300"));
}
#[test]
fn picks_nearest_when_no_exact() {
let imgs = [img("big", 500), img("small", 200)];
assert_eq!(pick_image_url(&imgs).as_deref(), Some("small"));
}
#[test]
fn empty_is_none() {
assert_eq!(pick_image_url(&[]), None);
}
#[test]
fn unknown_width_still_selectable() {
let imgs = [img("only", 0)];
assert_eq!(pick_image_url(&imgs).as_deref(), Some("only"));
}
#[test]
fn allows_only_https_spotify_cdn() {
assert!(is_allowed_art_url("https://i.scdn.co/image/ab67616d"));
assert!(is_allowed_art_url("https://scdn.co/x"));
}
#[test]
fn rejects_non_cdn_and_non_https() {
assert!(!is_allowed_art_url("http://i.scdn.co/image/x"));
assert!(!is_allowed_art_url(
"https://169.254.169.254/latest/meta-data"
));
assert!(!is_allowed_art_url("https://evil.com/scdn.co"));
assert!(!is_allowed_art_url("https://scdn.co.evil.com/x"));
assert!(!is_allowed_art_url("not a url"));
}
}