diff --git a/src/mpris.rs b/src/mpris.rs index 58d5f13..3457edf 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -1,3 +1,6 @@ +mod player; +pub use player::Player; + use adw::prelude::*; use gtk::glib; use tracing::{event, Level}; diff --git a/src/mpris/player.rs b/src/mpris/player.rs new file mode 100644 index 0000000..c23de94 --- /dev/null +++ b/src/mpris/player.rs @@ -0,0 +1,390 @@ +use crate::{model::Song, ui::Window}; +use adw::prelude::*; +use gtk::glib::SendWeakRef; +use std::collections::HashMap; +use tracing::{event, Level}; +use zbus::object_server::InterfaceRef; +use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value}; + +const MICROSECONDS: f64 = 1e6; // in a second + +#[derive(Default)] +struct MetadataMap { + // mpris + track_id: Option, + length: Option, + art_url: Option, + // xesam + album: Option, + //album_artist: Option>, + artist: Option>, + //as_text: Option, + //audio_bpm: Option, + //auto_rating: Option, + //comment: Option>, + //composer: Option>, + content_created: Option, + //disc_number: Option, + //first_used: Option, + genre: Option>, + //last_used: Option, + //lyricist: Option>, + title: Option, + track_number: Option, + //url: Option, + //use_count: Option, + user_rating: Option, +} + +impl MetadataMap { + fn from_playbin_song(song: Option<&Song>) -> Self { + song.map(|song| MetadataMap { + // use a unique growing counter to identify tracks + track_id: Some({ + format!("/eu/callcc/audrey/Track/{}", song.counter()) + .try_into() + .unwrap() + }), + length: Some(song.duration() * MICROSECONDS as i64), + //art_url: Some(song.cover_art_url()), // FIXME: this would leak credentials + 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 + //genre: Some(song.genre.iter().collect()), + title: Some(song.title()), + //track_number: song.track().map(|u| u as i32), + //user_rating: Some(if song.starred().is_none() { 0.0 } else { 1.0 }), + ..Default::default() + }) + .unwrap_or_default() + } + + fn as_hash_map(&self) -> HashMap<&'static str, OwnedValue> { + let mut map = HashMap::new(); + + if let Some(track_id) = &self.track_id { + map.insert( + "mpris:trackid", + Value::new(track_id.as_ref()).try_into().unwrap(), + ); + } + if let Some(art_url) = &self.art_url { + map.insert( + "mpris:artUrl", + Value::new(art_url.to_string()).try_into().unwrap(), + ); + } + if let Some(length) = &self.length { + map.insert("mpris:length", Value::new(length).try_into().unwrap()); + } + if let Some(album) = &self.album { + map.insert("xesam:album", Value::new(album).try_into().unwrap()); + } + if let Some(artist) = &self.artist { + map.insert("xesam:artist", Value::new(artist).try_into().unwrap()); + } + if let Some(content_created) = &self.content_created { + map.insert( + "xesam:contentCreated", + Value::new(content_created.format("%+").to_string()) + .try_into() + .unwrap(), + ); + } + if let Some(genre) = &self.genre { + map.insert("xesam:genre", Value::new(genre).try_into().unwrap()); + } + if let Some(track_number) = self.track_number { + map.insert( + "xesam:trackNumber", + Value::new(track_number).try_into().unwrap(), + ); + } + if let Some(title) = &self.title { + map.insert("xesam:title", Value::new(title).try_into().unwrap()); + } + if let Some(user_rating) = self.user_rating { + map.insert( + "xesam:userRating", + Value::new(user_rating).try_into().unwrap(), + ); + } + + map + } +} + +pub struct Player { + metadata: MetadataMap, + window: SendWeakRef, +} + +impl Player { + pub async fn new( + object_server: &zbus::ObjectServer, + playbin: &Window, + ) -> Result, zbus::Error> { + let player = Self { + metadata: MetadataMap::from_playbin_song(None), + window: playbin.downgrade().into(), + }; + + object_server.at("/org/mpris/MediaPlayer2", player).await?; + + Ok(object_server + .interface::<_, Self>("/org/mpris/MediaPlayer2") + .await?) + + /* + playbin.connect_new_track(glib::clone!( + #[strong] + player_ref, + move |_, song| { + let metadata = MetadataMap::from_playbin_song(Some(song)); + + let player_ref = player_ref.clone(); + glib::spawn_future_local(async move { + let mut player = player_ref.get_mut().await; + player.metadata = metadata; + player + .metadata_changed(player_ref.signal_emitter()) + .await + .unwrap(); + }); + } + )); + + playbin.connect_seeked(glib::clone!( + #[strong] + player_ref, + move |_, position| { + let player_ref = player_ref.clone(); + glib::spawn_future_local(async move { + player_ref + .seeked((position * MICROSECONDS) as i64) + .await + .unwrap(); + }); + } + )); + + playbin.connect_notify_future_local( + "play-queue-length", + glib::clone!( + #[strong] + player_ref, + move |_, _| { + let player_ref = player_ref.clone(); + async move { + let player = player_ref.get_mut().await; + // properties that depend on the play queue length + player + .can_go_next_changed(player_ref.signal_emitter()) + .await + .unwrap(); + player + .can_go_previous_changed(player_ref.signal_emitter()) + .await + .unwrap(); + player + .can_play_changed(player_ref.signal_emitter()) + .await + .unwrap(); + } + } + ), + ); + + playbin.connect_notify_future_local( + "state", + glib::clone!( + #[strong] + player_ref, + move |_, _| { + let player_ref = player_ref.clone(); + async move { + let player = player_ref.get_mut().await; + // properties that depend on the playbin state + player + .playback_status_changed(player_ref.signal_emitter()) + .await + .unwrap(); + } + } + ), + ); + + playbin.connect_notify_future_local( + "volume", + glib::clone!( + #[strong] + player_ref, + move |_, _| { + let player_ref = player_ref.clone(); + async move { + let player = player_ref.get_mut().await; + player + .volume_changed(player_ref.signal_emitter()) + .await + .unwrap(); + } + } + ), + ); + */ + } + + fn window(&self) -> Window { + self.window.upgrade().unwrap() + } +} + +#[zbus::interface(name = "org.mpris.MediaPlayer2.Player")] +impl Player { + fn next(&self) -> zbus::fdo::Result<()> { + todo!() + } + + fn previous(&self) -> zbus::fdo::Result<()> { + todo!() + } + + fn pause(&self) -> zbus::fdo::Result<()> { + todo!() + } + + fn play_pause(&self) -> zbus::fdo::Result<()> { + todo!() + } + + fn stop(&self) -> zbus::fdo::Result<()> { + todo!() + } + + fn play(&self) -> zbus::fdo::Result<()> { + todo!() + } + + fn seek(&self, _offset: i64) -> zbus::fdo::Result<()> { + todo!() + } + + fn set_position(&self, _track_id: ObjectPath<'_>, _position: i64) -> zbus::fdo::Result<()> { + todo!() + } + + fn open_uri(&self, _s: String) -> zbus::fdo::Result<()> { + Err(zbus::fdo::Error::NotSupported("OpenUri".into())) + } + + #[zbus(property)] + fn playback_status(&self) -> String { + if self.window().idle_active() { + "Stopped" + } else if self.window().pause() { + "Paused" + } else { + "Playing" + } + .to_string() + } + + #[zbus(property)] + fn loop_status(&self) -> zbus::fdo::Result { + Ok("None".into()) // TODO + } + + #[zbus(property)] + fn set_loop_status(&self, _loop_status: &str) -> zbus::Result<()> { + Err(zbus::fdo::Error::NotSupported("setting LoopStatus".into()).into()) // TODO + } + + #[zbus(property)] + fn rate(&self) -> zbus::fdo::Result { + Ok(1.0) + } + + #[zbus(property)] + fn set_rate(&self, _rate: f64) -> zbus::Result<()> { + // A value of 0.0 should not be set by the client. If it is, the media player should act as though Pause was called. + todo!() + } + + #[zbus(property)] + // FIXME: https://github.com/dbus2/zbus/issues/992 + fn shuffle(&self) -> zbus::fdo::Result { + Ok(false) + } + + #[zbus(property)] + // FIXME: see above + fn set_shuffle(&self, _shuffle: bool) -> zbus::Result<()> { + Err(zbus::fdo::Error::NotSupported("setting Shuffle".into()).into()) + } + + #[zbus(property)] + fn metadata(&self) -> HashMap<&'static str, OwnedValue> { + self.metadata.as_hash_map() + } + + #[zbus(property)] + fn volume(&self) -> f64 { + self.window().volume() as f64 / 100.0 + } + + #[zbus(property)] + fn set_volume(&self, _volume: f64) -> zbus::Result<()> { + todo!() + } + + #[zbus(property(emits_changed_signal = "false"))] + fn position(&self) -> i64 { + (self.window().time_pos() * MICROSECONDS as f64) as i64 + } + + #[zbus(property)] + fn minimum_rate(&self) -> f64 { + 1.0 + } + + #[zbus(property)] + fn maximum_rate(&self) -> f64 { + 1.0 + } + + #[zbus(property)] + fn can_go_next(&self) -> bool { + true // TODO + } + + #[zbus(property)] + fn can_go_previous(&self) -> bool { + true // TODO + } + + #[zbus(property)] + fn can_play(&self) -> bool { + true // TODO + } + + #[zbus(property)] + fn can_pause(&self) -> bool { + true // TODO + } + + #[zbus(property)] + fn can_seek(&self) -> bool { + true // TODO + } + + #[zbus(property(emits_changed_signal = "const"))] + fn can_control(&self) -> bool { + true + } +} + +impl Drop for Player { + fn drop(&mut self) { + event!(Level::DEBUG, "dropping MprisPlayer"); + } +} diff --git a/src/ui/window.rs b/src/ui/window.rs index 01a9df2..2f3385c 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -1,6 +1,6 @@ mod imp { use crate::model::Song; - use crate::mpv; + use crate::{mpris, mpv}; use adw::prelude::*; use adw::subclass::prelude::*; use glib::subclass::InitializingObject; @@ -9,6 +9,7 @@ mod imp { use std::cell::{Cell, RefCell}; use std::rc::Rc; use tracing::{event, span, Level}; + use zbus::object_server::InterfaceRef; #[derive(gtk::CompositeTemplate, glib::Properties)] #[template(resource = "/eu/callcc/audrey/window.ui")] @@ -64,6 +65,8 @@ mod imp { buffering_timeout: Cell>, time_pos_notify_timeout: Cell>, + + mpris_player: RefCell>>, } impl Default for Window { @@ -116,6 +119,8 @@ mod imp { buffering_timeout: Default::default(), time_pos_notify_timeout: Default::default(), + + mpris_player: Default::default(), } } } @@ -218,11 +223,12 @@ mod imp { crate::Mpris::setup(conn.object_server(), &window) .await .expect("could not serve mpris"); - /* - crate::mpris::Player::setup(conn.object_server(), &window.imp().playbin) - .await - .expect("could not serve mpris player"); - FIXME */ + + window.imp().mpris_player.replace(Some( + mpris::Player::new(conn.object_server(), &window) + .await + .expect("could not serve mpris player"), + )); // always set up handlers before requesting service name conn.request_name("org.mpris.MediaPlayer2.audrey")