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
|
# for helix
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
build
|
||||||
|
|
16
Cargo.lock
generated
16
Cargo.lock
generated
|
@ -408,9 +408,9 @@ checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.19.0"
|
version = "1.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
|
checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
|
@ -1247,9 +1247,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.6"
|
version = "0.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
|
checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -1682,9 +1682,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.11"
|
version = "1.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
checksum = "7a73e9fe3c49d7afb2ace819fa181a287ce54a0983eda4e0eb05c22f82ffe534"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
|
@ -3224,9 +3224,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.13"
|
version = "1.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
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))
|
.cover_art(&id, Some(50 * scale_factor))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(pixbuf) => pixbuf,
|
Ok(image) => audrey::util::image_to_pixbuf(image),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
event!(
|
event!(
|
||||||
Level::ERROR,
|
Level::ERROR,
|
||||||
|
|
|
@ -36,14 +36,6 @@ pub struct MetadataMap {
|
||||||
user_rating: Option<f32>,
|
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 {
|
impl MetadataMap {
|
||||||
pub fn from_playbin_song(song: Option<&Song>) -> Self {
|
pub fn from_playbin_song(song: Option<&Song>) -> Self {
|
||||||
song.map(|song| MetadataMap {
|
song.map(|song| MetadataMap {
|
||||||
|
@ -54,7 +46,11 @@ impl MetadataMap {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}),
|
}),
|
||||||
length: Some(song.duration() * MICROSECONDS as i64),
|
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()),
|
album: Some(song.album()),
|
||||||
artist: Some(vec![song.artist()]),
|
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
|
//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 rand::Rng;
|
||||||
use tracing::{event, Level};
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
UrlParseError(url::ParseError),
|
UrlParseError(url::ParseError),
|
||||||
|
@ -172,7 +161,7 @@ impl Client {
|
||||||
// note that this is why those silly bounds on T are needed, because we're
|
// 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
|
// sending back the result of the query from another thread
|
||||||
let future = request.send();
|
let future = request.send();
|
||||||
runtime().spawn(async move {
|
audrey::globals::runtime().spawn(async move {
|
||||||
// wrap this logic in a fn so we can use ?
|
// wrap this logic in a fn so we can use ?
|
||||||
async fn perform(
|
async fn perform(
|
||||||
response: Result<reqwest::Response, reqwest_middleware::Error>,
|
response: Result<reqwest::Response, reqwest_middleware::Error>,
|
||||||
|
@ -281,7 +270,7 @@ impl Client {
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
size: Option<u32>,
|
size: Option<u32>,
|
||||||
) -> Result<gtk::gdk_pixbuf::Pixbuf, Error> {
|
) -> Result<image::DynamicImage, Error> {
|
||||||
let url = self.cover_art_url(id, size);
|
let url = self.cover_art_url(id, size);
|
||||||
let byteresponse = self.get_bytes(url).await?;
|
let byteresponse = self.get_bytes(url).await?;
|
||||||
if byteresponse.content_type == "application/json" {
|
if byteresponse.content_type == "application/json" {
|
||||||
|
@ -295,7 +284,7 @@ impl Client {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
// decoding the image can take up a good chunk of cpu time, so spawn a blocking task
|
// 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 || {
|
.spawn_blocking(move || {
|
||||||
ImageReader::new(std::io::BufReader::new(std::io::Cursor::new(
|
ImageReader::new(std::io::BufReader::new(std::io::Cursor::new(
|
||||||
byteresponse.bytes,
|
byteresponse.bytes,
|
||||||
|
@ -308,24 +297,7 @@ impl Client {
|
||||||
.await
|
.await
|
||||||
.expect("could not receive image from blocking tokio task")?;
|
.expect("could not receive image from blocking tokio task")?;
|
||||||
|
|
||||||
let width = image.width();
|
Ok(image)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stream_url(&self, id: &str) -> url::Url {
|
pub fn stream_url(&self, id: &str) -> url::Url {
|
||||||
|
|
|
@ -738,11 +738,11 @@ mod imp {
|
||||||
.loading_cover_handle
|
.loading_cover_handle
|
||||||
.replace(Some(glib::spawn_future_local(async move {
|
.replace(Some(glib::spawn_future_local(async move {
|
||||||
let api = window.imp().api.borrow().as_ref().unwrap().clone();
|
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
|
.cover_art(&song_id, None) // full size
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(pixbuf) => pixbuf,
|
Ok(image) => image,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
event!(
|
event!(
|
||||||
Level::ERROR,
|
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() {
|
match window.song() {
|
||||||
Some(song) if song.id() == song_id => {
|
Some(song) if song.id() == song_id => {
|
||||||
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
||||||
|
@ -792,6 +806,24 @@ mod imp {
|
||||||
css.push('}');
|
css.push('}');
|
||||||
|
|
||||||
window.imp().css_provider.load_from_string(&css);
|
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!(
|
event!(
|
||||||
|
|
Loading…
Reference in a new issue