From 67caa89151a22e1cb0c010d530680135b1225390 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 31 Jan 2025 15:19:01 -0700 Subject: [PATCH] Show metadata for audio files, part of #56 --- i18n/en/cosmic_player.ftl | 1 + src/main.rs | 199 ++++++++++++++++++++++---------------- 2 files changed, 118 insertions(+), 82 deletions(-) diff --git a/i18n/en/cosmic_player.ftl b/i18n/en/cosmic_player.ftl index 9961387..45148b6 100644 --- a/i18n/en/cosmic_player.ftl +++ b/i18n/en/cosmic_player.ftl @@ -1,3 +1,4 @@ +album = Album: {$album} audio = Audio no-video-or-audio-file-open = No video or audio file open open-file = Open file diff --git a/src/main.rs b/src/main.rs index a6bc650..2a385a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -198,6 +198,7 @@ pub struct MprisMeta { album: String, album_art_opt: Option, album_artist: String, + album_year_opt: Option, artists: Vec, title: String, disc_number: i32, @@ -266,6 +267,7 @@ pub struct App { dropdown_opt: Option, fullscreen: bool, key_binds: HashMap, + mpris_meta: MprisMeta, mpris_opt: Option<(MprisMeta, MprisState, mpsc::UnboundedSender)>, nav_model: segmented_button::SingleSelectModel, projects: Vec<(String, PathBuf)>, @@ -585,94 +587,93 @@ impl App { } fn update_mpris_meta(&mut self) { - if let Some((old, _, tx)) = &mut self.mpris_opt { - let mut new = MprisMeta { - //TODO: clear url_opt when file is closed - url_opt: self.flags.url_opt.clone(), - duration_micros: (self.duration * 1_000_000.0) as i64, - ..Default::default() - }; - //TODO: use any other stream tags? - if let Some(tags) = self.audio_tags.get(0) { - log::info!("{:#?}", tags); - if let Some(tag) = tags.get::() { - new.album = tag.get().into(); - } - if let Some(tag) = tags.get::() { - new.album_artist = tag.get().into(); - } - if let Some(tag) = tags.get::() { - //TODO: how are multiple artists handled by gstreamer? - new.artists = vec![tag.get().into()]; - } - if let Some(tag) = tags.get::() { - new.title = tag.get().into(); - } - /*TODO: no gstreamer tag - if let Some(tag) = tags.get::() { - new.disc_number = tag.get(); - } - */ - if let Some(tag) = tags.get::() { - new.track_number = tag.get() as i32; - } - if self.album_art_opt.is_none() { - //TODO: run in thread or async to avoid blocking UI? - if let Some(tag) = tags.get::() { - let sample = tag.get(); - if let Some(buffer) = sample.buffer() { - match buffer.map_readable() { - //TODO: use original format instead of converting to PNG? - Ok(buffer_map) => match image::load_from_memory(&buffer_map) { - Ok(image) => { - match tempfile::Builder::new() - .prefix(&format!("cosmic-player.pid{}.", process::id())) - .suffix(".png") - .tempfile() - { - Ok(mut album_art) => { - match image.write_with_encoder( - image::codecs::png::PngEncoder::new( - &mut album_art, - ), - ) { - Ok(()) => self.album_art_opt = Some(album_art), - Err(err) => { - log::warn!( - "failed to write temporary image: {}", - err - ); - } + let mut new = MprisMeta { + //TODO: clear url_opt when file is closed + url_opt: self.flags.url_opt.clone(), + duration_micros: (self.duration * 1_000_000.0) as i64, + ..Default::default() + }; + //TODO: use any other stream tags? + if let Some(tags) = self.audio_tags.get(0) { + log::info!("{:#?}", tags); + if let Some(tag) = tags.get::() { + new.album = tag.get().into(); + } + if let Some(tag) = tags.get::() { + new.album_artist = tag.get().into(); + } + if let Some(tag) = tags.get::() { + new.album_year_opt = Some(tag.get().year()); + } + if let Some(tag) = tags.get::() { + //TODO: how are multiple artists handled by gstreamer? + new.artists = vec![tag.get().into()]; + } + if let Some(tag) = tags.get::() { + new.title = tag.get().into(); + } + /*TODO: no gstreamer tag + if let Some(tag) = tags.get::() { + new.disc_number = tag.get(); + } + */ + if let Some(tag) = tags.get::() { + new.track_number = tag.get() as i32; + } + if self.album_art_opt.is_none() { + //TODO: run in thread or async to avoid blocking UI? + if let Some(tag) = tags.get::() { + let sample = tag.get(); + if let Some(buffer) = sample.buffer() { + match buffer.map_readable() { + //TODO: use original format instead of converting to PNG? + Ok(buffer_map) => match image::load_from_memory(&buffer_map) { + Ok(image) => { + match tempfile::Builder::new() + .prefix(&format!("cosmic-player.pid{}.", process::id())) + .suffix(".png") + .tempfile() + { + Ok(mut album_art) => { + match image.write_with_encoder( + image::codecs::png::PngEncoder::new(&mut album_art), + ) { + Ok(()) => self.album_art_opt = Some(album_art), + Err(err) => { + log::warn!( + "failed to write temporary image: {}", + err + ); } } - Err(err) => { - log::warn!( - "failed to create temporary image: {}", - err - ); - } + } + Err(err) => { + log::warn!("failed to create temporary image: {}", err); } } - Err(err) => { - log::warn!("failed to load image from memory: {}", err); - } - }, - Err(err) => { - log::warn!("failed to map image buffer: {}", err); } + Err(err) => { + log::warn!("failed to load image from memory: {}", err); + } + }, + Err(err) => { + log::warn!("failed to map image buffer: {}", err); } } } } - if let Some(album_art) = &self.album_art_opt { - new.album_art_opt = url::Url::from_file_path(album_art.path()).ok(); - } } - if new != *old { - *old = new.clone(); - let _ = tx.send(MprisEvent::Meta(new)); + if let Some(album_art) = &self.album_art_opt { + new.album_art_opt = url::Url::from_file_path(album_art.path()).ok(); } } + if let Some((old, _, tx)) = &mut self.mpris_opt { + if new != *old { + *old = new.clone(); + let _ = tx.send(MprisEvent::Meta(new.clone())); + } + } + self.mpris_meta = new; } fn update_mpris_state(&mut self) { @@ -782,6 +783,7 @@ impl Application for App { dropdown_opt: None, fullscreen: false, key_binds: key_binds(), + mpris_meta: MprisMeta::default(), mpris_opt: None, nav_model: nav_bar::Model::builder().build(), projects: Vec::new(), @@ -1222,6 +1224,7 @@ impl Application for App { let cosmic_theme::Spacing { space_xxs, space_xs, + space_s, space_m, .. } = theme::active().cosmic().spacing; @@ -1273,14 +1276,46 @@ impl Application for App { if let Some(album_art) = &self.album_art_opt { if !video.has_video() { - // This is a hack to have the video player running but not visible (since the controls will cover it as an overlay) - video_player = widget::column::with_children(vec![ + let mut col = widget::column(); + col = col.push(widget::vertical_space(Length::Fill)); + col = col.push( widget::image(widget::image::Handle::from_path(album_art.path())) .content_fit(ContentFit::ScaleDown) - .width(Length::Fill) - .height(Length::Fill) - .into(), - widget::container(video_player).height(space_m).into(), + .width(Length::Fill), + ); + col = col.push(widget::vertical_space(space_s)); + //TODO: fallback if title missing + col = col.push(widget::text::title4(&self.mpris_meta.title)); + for artist in self.mpris_meta.artists.iter() { + col = col.push(widget::text::body(artist)); + } + col = col.push(widget::vertical_space(space_s)); + if !self.mpris_meta.album.is_empty() { + col = col.push(widget::text::body(fl!( + "album", + album = self.mpris_meta.album.as_str() + ))); + } + if let Some(year) = &self.mpris_meta.album_year_opt { + col = col.push(widget::text::body(format!("{}", year))); + } + col = col.push(widget::vertical_space(Length::Fill)); + + // Space to keep from going under control overlay + let mut control_height = space_xxs + 32 + space_xxs; + if self.core.is_condensed() { + control_height += space_xxs + 32; + } + + // This is a hack to have the video player running but not visible (since the controls will cover it as an overlay) + video_player = widget::row::with_children(vec![ + widget::horizontal_space(Length::Fill).into(), + widget::container( + col.push(widget::container(video_player).height(control_height)), + ) + .width(320) + .into(), + widget::horizontal_space(Length::Fill).into(), ]) .into(); }