diff --git a/.ignore b/.ignore index b21378a..12fbf84 100644 --- a/.ignore +++ b/.ignore @@ -1,2 +1,3 @@ # for helix Cargo.lock +build diff --git a/Cargo.lock b/Cargo.lock index 3f5306a..7bfb739 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -408,9 +408,9 @@ checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" [[package]] name = "bytemuck" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" [[package]] name = "byteorder" @@ -1247,9 +1247,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", @@ -1682,9 +1682,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "7a73e9fe3c49d7afb2ace819fa181a287ce54a0983eda4e0eb05c22f82ffe534" [[package]] name = "js-sys" @@ -3224,9 +3224,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-width" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..61198a1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,47 @@ +pub mod globals { + pub fn art_path() -> &'static std::path::Path { + static ART_PATH: std::sync::LazyLock = std::sync::LazyLock::new(|| { + let xdg_dirs = + xdg::BaseDirectories::with_prefix("audrey").expect("failed to get xdg dirs"); + xdg_dirs + .place_state_file("mpris-art") + .expect("failed to create mpris art state dir") + }); + &ART_PATH + } + + pub fn runtime() -> &'static tokio::runtime::Runtime { + static RUNTIME: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .thread_name("audrey-tokio-runtime") + .build() + .expect("tokio no spawn :(") + }); + &RUNTIME + } +} + +pub mod util { + use gtk::{gdk_pixbuf, glib}; + + pub fn image_to_pixbuf(image: image::DynamicImage) -> gdk_pixbuf::Pixbuf { + let width = image.width(); + let height = image.height(); + // 8bpc rgba -> 32 -> 4 bytes + let stride = width * 4; + let image = image.into_rgba8().into_vec(); + + gtk::gdk_pixbuf::Pixbuf::from_bytes( + &glib::Bytes::from_owned(image), + gtk::gdk_pixbuf::Colorspace::Rgb, + true, + 8, + // TODO: bubble up these to fail image loads instead of crashing + width.try_into().unwrap(), + height.try_into().unwrap(), + stride.try_into().unwrap(), + ) + } +} diff --git a/src/model/song.rs b/src/model/song.rs index c872371..884abbd 100644 --- a/src/model/song.rs +++ b/src/model/song.rs @@ -103,7 +103,7 @@ impl Song { .cover_art(&id, Some(50 * scale_factor)) .await { - Ok(pixbuf) => pixbuf, + Ok(image) => audrey::util::image_to_pixbuf(image), Err(err) => { event!( Level::ERROR, diff --git a/src/mpris/player.rs b/src/mpris/player.rs index 67e3484..ef3e04b 100644 --- a/src/mpris/player.rs +++ b/src/mpris/player.rs @@ -36,14 +36,6 @@ pub struct MetadataMap { user_rating: Option, } -pub fn art_path() -> &'static std::path::Path { - static PATH: std::sync::OnceLock = std::sync::OnceLock::new(); - PATH.get_or_init(|| { - let xdg_dirs = xdg::BaseDirectories::with_prefix("audrey").expect("failed to get xdg dirs"); - xdg_dirs.get_state_file("mpris-art") - }) -} - impl MetadataMap { pub fn from_playbin_song(song: Option<&Song>) -> Self { song.map(|song| MetadataMap { @@ -54,7 +46,11 @@ impl MetadataMap { .unwrap() }), length: Some(song.duration() * MICROSECONDS as i64), - art_url: Some(url::Url::from_file_path(art_path()).unwrap().to_string()), + art_url: Some( + url::Url::from_file_path(audrey::globals::art_path()) + .unwrap() + .to_string(), + ), album: Some(song.album()), artist: Some(vec![song.artist()]), //content_created: song.year().map(|year| chrono::NaiveDate::from_yo_opt(year, 1).unwrap()), // FIXME: replace this unwrap with Some(Err) -> None diff --git a/src/subsonic.rs b/src/subsonic.rs index 3f51337..b79137e 100644 --- a/src/subsonic.rs +++ b/src/subsonic.rs @@ -9,17 +9,6 @@ use image::ImageReader; use rand::Rng; use tracing::{event, Level}; -fn runtime() -> &'static tokio::runtime::Runtime { - static RUNTIME: std::sync::OnceLock = std::sync::OnceLock::new(); - RUNTIME.get_or_init(|| { - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .thread_name("audrey-tokio-runtime") - .build() - .expect("tokio no spawn :(") - }) -} - #[derive(Debug)] pub enum Error { UrlParseError(url::ParseError), @@ -172,7 +161,7 @@ impl Client { // note that this is why those silly bounds on T are needed, because we're // sending back the result of the query from another thread let future = request.send(); - runtime().spawn(async move { + audrey::globals::runtime().spawn(async move { // wrap this logic in a fn so we can use ? async fn perform( response: Result, @@ -281,7 +270,7 @@ impl Client { &self, id: &str, size: Option, - ) -> Result { + ) -> Result { let url = self.cover_art_url(id, size); let byteresponse = self.get_bytes(url).await?; if byteresponse.content_type == "application/json" { @@ -295,7 +284,7 @@ impl Client { return error; } // decoding the image can take up a good chunk of cpu time, so spawn a blocking task - let image = runtime() + let image = audrey::globals::runtime() .spawn_blocking(move || { ImageReader::new(std::io::BufReader::new(std::io::Cursor::new( byteresponse.bytes, @@ -308,24 +297,7 @@ impl Client { .await .expect("could not receive image from blocking tokio task")?; - let width = image.width(); - let height = image.height(); - // 8bpc rgba -> 32 -> 4 bytes - let stride = width * 4; - // gtk only supports 8bpc; maybe in the future we should handle hdr >8bpc too - let image = image.into_rgba8().into_vec(); - - let pixbuf = gtk::gdk_pixbuf::Pixbuf::from_bytes( - &glib::Bytes::from_owned(image), - gtk::gdk_pixbuf::Colorspace::Rgb, - true, - 8, - // TODO: bubble up these to fail image loads instead of crashing - width.try_into().unwrap(), - height.try_into().unwrap(), - stride.try_into().unwrap(), - ); - Ok(pixbuf) + Ok(image) } pub fn stream_url(&self, id: &str) -> url::Url { diff --git a/src/ui/window.rs b/src/ui/window.rs index 6a9477a..7f45901 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -738,11 +738,11 @@ mod imp { .loading_cover_handle .replace(Some(glib::spawn_future_local(async move { let api = window.imp().api.borrow().as_ref().unwrap().clone(); - let pixbuf = match api + let image = match api .cover_art(&song_id, None) // full size .await { - Ok(pixbuf) => pixbuf, + Ok(image) => image, Err(err) => { event!( Level::ERROR, @@ -752,6 +752,20 @@ mod imp { } }; + // we consume image below into a pixbuf so we need a copy for save + let image_copy = image.clone(); + let save_future = audrey::globals::runtime().spawn_blocking( + move || -> Result<(), image::ImageError> { + let save_path = audrey::globals::art_path(); + let resized = + image_copy.resize(400, 400, image::imageops::FilterType::Lanczos3); + resized.save_with_format(save_path, image::ImageFormat::Jpeg)?; + Ok(()) + }, + ); + + let pixbuf = audrey::util::image_to_pixbuf(image); + match window.song() { Some(song) if song.id() == song_id => { let texture = gdk::Texture::for_pixbuf(&pixbuf); @@ -792,6 +806,24 @@ mod imp { css.push('}'); window.imp().css_provider.load_from_string(&css); + + match save_future.await { + // signal to mpris we saved the cover art for loading + Ok(res) => match res { + Ok(()) => window.imp().mpris_player_metadata_changed(), + Err(e) => event!( + Level::ERROR, + "failed to bg save cover art for mpris: {:?}", + e + ), + }, + Err(_) => { + event!( + Level::ERROR, + "failed to bg save cover art for mpris (tokio)" + ) + } + } } _ => { event!(