place image for mpris
This commit is contained in:
parent
46231c8012
commit
d35e55750a
7 changed files with 100 additions and 52 deletions
1
.ignore
1
.ignore
|
@ -1,2 +1,3 @@
|
|||
# for helix
|
||||
Cargo.lock
|
||||
build
|
||||
|
|
16
Cargo.lock
generated
16
Cargo.lock
generated
|
@ -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
47
src/lib.rs
Normal 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(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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!(
|
||||
|
|
Loading…
Reference in a new issue