place image for mpris

This commit is contained in:
psykose 2024-11-20 13:23:54 +01:00
parent 46231c8012
commit d35e55750a
Signed by: psykose
SSH key fingerprint: SHA256:pRMVjV3kRB6zl+wNx+sV8KoMnPqQAW6v8dNCxsCGZv8
7 changed files with 100 additions and 52 deletions

View file

@ -1,2 +1,3 @@
# for helix
Cargo.lock
build

16
Cargo.lock generated
View file

@ -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"

47
src/lib.rs Normal file
View file

@ -0,0 +1,47 @@
pub mod globals {
pub fn art_path() -> &'static std::path::Path {
static ART_PATH: std::sync::LazyLock<std::path::PathBuf> = 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<tokio::runtime::Runtime> =
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(),
)
}
}

View file

@ -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,

View file

@ -36,14 +36,6 @@ pub struct MetadataMap {
user_rating: Option<f32>,
}
pub fn art_path() -> &'static std::path::Path {
static PATH: std::sync::OnceLock<std::path::PathBuf> = 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

View file

@ -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<tokio::runtime::Runtime> = 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<reqwest::Response, reqwest_middleware::Error>,
@ -281,7 +270,7 @@ impl Client {
&self,
id: &str,
size: Option<u32>,
) -> Result<gtk::gdk_pixbuf::Pixbuf, Error> {
) -> Result<image::DynamicImage, Error> {
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 {

View file

@ -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!(