From f7975e8db6ef73e981e8bd196dd0a41951bec3b6 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Wed, 25 Sep 2024 21:33:45 +0200 Subject: [PATCH 01/55] Bump zbus version Additionally removes the blocking code generation since it's useless now --- Cargo.toml | 2 +- src/proxies.rs | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 956c986..ac2da3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ tokio = ["zbus/tokio"] serde = ["dep:serde"] [dependencies] -zbus = "3.4.0" +zbus = "4.4.0" serde = { version = "1.0.164", optional = true } # Example dependencies diff --git a/src/proxies.rs b/src/proxies.rs index ae7bc16..e3119b7 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -1,23 +1,25 @@ use std::collections::HashMap; -use zbus::dbus_proxy; +use zbus::proxy; use zbus::zvariant::Value; -#[dbus_proxy( +#[proxy( default_service = "org.freedesktop.DBus", interface = "org.freedesktop.DBus", - default_path = "/org/freedesktop/DBus" + default_path = "/org/freedesktop/DBus", + gen_blocking = false )] pub(crate) trait DBus { fn list_names(&self) -> zbus::Result>; } -#[dbus_proxy( +#[proxy( interface = "org.mpris.MediaPlayer2", - default_path = "/org/mpris/MediaPlayer2" + default_path = "/org/mpris/MediaPlayer2", + gen_blocking = false )] pub(crate) trait MediaPlayer2 { - #[dbus_proxy(property)] + #[zbus(property)] fn identity(&self) -> zbus::Result; } @@ -27,11 +29,12 @@ impl MediaPlayer2Proxy<'_> { } } -#[dbus_proxy( +#[proxy( interface = "org.mpris.MediaPlayer2.Player", - default_path = "/org/mpris/MediaPlayer2" + default_path = "/org/mpris/MediaPlayer2", + gen_blocking = false )] pub(crate) trait Player { - #[dbus_proxy(property)] + #[zbus(property)] fn metadata(&self) -> zbus::Result>; } From 54b2f039d084abb9f1740ea9ea350a3b3817301d Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sat, 28 Sep 2024 21:23:55 +0200 Subject: [PATCH 02/55] Implement basic properties and methods Includes the properties and methods from the org.mpris.MediaPlayer2 and org.mpris.MediaPlayer2.Player interfaces. --- src/player.rs | 166 +++++++++++++++++++++++++++++++++++++++++++++++-- src/proxies.rs | 138 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 299 insertions(+), 5 deletions(-) diff --git a/src/player.rs b/src/player.rs index 3357ae0..30fa6d0 100644 --- a/src/player.rs +++ b/src/player.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use zbus::{names::BusName, Connection}; use crate::{ - metadata::MetadataValue, + metadata::{MetadataValue, TrackID}, proxies::{DBusProxy, MediaPlayer2Proxy, PlayerProxy}, Metadata, Mpris, }; @@ -44,10 +44,6 @@ impl<'conn> Player<'conn> { }) } - pub async fn identity(&self) -> Result> { - Ok(self.mp2_proxy.identity().await?) - } - pub async fn metadata(&self) -> Result> { Ok(self.raw_metadata().await?.into()) } @@ -64,6 +60,166 @@ impl<'conn> Player<'conn> { pub fn bus_name(&self) -> &str { self.mp2_proxy.bus_name() } + + pub async fn quit(&self) -> Result<(), Box> { + Ok(self.mp2_proxy.quit().await?) + } + + pub async fn can_quit(&self) -> Result> { + Ok(self.mp2_proxy.can_quit().await?) + } + + pub async fn raise(&self) -> Result<(), Box> { + Ok(self.mp2_proxy.raise().await?) + } + + pub async fn can_raise(&self) -> Result> { + Ok(self.mp2_proxy.can_raise().await?) + } + + pub async fn desktop_entry(&self) -> Result> { + Ok(self.mp2_proxy.desktop_entry().await?) + } + + pub async fn has_track_list(&self) -> Result> { + Ok(self.mp2_proxy.has_track_list().await?) + } + + pub async fn identity(&self) -> Result> { + Ok(self.mp2_proxy.identity().await?) + } + + pub async fn supported_mime_types(&self) -> Result, Box> { + Ok(self.mp2_proxy.supported_mime_types().await?) + } + + pub async fn supported_uri_schemes(&self) -> Result, Box> { + Ok(self.mp2_proxy.supported_uri_schemes().await?) + } + + pub async fn can_control(&self) -> Result> { + Ok(self.player_proxy.can_control().await?) + } + + pub async fn next(&self) -> Result<(), Box> { + Ok(self.player_proxy.next().await?) + } + + pub async fn can_go_next(&self) -> Result> { + Ok(self.player_proxy.can_go_next().await?) + } + + pub async fn previous(&self) -> Result<(), Box> { + Ok(self.player_proxy.previous().await?) + } + + pub async fn can_go_previous(&self) -> Result> { + Ok(self.player_proxy.can_go_previous().await?) + } + + pub async fn play(&self) -> Result<(), Box> { + Ok(self.player_proxy.play().await?) + } + + pub async fn can_play(&self) -> Result> { + Ok(self.player_proxy.can_play().await?) + } + + pub async fn pause(&self) -> Result<(), Box> { + Ok(self.player_proxy.pause().await?) + } + + pub async fn can_pause(&self) -> Result> { + Ok(self.player_proxy.can_pause().await?) + } + + pub async fn play_pause(&self) -> Result<(), Box> { + Ok(self.player_proxy.play_pause().await?) + } + + pub async fn stop(&self) -> Result<(), Box> { + Ok(self.player_proxy.stop().await?) + } + + pub async fn stop_after_current(&self) -> Result<(), Box> { + Ok(self.player_proxy.stop_after_current().await?) + } + + pub async fn seek( + &self, + offset_in_microseconds: i64, + ) -> Result<(), Box> { + Ok(self.player_proxy.seek(offset_in_microseconds).await?) + } + + pub async fn can_seek(&self) -> Result> { + Ok(self.player_proxy.can_seek().await?) + } + + pub async fn get_position(&self) -> Result> { + Ok(self.player_proxy.position().await?) + } + + pub async fn set_position( + &self, + track_id: &TrackID, + position: i64, + ) -> Result<(), Box> { + Ok(self + .player_proxy + .set_position(&(**track_id).try_into()?, position) + .await?) + } + + pub async fn get_loop_status(&self) -> Result> { + Ok(self.player_proxy.loop_status().await?) + } + + pub async fn set_loop_status(&self, loop_status: &str) -> Result<(), Box> { + Ok(self.player_proxy.set_loop_status(loop_status).await?) + } + + + pub async fn playback_status(&self) -> Result> { + Ok(self.player_proxy.playback_status().await?) + } + + + pub async fn open_uri(&self, uri: &str) -> Result<(), Box> { + Ok(self.player_proxy.open_uri(uri).await?) + } + + pub async fn maximum_rate(&self) -> Result> { + Ok(self.player_proxy.maximum_rate().await?) + } + + pub async fn minimum_rate(&self) -> Result> { + Ok(self.player_proxy.minimum_rate().await?) + } + + pub async fn get_playback_rate(&self) -> Result> { + Ok(self.player_proxy.rate().await?) + } + + pub async fn set_playback_rate(&self, rate: f64) -> Result<(), Box> { + Ok(self.player_proxy.set_rate(rate).await?) + } + + pub async fn get_shuffle(&self) -> Result> { + Ok(self.player_proxy.shuffle().await?) + } + + pub async fn set_shuffle(&self, shuffle: bool) -> Result<(), Box> { + Ok(self.player_proxy.set_shuffle(shuffle).await?) + } + + pub async fn get_volume(&self) -> Result> { + Ok(self.player_proxy.volume().await?) + } + + pub async fn set_volume(&self, volume: f64) -> Result<(), Box> { + Ok(self.player_proxy.set_volume(volume).await?) + } } impl<'a> std::fmt::Debug for Player<'a> { diff --git a/src/proxies.rs b/src/proxies.rs index e3119b7..445d5c5 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -19,8 +19,39 @@ pub(crate) trait DBus { gen_blocking = false )] pub(crate) trait MediaPlayer2 { + /// Quit method + fn quit(&self) -> zbus::Result<()>; + + /// Raise method + fn raise(&self) -> zbus::Result<()>; + + /// CanQuit property + #[zbus(property)] + fn can_quit(&self) -> zbus::Result; + + /// CanRaise property + #[zbus(property)] + fn can_raise(&self) -> zbus::Result; + + /// DesktopEntry property + #[zbus(property)] + fn desktop_entry(&self) -> zbus::Result; + + /// HasTrackList property + #[zbus(property)] + fn has_track_list(&self) -> zbus::Result; + + /// Identity property #[zbus(property)] fn identity(&self) -> zbus::Result; + + /// SupportedMimeTypes property + #[zbus(property)] + fn supported_mime_types(&self) -> zbus::Result>; + + /// SupportedUriSchemes property + #[zbus(property)] + fn supported_uri_schemes(&self) -> zbus::Result>; } impl MediaPlayer2Proxy<'_> { @@ -35,6 +66,113 @@ impl MediaPlayer2Proxy<'_> { gen_blocking = false )] pub(crate) trait Player { + /// Next method + fn next(&self) -> zbus::Result<()>; + + /// OpenUri method + fn open_uri(&self, uri: &str) -> zbus::Result<()>; + + /// Pause method + fn pause(&self) -> zbus::Result<()>; + + /// Play method + fn play(&self) -> zbus::Result<()>; + + /// PlayPause method + fn play_pause(&self) -> zbus::Result<()>; + + /// Previous method + fn previous(&self) -> zbus::Result<()>; + + /// Seek method + fn seek(&self, offset: i64) -> zbus::Result<()>; + + /// SetPosition method + fn set_position( + &self, + track_id: &zbus::zvariant::ObjectPath<'_>, + position: i64, + ) -> zbus::Result<()>; + + /// Stop method + fn stop(&self) -> zbus::Result<()>; + + /// StopAfterCurrent method + fn stop_after_current(&self) -> zbus::Result<()>; + + /// Seeked signal + #[zbus(signal)] + fn seeked(&self, position: i64) -> zbus::Result<()>; + + /// CanControl property + #[zbus(property)] + fn can_control(&self) -> zbus::Result; + + /// CanGoNext property + #[zbus(property)] + fn can_go_next(&self) -> zbus::Result; + + /// CanGoPrevious property + #[zbus(property)] + fn can_go_previous(&self) -> zbus::Result; + + /// CanPause property + #[zbus(property)] + fn can_pause(&self) -> zbus::Result; + + /// CanPlay property + #[zbus(property)] + fn can_play(&self) -> zbus::Result; + + /// CanSeek property + #[zbus(property)] + fn can_seek(&self) -> zbus::Result; + + /// LoopStatus property + #[zbus(property)] + fn loop_status(&self) -> zbus::Result; + + #[zbus(property)] + fn set_loop_status(&self, value: &str) -> zbus::Result<()>; + + /// MaximumRate property + #[zbus(property)] + fn maximum_rate(&self) -> zbus::Result; + + /// Metadata property #[zbus(property)] fn metadata(&self) -> zbus::Result>; + + /// MinimumRate property + #[zbus(property)] + fn minimum_rate(&self) -> zbus::Result; + + /// PlaybackStatus property + #[zbus(property)] + fn playback_status(&self) -> zbus::Result; + + /// Position property + #[zbus(property)] + fn position(&self) -> zbus::Result; + + /// Rate property + #[zbus(property)] + fn rate(&self) -> zbus::Result; + + #[zbus(property)] + fn set_rate(&self, value: f64) -> zbus::Result<()>; + + /// Shuffle property + #[zbus(property)] + fn shuffle(&self) -> zbus::Result; + + #[zbus(property)] + fn set_shuffle(&self, value: bool) -> zbus::Result<()>; + + /// Volume property + #[zbus(property)] + fn volume(&self) -> zbus::Result; + + #[zbus(property)] + fn set_volume(&self, value: f64) -> zbus::Result<()>; } From 2cc1b1843af63fe0aa9682d5faeef5cec9db7b54 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Wed, 2 Oct 2024 21:17:29 +0200 Subject: [PATCH 03/55] Implement basic error handling, verify TrackID MprisError is now the general error that's returned from all of the functions. TrackID now uses zbus::ObjectPath to verify if the string is a valid D-Bus path. Also includes helpful traits and methods. --- examples/list_players.rs | 5 +- examples/metadata.rs | 7 +- src/lib.rs | 206 ++++++++++++++++++++++++++++++++++++++- src/metadata.rs | 2 +- src/metadata/track_id.rs | 149 +++++++++++++++++++++++----- src/player.rs | 110 ++++++++++----------- 6 files changed, 381 insertions(+), 98 deletions(-) diff --git a/examples/list_players.rs b/examples/list_players.rs index bac6648..860fef8 100644 --- a/examples/list_players.rs +++ b/examples/list_players.rs @@ -1,8 +1,7 @@ -use mpris::Mpris; -use std::error::Error; +use mpris::{Mpris, MprisError}; #[async_std::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), MprisError> { let mpris = Mpris::new().await?; for player in mpris.players().await? { println!("{:?}", player); diff --git a/examples/metadata.rs b/examples/metadata.rs index b59e9e7..5c27ca4 100644 --- a/examples/metadata.rs +++ b/examples/metadata.rs @@ -1,8 +1,7 @@ -use mpris::{Mpris, Player}; -use std::error::Error; +use mpris::{Mpris, MprisError, Player}; #[async_std::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), MprisError> { let mpris = Mpris::new().await?; let mut total = 0; @@ -18,7 +17,7 @@ async fn main() -> Result<(), Box> { Ok(()) } -async fn print_metadata(player: Player<'_>) -> Result<(), Box> { +async fn print_metadata(player: Player<'_>) -> Result<(), MprisError> { println!( "Player: {} ({})", player.identity().await?, diff --git a/src/lib.rs b/src/lib.rs index 3974bd1..805c932 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,14 @@ +use std::fmt::Display; + use zbus::Connection; mod metadata; mod player; mod proxies; -pub use metadata::Metadata; +use metadata::InvalidTrackID; + +pub use metadata::{Metadata, TrackID}; pub use player::Player; pub struct Mpris { @@ -12,12 +16,208 @@ pub struct Mpris { } impl Mpris { - pub async fn new() -> Result> { + pub async fn new() -> Result { let connection = Connection::session().await?; Ok(Self { connection }) } - pub async fn players(&self) -> Result, Box> { + pub async fn players(&self) -> Result, MprisError> { player::all(&self.connection).await } } + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +/// The [`Player`]'s playback status +/// +/// See: [MPRIS2 specification about `PlaybackStatus`][playback_status] +/// +/// [playback_status]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Enum:Playback_Status +pub enum PlaybackStatus { + /// A track is currently playing. + Playing, + /// A track is currently paused. + Paused, + /// There is no track currently playing. + Stopped, +} + +/// [`PlaybackStatus`] had an invalid string value. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InvalidPlaybackStatus(String); + +impl ::std::str::FromStr for PlaybackStatus { + type Err = InvalidPlaybackStatus; + + fn from_str(string: &str) -> Result { + match string { + "Playing" => Ok(Self::Playing), + "Paused" => Ok(Self::Paused), + "Stopped" => Ok(Self::Stopped), + _ => Err(InvalidPlaybackStatus(string.to_owned())), + } + } +} + +impl PlaybackStatus { + pub fn as_str(&self) -> &str { + match self { + PlaybackStatus::Playing => "Playing", + PlaybackStatus::Paused => "Paused", + PlaybackStatus::Stopped => "Stopped", + } + } +} + +impl Display for PlaybackStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] +/// A [`Player`]'s looping status. +/// +/// See: [MPRIS2 specification about `Loop_Status`][loop_status] +/// +/// [loop_status]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Enum:Loop_Status +pub enum LoopStatus { + /// The playback will stop when there are no more tracks to play + None, + + /// The current track will start again from the begining once it has finished playing + Track, + + /// The playback loops through a list of tracks + Playlist, +} + +/// [`LoopStatus`] had an invalid string value. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InvalidLoopStatus(String); + +impl ::std::str::FromStr for LoopStatus { + type Err = InvalidLoopStatus; + + fn from_str(string: &str) -> Result { + match string { + "None" => Ok(LoopStatus::None), + "Track" => Ok(LoopStatus::Track), + "Playlist" => Ok(LoopStatus::Playlist), + _ => Err(InvalidLoopStatus(string.to_owned())), + } + } +} + +impl LoopStatus { + pub fn as_str(&self) -> &str { + match self { + LoopStatus::None => "None", + LoopStatus::Track => "Track", + LoopStatus::Playlist => "Playlist", + } + } +} + +impl Display for LoopStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum MprisError { + /// An error occurred while talking to the D-Bus. + DbusError(zbus::Error), + + /// Failed to parse an enum from a string value received from the [`Player`]. This means that the + /// [`Player`] replied with unexpected data. + ParseError(String), + + /// Some other unexpected error occurred. + Miscellaneous(String), +} + +impl From for MprisError { + fn from(value: zbus::Error) -> Self { + MprisError::DbusError(value) + } +} + +impl From for MprisError { + fn from(value: InvalidPlaybackStatus) -> Self { + Self::ParseError(value.0) + } +} + +impl From for MprisError { + fn from(value: InvalidLoopStatus) -> Self { + Self::ParseError(value.0) + } +} +impl From for MprisError { + fn from(value: InvalidTrackID) -> Self { + Self::ParseError(value.0) + } +} + +#[cfg(test)] +mod error_tests { + use super::*; + + #[test] + fn valid_playback_status() { + assert_eq!("Playing".parse(), Ok(PlaybackStatus::Playing)); + assert_eq!("Paused".parse(), Ok(PlaybackStatus::Paused)); + assert_eq!("Stopped".parse(), Ok(PlaybackStatus::Stopped)); + } + + #[test] + fn invalid_playback_status() { + assert_eq!( + "".parse::(), + Err(InvalidPlaybackStatus("".into())) + ); + assert_eq!( + "playing".parse::(), + Err(InvalidPlaybackStatus("playing".into())) + ); + assert_eq!( + "wrong".parse::(), + Err(InvalidPlaybackStatus("wrong".into())) + ); + } + + #[test] + fn playback_status_as_str() { + assert_eq!(PlaybackStatus::Playing.as_str(), "Playing"); + assert_eq!(PlaybackStatus::Paused.as_str(), "Paused"); + assert_eq!(PlaybackStatus::Stopped.as_str(), "Stopped"); + } + + #[test] + fn valid_loop_status() { + assert_eq!("None".parse(), Ok(LoopStatus::None)); + assert_eq!("Track".parse(), Ok(LoopStatus::Track)); + assert_eq!("Playlist".parse(), Ok(LoopStatus::Playlist)); + } + + #[test] + fn invalid_loop_status() { + assert_eq!("".parse::(), Err(InvalidLoopStatus("".into()))); + assert_eq!( + "track".parse::(), + Err(InvalidLoopStatus("track".into())) + ); + assert_eq!( + "wrong".parse::(), + Err(InvalidLoopStatus("wrong".into())) + ); + } + + #[test] + fn loop_status_as_str() { + assert_eq!(LoopStatus::None.as_str(), "None"); + assert_eq!(LoopStatus::Track.as_str(), "Track"); + assert_eq!(LoopStatus::Playlist.as_str(), "Playlist"); + } +} diff --git a/src/metadata.rs b/src/metadata.rs index 043c0d0..8cb6099 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -3,5 +3,5 @@ mod track_id; mod values; pub use self::metadata::Metadata; -pub use self::track_id::TrackID; +pub use self::track_id::{InvalidTrackID, TrackID}; pub use self::values::MetadataValue; diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index 2e5e060..89d405a 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -1,73 +1,149 @@ use std::ops::Deref; -use zbus::zvariant::{OwnedValue, Value}; +use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value}; use super::MetadataValue; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +const NO_TRACK: &str = "/org/mpris/MediaPlayer2/TrackList/NoTrack"; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] pub struct TrackID(String); +impl TrackID { + pub fn new(id: String) -> Result { + Self::try_from(id) + } + + pub fn no_track() -> Self { + // We know it's a valid path so it's safe to skip the check + Self(NO_TRACK.into()) + } + + pub fn is_no_track(&self) -> bool { + self.as_str() == NO_TRACK + } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + pub(crate) fn get_object_path(&self) -> ObjectPath { + // Safe because we checked the string at creation + ObjectPath::from_str_unchecked(&self.0) + } +} + +/// [`LoopStatus`] had an invalid string value. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InvalidTrackID(pub(crate) String); + impl TryFrom<&str> for TrackID { - type Error = (); + type Error = InvalidTrackID; fn try_from(value: &str) -> Result { - if value.is_empty() || value == "/org/mpris/MediaPlayer2/TrackList/NoTrack" { - Err(()) - } else { - Ok(TrackID(value.to_owned())) + match ObjectPath::try_from(value) { + Ok(_) => Ok(Self(value.to_string())), + Err(e) => { + if let zbus::zvariant::Error::Message(s) = e { + Err(InvalidTrackID(s)) + } else { + // ObjectValue only creates Serde errors which get converted into + // zbus::zvariant::Error::Message + unreachable!("ObjectPath should only return Message errors") + } + } + } + } +} + +impl TryFrom for TrackID { + type Error = InvalidTrackID; + + fn try_from(value: String) -> Result { + match ObjectPath::try_from(value.as_str()) { + Ok(_) => Ok(Self(value)), + Err(e) => { + if let zbus::zvariant::Error::Message(s) = e { + Err(InvalidTrackID(s)) + } else { + // ObjectValue only creates Serde errors which get converted into + // zbus::zvariant::Error::Message + unreachable!("ObjectPath should only return Message errors") + } + } } } } impl TryFrom for TrackID { - type Error = (); + type Error = InvalidTrackID; fn try_from(value: MetadataValue) -> Result { match value { - MetadataValue::String(s) => s.as_str().try_into(), - _ => Err(()), + MetadataValue::String(s) => s.try_into(), + _ => Err(InvalidTrackID(String::from("not a string"))), } } } impl TryFrom for TrackID { - type Error = (); + type Error = InvalidTrackID; fn try_from(value: OwnedValue) -> Result { - match value.deref() { - Value::Str(s) => s.as_str().try_into(), - Value::ObjectPath(path) => path.as_str().try_into(), - _ => Err(()), - } + Self::try_from(&value) } } impl TryFrom<&OwnedValue> for TrackID { - type Error = (); + type Error = InvalidTrackID; fn try_from(value: &OwnedValue) -> Result { match value.deref() { Value::Str(s) => s.as_str().try_into(), - Value::ObjectPath(path) => path.as_str().try_into(), - _ => Err(()), + Value::ObjectPath(path) => Ok(Self(path.to_string())), + _ => Err(InvalidTrackID(String::from("not a String or ObjectPath"))), } } } impl<'a> TryFrom> for TrackID { - type Error = (); + type Error = InvalidTrackID; fn try_from(value: Value) -> Result { match value { Value::Str(s) => s.as_str().try_into(), - Value::ObjectPath(path) => path.as_str().try_into(), - _ => Err(()), + Value::ObjectPath(path) => Ok(Self(path.to_string())), + _ => Err(InvalidTrackID(String::from("not a String or ObjectPath"))), } } } +impl From for TrackID { + fn from(value: OwnedObjectPath) -> Self { + Self(value.to_string()) + } +} + +impl From<&OwnedObjectPath> for TrackID { + fn from(value: &OwnedObjectPath) -> Self { + Self(value.to_string()) + } +} + +impl From> for TrackID { + fn from(value: ObjectPath) -> Self { + Self(value.to_string()) + } +} + +impl From<&ObjectPath<'_>> for TrackID { + fn from(value: &ObjectPath) -> Self { + Self(value.to_string()) + } +} + impl Deref for TrackID { type Target = str; @@ -76,20 +152,39 @@ impl Deref for TrackID { } } +impl From for ObjectPath<'_> { + fn from(val: TrackID) -> Self { + // We used a ObjectPath when creating TrackID so it's safe to skip the check + ObjectPath::from_string_unchecked(val.0) + } +} + +impl From for OwnedObjectPath { + fn from(val: TrackID) -> Self { + OwnedObjectPath::from(ObjectPath::from(val)) + } +} + +impl From for String { + fn from(value: TrackID) -> Self { + value.0 + } +} + #[cfg(all(test, feature = "serde"))] mod serde_tests { use super::*; - use serde_test::{assert_de_tokens, assert_tokens, Token}; + use serde_test::{assert_de_tokens, assert_ser_tokens, Token}; #[test] fn test_serialization() { - let track_id = TrackID("/foo/bar".to_owned()); - assert_tokens(&track_id, &[Token::String("/foo/bar")]); + let track_id = TrackID::try_from("/foo/bar").unwrap(); + assert_ser_tokens(&track_id, &[Token::Str("/foo/bar")]); } #[test] fn test_deserialization() { - let track_id = TrackID("/foo/bar".to_owned()); - assert_de_tokens(&track_id, &[Token::String("/foo/bar")]); + let track_id = TrackID::try_from("/foo/bar").unwrap(); + assert_de_tokens(&track_id, &[Token::Str("/foo/bar")]); } } diff --git a/src/player.rs b/src/player.rs index 30fa6d0..ae66ef6 100644 --- a/src/player.rs +++ b/src/player.rs @@ -5,7 +5,7 @@ use zbus::{names::BusName, Connection}; use crate::{ metadata::{MetadataValue, TrackID}, proxies::{DBusProxy, MediaPlayer2Proxy, PlayerProxy}, - Metadata, Mpris, + LoopStatus, Metadata, Mpris, MprisError, PlaybackStatus, }; pub(crate) const MPRIS2_PREFIX: &str = "org.mpris.MediaPlayer2."; @@ -20,14 +20,14 @@ impl<'conn> Player<'conn> { pub async fn new( mpris: &'conn Mpris, bus_name: BusName<'static>, - ) -> Result, Box> { + ) -> Result, MprisError> { Player::new_from_connection(mpris.connection.clone(), bus_name).await } pub(crate) async fn new_from_connection( connection: Connection, bus_name: BusName<'static>, - ) -> Result, Box> { + ) -> Result, MprisError> { let mp2_proxy = MediaPlayer2Proxy::builder(&connection) .destination(bus_name.clone())? .build() @@ -44,13 +44,11 @@ impl<'conn> Player<'conn> { }) } - pub async fn metadata(&self) -> Result> { + pub async fn metadata(&self) -> Result { Ok(self.raw_metadata().await?.into()) } - pub async fn raw_metadata( - &self, - ) -> Result, Box> { + pub async fn raw_metadata(&self) -> Result, MprisError> { let data = self.player_proxy.metadata().await?; let raw: HashMap = data.into_iter().map(|(k, v)| (k, v.into())).collect(); @@ -61,163 +59,157 @@ impl<'conn> Player<'conn> { self.mp2_proxy.bus_name() } - pub async fn quit(&self) -> Result<(), Box> { + pub async fn quit(&self) -> Result<(), MprisError> { Ok(self.mp2_proxy.quit().await?) } - pub async fn can_quit(&self) -> Result> { + pub async fn can_quit(&self) -> Result { Ok(self.mp2_proxy.can_quit().await?) } - pub async fn raise(&self) -> Result<(), Box> { + pub async fn raise(&self) -> Result<(), MprisError> { Ok(self.mp2_proxy.raise().await?) } - pub async fn can_raise(&self) -> Result> { + pub async fn can_raise(&self) -> Result { Ok(self.mp2_proxy.can_raise().await?) } - pub async fn desktop_entry(&self) -> Result> { + pub async fn desktop_entry(&self) -> Result { Ok(self.mp2_proxy.desktop_entry().await?) } - pub async fn has_track_list(&self) -> Result> { + pub async fn has_track_list(&self) -> Result { Ok(self.mp2_proxy.has_track_list().await?) } - pub async fn identity(&self) -> Result> { + pub async fn identity(&self) -> Result { Ok(self.mp2_proxy.identity().await?) } - pub async fn supported_mime_types(&self) -> Result, Box> { + pub async fn supported_mime_types(&self) -> Result, MprisError> { Ok(self.mp2_proxy.supported_mime_types().await?) } - pub async fn supported_uri_schemes(&self) -> Result, Box> { + pub async fn supported_uri_schemes(&self) -> Result, MprisError> { Ok(self.mp2_proxy.supported_uri_schemes().await?) } - pub async fn can_control(&self) -> Result> { + pub async fn can_control(&self) -> Result { Ok(self.player_proxy.can_control().await?) } - pub async fn next(&self) -> Result<(), Box> { + pub async fn next(&self) -> Result<(), MprisError> { Ok(self.player_proxy.next().await?) } - pub async fn can_go_next(&self) -> Result> { + pub async fn can_go_next(&self) -> Result { Ok(self.player_proxy.can_go_next().await?) } - pub async fn previous(&self) -> Result<(), Box> { + pub async fn previous(&self) -> Result<(), MprisError> { Ok(self.player_proxy.previous().await?) } - pub async fn can_go_previous(&self) -> Result> { + pub async fn can_go_previous(&self) -> Result { Ok(self.player_proxy.can_go_previous().await?) } - pub async fn play(&self) -> Result<(), Box> { + pub async fn play(&self) -> Result<(), MprisError> { Ok(self.player_proxy.play().await?) } - pub async fn can_play(&self) -> Result> { + pub async fn can_play(&self) -> Result { Ok(self.player_proxy.can_play().await?) } - pub async fn pause(&self) -> Result<(), Box> { + pub async fn pause(&self) -> Result<(), MprisError> { Ok(self.player_proxy.pause().await?) } - pub async fn can_pause(&self) -> Result> { + pub async fn can_pause(&self) -> Result { Ok(self.player_proxy.can_pause().await?) } - pub async fn play_pause(&self) -> Result<(), Box> { + pub async fn play_pause(&self) -> Result<(), MprisError> { Ok(self.player_proxy.play_pause().await?) } - pub async fn stop(&self) -> Result<(), Box> { + pub async fn stop(&self) -> Result<(), MprisError> { Ok(self.player_proxy.stop().await?) } - pub async fn stop_after_current(&self) -> Result<(), Box> { + pub async fn stop_after_current(&self) -> Result<(), MprisError> { Ok(self.player_proxy.stop_after_current().await?) } - pub async fn seek( - &self, - offset_in_microseconds: i64, - ) -> Result<(), Box> { + pub async fn seek(&self, offset_in_microseconds: i64) -> Result<(), MprisError> { Ok(self.player_proxy.seek(offset_in_microseconds).await?) } - pub async fn can_seek(&self) -> Result> { + pub async fn can_seek(&self) -> Result { Ok(self.player_proxy.can_seek().await?) } - pub async fn get_position(&self) -> Result> { + pub async fn get_position(&self) -> Result { Ok(self.player_proxy.position().await?) } - pub async fn set_position( - &self, - track_id: &TrackID, - position: i64, - ) -> Result<(), Box> { + pub async fn set_position(&self, track_id: &TrackID, position: i64) -> Result<(), MprisError> { Ok(self .player_proxy - .set_position(&(**track_id).try_into()?, position) + .set_position(&track_id.get_object_path(), position) .await?) } - pub async fn get_loop_status(&self) -> Result> { - Ok(self.player_proxy.loop_status().await?) + pub async fn get_loop_status(&self) -> Result { + Ok(self.player_proxy.loop_status().await?.parse()?) } - pub async fn set_loop_status(&self, loop_status: &str) -> Result<(), Box> { - Ok(self.player_proxy.set_loop_status(loop_status).await?) + pub async fn set_loop_status(&self, loop_status: LoopStatus) -> Result<(), MprisError> { + Ok(self + .player_proxy + .set_loop_status(loop_status.as_str()) + .await?) } - - pub async fn playback_status(&self) -> Result> { - Ok(self.player_proxy.playback_status().await?) + pub async fn playback_status(&self) -> Result { + Ok(self.player_proxy.playback_status().await?.parse()?) } - - pub async fn open_uri(&self, uri: &str) -> Result<(), Box> { + pub async fn open_uri(&self, uri: &str) -> Result<(), MprisError> { Ok(self.player_proxy.open_uri(uri).await?) } - pub async fn maximum_rate(&self) -> Result> { + pub async fn maximum_rate(&self) -> Result { Ok(self.player_proxy.maximum_rate().await?) } - pub async fn minimum_rate(&self) -> Result> { + pub async fn minimum_rate(&self) -> Result { Ok(self.player_proxy.minimum_rate().await?) } - pub async fn get_playback_rate(&self) -> Result> { + pub async fn get_playback_rate(&self) -> Result { Ok(self.player_proxy.rate().await?) } - pub async fn set_playback_rate(&self, rate: f64) -> Result<(), Box> { + pub async fn set_playback_rate(&self, rate: f64) -> Result<(), MprisError> { Ok(self.player_proxy.set_rate(rate).await?) } - pub async fn get_shuffle(&self) -> Result> { + pub async fn get_shuffle(&self) -> Result { Ok(self.player_proxy.shuffle().await?) } - pub async fn set_shuffle(&self, shuffle: bool) -> Result<(), Box> { + pub async fn set_shuffle(&self, shuffle: bool) -> Result<(), MprisError> { Ok(self.player_proxy.set_shuffle(shuffle).await?) } - pub async fn get_volume(&self) -> Result> { + pub async fn get_volume(&self) -> Result { Ok(self.player_proxy.volume().await?) } - pub async fn set_volume(&self, volume: f64) -> Result<(), Box> { + pub async fn set_volume(&self, volume: f64) -> Result<(), MprisError> { Ok(self.player_proxy.set_volume(volume).await?) } } @@ -230,9 +222,7 @@ impl<'a> std::fmt::Debug for Player<'a> { } } -pub(crate) async fn all( - connection: &Connection, -) -> Result, Box> { +pub(crate) async fn all(connection: &Connection) -> Result, MprisError> { let connection = connection.clone(); let proxy = DBusProxy::new(&connection).await?; let names = proxy.list_names().await?; From 95f4adbe6f643b6e318b43d0b11a1ea2397807ad Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Wed, 2 Oct 2024 22:39:40 +0200 Subject: [PATCH 04/55] Mark properties that don't emit signals on change --- src/proxies.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/proxies.rs b/src/proxies.rs index 445d5c5..0a8e444 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -105,7 +105,7 @@ pub(crate) trait Player { fn seeked(&self, position: i64) -> zbus::Result<()>; /// CanControl property - #[zbus(property)] + #[zbus(property(emits_changed_signal = "const"))] fn can_control(&self) -> zbus::Result; /// CanGoNext property @@ -152,7 +152,7 @@ pub(crate) trait Player { fn playback_status(&self) -> zbus::Result; /// Position property - #[zbus(property)] + #[zbus(property(emits_changed_signal = "const"))] fn position(&self) -> zbus::Result; /// Rate property From f3ae02590a3eebbd6b7a84b8a5025965abf7c168 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Fri, 4 Oct 2024 18:31:30 +0200 Subject: [PATCH 05/55] Remove lifetime from Player The proxies use a owned BusName anyway so there's no need for a lifetime --- examples/metadata.rs | 2 +- src/player.rs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/metadata.rs b/examples/metadata.rs index 5c27ca4..abfa2ba 100644 --- a/examples/metadata.rs +++ b/examples/metadata.rs @@ -17,7 +17,7 @@ async fn main() -> Result<(), MprisError> { Ok(()) } -async fn print_metadata(player: Player<'_>) -> Result<(), MprisError> { +async fn print_metadata(player: Player) -> Result<(), MprisError> { println!( "Player: {} ({})", player.identity().await?, diff --git a/src/player.rs b/src/player.rs index ae66ef6..e822ae0 100644 --- a/src/player.rs +++ b/src/player.rs @@ -11,23 +11,23 @@ use crate::{ pub(crate) const MPRIS2_PREFIX: &str = "org.mpris.MediaPlayer2."; // pub(crate) const MPRIS2_PATH: &str = "/org/mpris/MediaPlayer2"; -pub struct Player<'conn> { - mp2_proxy: MediaPlayer2Proxy<'conn>, - player_proxy: PlayerProxy<'conn>, +pub struct Player { + mp2_proxy: MediaPlayer2Proxy<'static>, + player_proxy: PlayerProxy<'static>, } -impl<'conn> Player<'conn> { +impl Player { pub async fn new( - mpris: &'conn Mpris, + mpris: &Mpris, bus_name: BusName<'static>, - ) -> Result, MprisError> { + ) -> Result { Player::new_from_connection(mpris.connection.clone(), bus_name).await } pub(crate) async fn new_from_connection( connection: Connection, bus_name: BusName<'static>, - ) -> Result, MprisError> { + ) -> Result { let mp2_proxy = MediaPlayer2Proxy::builder(&connection) .destination(bus_name.clone())? .build() @@ -214,7 +214,7 @@ impl<'conn> Player<'conn> { } } -impl<'a> std::fmt::Debug for Player<'a> { +impl std::fmt::Debug for Player { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Player") .field("bus_name", &self.bus_name()) From f9c2996c062f2e67ca4ac6f9410107a090e9b7a0 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sun, 6 Oct 2024 21:33:55 +0200 Subject: [PATCH 06/55] Implement Metadata::is_empty() --- src/metadata/metadata.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/metadata/metadata.rs b/src/metadata/metadata.rs index e0333a3..9202e9b 100644 --- a/src/metadata/metadata.rs +++ b/src/metadata/metadata.rs @@ -29,6 +29,33 @@ pub struct Metadata { pub user_rating: Option, } +impl Metadata { + pub fn is_empty(&self) -> bool { + self.album_artists.is_none() + && self.album_name.is_none() + && self.art_url.is_none() + && self.artists.is_none() + && self.audio_bpm.is_none() + && self.auto_rating.is_none() + && self.comments.is_none() + && self.composers.is_none() + && self.content_created.is_none() + && self.disc_number.is_none() + && self.first_used.is_none() + && self.genres.is_none() + && self.last_used.is_none() + && self.length.is_none() + && self.lyricists.is_none() + && self.lyrics.is_none() + && self.title.is_none() + && self.track_id.is_none() + && self.track_number.is_none() + && self.url.is_none() + && self.url.is_none() + && self.user_rating.is_none() + } +} + macro_rules! extract { ($hash:ident, $key:expr, $f:expr) => { extract(&mut $hash, $key, $f) From 6204af404a9e870558c9b55df05663444114fe45 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sun, 6 Oct 2024 21:40:56 +0200 Subject: [PATCH 07/55] Add finder methods --- examples/list_players.rs | 2 +- examples/metadata.rs | 2 +- src/lib.rs | 92 ++++++++++++++++++++++++++++++++++++++-- src/player.rs | 22 +--------- 4 files changed, 92 insertions(+), 26 deletions(-) diff --git a/examples/list_players.rs b/examples/list_players.rs index 860fef8..0eb0bff 100644 --- a/examples/list_players.rs +++ b/examples/list_players.rs @@ -3,7 +3,7 @@ use mpris::{Mpris, MprisError}; #[async_std::main] async fn main() -> Result<(), MprisError> { let mpris = Mpris::new().await?; - for player in mpris.players().await? { + for player in mpris.all_players().await? { println!("{:?}", player); } Ok(()) diff --git a/examples/metadata.rs b/examples/metadata.rs index abfa2ba..3bcd3f3 100644 --- a/examples/metadata.rs +++ b/examples/metadata.rs @@ -5,7 +5,7 @@ async fn main() -> Result<(), MprisError> { let mpris = Mpris::new().await?; let mut total = 0; - for player in mpris.players().await? { + for player in mpris.all_players().await? { print_metadata(player).await?; total += 1; } diff --git a/src/lib.rs b/src/lib.rs index 805c932..b30a14b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,10 @@ +use crate::proxies::DBusProxy; use std::fmt::Display; -use zbus::Connection; +use zbus::{ + names::{BusName, WellKnownName}, + Connection, +}; mod metadata; mod player; @@ -11,6 +15,8 @@ use metadata::InvalidTrackID; pub use metadata::{Metadata, TrackID}; pub use player::Player; +pub(crate) const MPRIS2_PREFIX: &str = "org.mpris.MediaPlayer2."; + pub struct Mpris { connection: Connection, } @@ -21,8 +27,88 @@ impl Mpris { Ok(Self { connection }) } - pub async fn players(&self) -> Result, MprisError> { - player::all(&self.connection).await + pub fn new_from_connection(connection: Connection) -> Self { + Self { connection } + } + + pub async fn find_first(&self) -> Result, MprisError> { + match self.all_player_bus_names().await?.into_iter().next() { + Some(bus) => Ok(Some( + Player::new_from_connection(self.connection.clone(), bus).await?, + )), + None => Ok(None), + } + } + + pub async fn find_active(&self) -> Result, MprisError> { + let players = self.all_players().await?; + if players.is_empty() { + return Ok(None); + } + + let mut first_paused: Option = None; + let mut first_with_track: Option = None; + let mut first_found: Option = None; + + for player in players { + let player_status = player.playback_status().await?; + + if player_status == PlaybackStatus::Playing { + return Ok(Some(player)); + } + + if first_paused.is_none() && player_status == PlaybackStatus::Paused { + first_paused.replace(player); + } else if first_with_track.is_none() && !player.metadata().await?.is_empty() { + first_with_track.replace(player); + } else if first_found.is_none() { + first_found.replace(player); + } + } + + Ok(first_paused.or(first_with_track).or(first_found)) + } + + pub async fn find_by_name(&self, name: &str) -> Result, MprisError> { + let players = self.all_players().await?; + if players.is_empty() { + return Ok(None); + } + for player in players { + if player.identity().await?.to_lowercase() == name { + return Ok(Some(player)); + } + } + Ok(None) + } + + pub async fn all_players(&self) -> Result, MprisError> { + let bus_names = self.all_player_bus_names().await?; + let mut players = Vec::with_capacity(bus_names.len()); + for player_name in bus_names { + players.push(Player::new_from_connection(self.connection.clone(), player_name).await?); + } + Ok(players) + } + + async fn all_player_bus_names(&self) -> Result>, MprisError> { + let proxy = DBusProxy::new(&self.connection).await?; + let mut names: Vec = proxy + .list_names() + .await? + .into_iter() + .filter(|name| name.starts_with(MPRIS2_PREFIX)) + // We got the bus name from the D-Bus server so unchecked is fine + .map(|name| BusName::from(WellKnownName::from_string_unchecked(name))) + .collect(); + names.sort_unstable_by_key(|n| n.to_lowercase()); + Ok(names) + } +} + +impl From for Mpris { + fn from(value: Connection) -> Self { + Self::new_from_connection(value) } } diff --git a/src/player.rs b/src/player.rs index e822ae0..d62336d 100644 --- a/src/player.rs +++ b/src/player.rs @@ -4,13 +4,10 @@ use zbus::{names::BusName, Connection}; use crate::{ metadata::{MetadataValue, TrackID}, - proxies::{DBusProxy, MediaPlayer2Proxy, PlayerProxy}, + proxies::{MediaPlayer2Proxy, PlayerProxy}, LoopStatus, Metadata, Mpris, MprisError, PlaybackStatus, }; -pub(crate) const MPRIS2_PREFIX: &str = "org.mpris.MediaPlayer2."; -// pub(crate) const MPRIS2_PATH: &str = "/org/mpris/MediaPlayer2"; - pub struct Player { mp2_proxy: MediaPlayer2Proxy<'static>, player_proxy: PlayerProxy<'static>, @@ -221,20 +218,3 @@ impl std::fmt::Debug for Player { .finish() } } - -pub(crate) async fn all(connection: &Connection) -> Result, MprisError> { - let connection = connection.clone(); - let proxy = DBusProxy::new(&connection).await?; - let names = proxy.list_names().await?; - - let mut players = Vec::new(); - for name in names.into_iter() { - if name.starts_with(MPRIS2_PREFIX) { - if let Ok(bus_name) = name.try_into() { - players.push(Player::new_from_connection(connection.clone(), bus_name).await?); - } - } - } - - Ok(players) -} From ed3803d947529f3fbfbcbe9806eaba71b6ea269a Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Mon, 7 Oct 2024 20:50:59 +0200 Subject: [PATCH 08/55] Player improvements Player now holds the BusName directly since the proxies use a Arc anyway. Added `seek_forwards` and `seek_backwards` that take a Duration instead of just an i64. `get_position` and `set_position` now also take a Duration instead of i64 Also added the DurationExt trait for Duration that makes it easy to convert from Duration to a Result --- src/extensions.rs | 39 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 8 ++++++++ src/player.rs | 37 +++++++++++++++++++++++++++---------- src/proxies.rs | 6 ------ 4 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 src/extensions.rs diff --git a/src/extensions.rs b/src/extensions.rs new file mode 100644 index 0000000..7e5ad9b --- /dev/null +++ b/src/extensions.rs @@ -0,0 +1,39 @@ +use std::time::Duration; + +use crate::MprisError; + +pub(crate) trait DurationExt { + /// Tries to convert the Duration as microseconds to a valid i64 + fn convert_to_micro(self) -> Result; +} + +impl DurationExt for Duration { + fn convert_to_micro(self) -> Result { + i64::try_from(self.as_micros()).map_err(|_| { + MprisError::Miscellaneous( + "could not convert Duration into microseconds, Duration too big".to_string(), + ) + }) + } +} + +#[cfg(test)] +mod duration_ext_tests { + use super::*; + + #[test] + fn valid_convert() { + assert_eq!(Duration::default().convert_to_micro(), Ok(0)); + assert_eq!( + Duration::from_micros(i64::MAX as u64).convert_to_micro(), + Ok(i64::MAX) + ) + } + + #[test] + fn invalid_convert() { + assert!(Duration::from_micros(i64::MAX as u64 + 1) + .convert_to_micro() + .is_err()); + } +} diff --git a/src/lib.rs b/src/lib.rs index b30a14b..bd7abc2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use zbus::{ Connection, }; +mod extensions; mod metadata; mod player; mod proxies; @@ -240,12 +241,19 @@ impl From for MprisError { Self::ParseError(value.0) } } + impl From for MprisError { fn from(value: InvalidTrackID) -> Self { Self::ParseError(value.0) } } +impl From for MprisError { + fn from(value: String) -> Self { + Self::Miscellaneous(value) + } +} + #[cfg(test)] mod error_tests { use super::*; diff --git a/src/player.rs b/src/player.rs index d62336d..9cb19e0 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,23 +1,22 @@ -use std::collections::HashMap; +use std::{collections::HashMap, time::Duration}; use zbus::{names::BusName, Connection}; use crate::{ + extensions::DurationExt, metadata::{MetadataValue, TrackID}, proxies::{MediaPlayer2Proxy, PlayerProxy}, LoopStatus, Metadata, Mpris, MprisError, PlaybackStatus, }; pub struct Player { + bus_name: BusName<'static>, mp2_proxy: MediaPlayer2Proxy<'static>, player_proxy: PlayerProxy<'static>, } impl Player { - pub async fn new( - mpris: &Mpris, - bus_name: BusName<'static>, - ) -> Result { + pub async fn new(mpris: &Mpris, bus_name: BusName<'static>) -> Result { Player::new_from_connection(mpris.connection.clone(), bus_name).await } @@ -36,6 +35,7 @@ impl Player { .await?; Ok(Player { + bus_name, mp2_proxy, player_proxy, }) @@ -53,7 +53,7 @@ impl Player { } pub fn bus_name(&self) -> &str { - self.mp2_proxy.bus_name() + self.bus_name.as_str() } pub async fn quit(&self) -> Result<(), MprisError> { @@ -144,18 +144,35 @@ impl Player { Ok(self.player_proxy.seek(offset_in_microseconds).await?) } + pub async fn seek_forwards(&self, offset: Duration) -> Result<(), MprisError> { + Ok(self.player_proxy.seek(offset.convert_to_micro()?).await?) + } + + pub async fn seek_backwards(&self, offset: Duration) -> Result<(), MprisError> { + Ok(self + .player_proxy + .seek(-(offset.convert_to_micro()?)) + .await?) + } + pub async fn can_seek(&self) -> Result { Ok(self.player_proxy.can_seek().await?) } - pub async fn get_position(&self) -> Result { - Ok(self.player_proxy.position().await?) + pub async fn get_position(&self) -> Result { + Ok(Duration::from_micros( + self.player_proxy.position().await? as u64, + )) } - pub async fn set_position(&self, track_id: &TrackID, position: i64) -> Result<(), MprisError> { + pub async fn set_position( + &self, + track_id: &TrackID, + position: Duration, + ) -> Result<(), MprisError> { Ok(self .player_proxy - .set_position(&track_id.get_object_path(), position) + .set_position(&track_id.get_object_path(), position.convert_to_micro()?) .await?) } diff --git a/src/proxies.rs b/src/proxies.rs index 0a8e444..97f6aef 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -54,12 +54,6 @@ pub(crate) trait MediaPlayer2 { fn supported_uri_schemes(&self) -> zbus::Result>; } -impl MediaPlayer2Proxy<'_> { - pub fn bus_name(&self) -> &str { - self.inner().destination().as_str() - } -} - #[proxy( interface = "org.mpris.MediaPlayer2.Player", default_path = "/org/mpris/MediaPlayer2", From 0575ae243e6211af7ba93bed91e05ee088e72880 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Mon, 7 Oct 2024 21:28:45 +0200 Subject: [PATCH 09/55] Move all errors to separate file --- src/errors.rs | 54 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 57 +++------------------------------------- src/metadata.rs | 2 +- src/metadata/track_id.rs | 5 +--- 4 files changed, 60 insertions(+), 58 deletions(-) create mode 100644 src/errors.rs diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..3089c05 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,54 @@ +/// [`PlaybackStatus`][crate::PlaybackStatus] had an invalid string value. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InvalidPlaybackStatus(pub(crate) String); + +/// [`LoopStatus`][crate::LoopStatus] had an invalid string value. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InvalidLoopStatus(pub(crate) String); + +/// [`TrackID`][crate::metadata::TrackID] had an invalid ObjectPath. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InvalidTrackID(pub(crate) String); + +#[derive(Debug, PartialEq, Clone)] +pub enum MprisError { + /// An error occurred while talking to the D-Bus. + DbusError(zbus::Error), + + /// Failed to parse an enum from a string value received from the [`Player`][crate::Player]. + /// This means that the [`Player`][crate::Player] replied with unexpected data. + ParseError(String), + + /// Some other unexpected error occurred. + Miscellaneous(String), +} + +impl From for MprisError { + fn from(value: zbus::Error) -> Self { + MprisError::DbusError(value) + } +} + +impl From for MprisError { + fn from(value: InvalidPlaybackStatus) -> Self { + Self::ParseError(value.0) + } +} + +impl From for MprisError { + fn from(value: InvalidLoopStatus) -> Self { + Self::ParseError(value.0) + } +} + +impl From for MprisError { + fn from(value: InvalidTrackID) -> Self { + Self::ParseError(value.0) + } +} + +impl From for MprisError { + fn from(value: String) -> Self { + Self::Miscellaneous(value) + } +} diff --git a/src/lib.rs b/src/lib.rs index bd7abc2..2f7febe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,13 +6,15 @@ use zbus::{ Connection, }; +pub mod errors; mod extensions; mod metadata; mod player; mod proxies; -use metadata::InvalidTrackID; +use errors::*; +pub use errors::MprisError; pub use metadata::{Metadata, TrackID}; pub use player::Player; @@ -128,10 +130,6 @@ pub enum PlaybackStatus { Stopped, } -/// [`PlaybackStatus`] had an invalid string value. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InvalidPlaybackStatus(String); - impl ::std::str::FromStr for PlaybackStatus { type Err = InvalidPlaybackStatus; @@ -178,10 +176,6 @@ pub enum LoopStatus { Playlist, } -/// [`LoopStatus`] had an invalid string value. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InvalidLoopStatus(String); - impl ::std::str::FromStr for LoopStatus { type Err = InvalidLoopStatus; @@ -211,51 +205,8 @@ impl Display for LoopStatus { } } -#[derive(Debug, PartialEq, Clone)] -pub enum MprisError { - /// An error occurred while talking to the D-Bus. - DbusError(zbus::Error), - - /// Failed to parse an enum from a string value received from the [`Player`]. This means that the - /// [`Player`] replied with unexpected data. - ParseError(String), - - /// Some other unexpected error occurred. - Miscellaneous(String), -} - -impl From for MprisError { - fn from(value: zbus::Error) -> Self { - MprisError::DbusError(value) - } -} - -impl From for MprisError { - fn from(value: InvalidPlaybackStatus) -> Self { - Self::ParseError(value.0) - } -} - -impl From for MprisError { - fn from(value: InvalidLoopStatus) -> Self { - Self::ParseError(value.0) - } -} - -impl From for MprisError { - fn from(value: InvalidTrackID) -> Self { - Self::ParseError(value.0) - } -} - -impl From for MprisError { - fn from(value: String) -> Self { - Self::Miscellaneous(value) - } -} - #[cfg(test)] -mod error_tests { +mod status_enums_tests { use super::*; #[test] diff --git a/src/metadata.rs b/src/metadata.rs index 8cb6099..043c0d0 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -3,5 +3,5 @@ mod track_id; mod values; pub use self::metadata::Metadata; -pub use self::track_id::{InvalidTrackID, TrackID}; +pub use self::track_id::TrackID; pub use self::values::MetadataValue; diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index 89d405a..3a6b1dd 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -3,6 +3,7 @@ use std::ops::Deref; use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value}; use super::MetadataValue; +use crate::errors::InvalidTrackID; const NO_TRACK: &str = "/org/mpris/MediaPlayer2/TrackList/NoTrack"; @@ -35,10 +36,6 @@ impl TrackID { } } -/// [`LoopStatus`] had an invalid string value. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InvalidTrackID(pub(crate) String); - impl TryFrom<&str> for TrackID { type Error = InvalidTrackID; From e4a18da441d898b348c4a9b6f3c9e4995db41df2 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Wed, 9 Oct 2024 21:13:24 +0200 Subject: [PATCH 10/55] Fix `Mpris::find_by_name()` being case sensitive --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 2f7febe..0206e53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,7 +78,7 @@ impl Mpris { return Ok(None); } for player in players { - if player.identity().await?.to_lowercase() == name { + if player.identity().await?.to_lowercase() == name.to_lowercase() { return Ok(Some(player)); } } From 21b084a1fc47908011241c11641ce3cb08162278 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Thu, 10 Oct 2024 18:31:18 +0200 Subject: [PATCH 11/55] Implement Stream over Players PlayerStream just holds a Vec of Futures that return a Player and polls them one by one. The new futures-util dependency is for the Stream trait and the StreamExt trait which makes using streams easier. --- Cargo.toml | 1 + src/lib.rs | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ac2da3c..6cec0ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ serde = ["dep:serde"] [dependencies] zbus = "4.4.0" serde = { version = "1.0.164", optional = true } +futures-util = "0.3.31" # Example dependencies [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index 0206e53..6d9034a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,9 @@ -use crate::proxies::DBusProxy; use std::fmt::Display; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use futures_util::stream::{FusedStream, Stream}; use zbus::{ names::{BusName, WellKnownName}, Connection, @@ -14,12 +17,15 @@ mod proxies; use errors::*; +use crate::proxies::DBusProxy; pub use errors::MprisError; pub use metadata::{Metadata, TrackID}; pub use player::Player; pub(crate) const MPRIS2_PREFIX: &str = "org.mpris.MediaPlayer2."; +type PlayerFuture = Pin> + Send + Sync>>; + pub struct Mpris { connection: Connection, } @@ -107,6 +113,57 @@ impl Mpris { names.sort_unstable_by_key(|n| n.to_lowercase()); Ok(names) } + + pub async fn into_stream(&self) -> Result { + let buses = self.all_player_bus_names().await?; + Ok(PlayerStream::new(&self.connection, buses)) + } +} + +pub struct PlayerStream { + futures: Vec, +} + +impl PlayerStream { + pub fn new(connection: &Connection, buses: Vec>) -> Self { + let mut futures: Vec = Vec::with_capacity(buses.len()); + for fut in buses + .into_iter() + .rev() + .map(|bus_name| Box::pin(Player::new_from_connection(connection.clone(), bus_name))) + { + futures.push(fut); + } + Self { futures } + } +} + +impl Stream for PlayerStream { + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.futures.last_mut() { + Some(last) => match last.as_mut().poll(cx) { + Poll::Ready(result) => { + self.futures.pop(); + Poll::Ready(Some(result)) + } + Poll::Pending => Poll::Pending, + }, + None => Poll::Ready(None), + } + } + + fn size_hint(&self) -> (usize, Option) { + let l = self.futures.len(); + (l, Some(l)) + } +} + +impl FusedStream for PlayerStream { + fn is_terminated(&self) -> bool { + self.futures.is_empty() + } } impl From for Mpris { From 63cc58fb418fc161bed23520b41ba223251efa33 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Thu, 10 Oct 2024 18:34:59 +0200 Subject: [PATCH 12/55] Make Player::new() actually use async Both proxies are now created with join! --- src/player.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/player.rs b/src/player.rs index 9cb19e0..ff8d5d1 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, time::Duration}; +use futures_util::join; use zbus::{names::BusName, Connection}; use crate::{ @@ -24,20 +25,14 @@ impl Player { connection: Connection, bus_name: BusName<'static>, ) -> Result { - let mp2_proxy = MediaPlayer2Proxy::builder(&connection) - .destination(bus_name.clone())? - .build() - .await?; - - let player_proxy = PlayerProxy::builder(&connection) - .destination(bus_name.clone())? - .build() - .await?; - + let (mp2_proxy, player_proxy) = join!( + MediaPlayer2Proxy::new(&connection, bus_name.clone()), + PlayerProxy::new(&connection, bus_name.clone()) + ); Ok(Player { bus_name, - mp2_proxy, - player_proxy, + mp2_proxy: mp2_proxy?, + player_proxy: player_proxy?, }) } From c075f125332cf928d0a0b5759897af9f39f61cb9 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Thu, 10 Oct 2024 21:11:25 +0200 Subject: [PATCH 13/55] Make finder methods use PlayerStream For some finder methods it's more efficient to stop when a matching Player is found. --- src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6d9034a..4b4be8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -use futures_util::stream::{FusedStream, Stream}; +use futures_util::stream::{FusedStream, Stream, TryStreamExt}; use zbus::{ names::{BusName, WellKnownName}, Connection, @@ -50,8 +50,8 @@ impl Mpris { } pub async fn find_active(&self) -> Result, MprisError> { - let players = self.all_players().await?; - if players.is_empty() { + let mut players = self.into_stream().await?; + if players.is_terminated() { return Ok(None); } @@ -59,7 +59,7 @@ impl Mpris { let mut first_with_track: Option = None; let mut first_found: Option = None; - for player in players { + while let Some(player) = players.try_next().await? { let player_status = player.playback_status().await?; if player_status == PlaybackStatus::Playing { @@ -79,11 +79,11 @@ impl Mpris { } pub async fn find_by_name(&self, name: &str) -> Result, MprisError> { - let players = self.all_players().await?; - if players.is_empty() { + let mut players = self.into_stream().await?; + if players.is_terminated() { return Ok(None); } - for player in players { + while let Some(player) = players.try_next().await? { if player.identity().await?.to_lowercase() == name.to_lowercase() { return Ok(Some(player)); } From 0b71005c6ed8e24a7bbe87599c43053157ad01ad Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Thu, 10 Oct 2024 21:12:22 +0200 Subject: [PATCH 14/55] Move DbusProxy into the Mpris struct --- src/lib.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4b4be8c..9ac5014 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,16 +28,26 @@ type PlayerFuture = Pin> + Se pub struct Mpris { connection: Connection, + dbus_proxy: DBusProxy<'static>, } impl Mpris { pub async fn new() -> Result { let connection = Connection::session().await?; - Ok(Self { connection }) + let dbus_proxy = DBusProxy::new(&connection).await?; + + Ok(Self { + connection, + dbus_proxy, + }) } - pub fn new_from_connection(connection: Connection) -> Self { - Self { connection } + pub async fn new_from_connection(connection: Connection) -> Result { + let dbus_proxy = DBusProxy::new(&connection).await?; + Ok(Self { + connection, + dbus_proxy, + }) } pub async fn find_first(&self) -> Result, MprisError> { @@ -101,8 +111,8 @@ impl Mpris { } async fn all_player_bus_names(&self) -> Result>, MprisError> { - let proxy = DBusProxy::new(&self.connection).await?; - let mut names: Vec = proxy + let mut names: Vec = self + .dbus_proxy .list_names() .await? .into_iter() @@ -166,12 +176,6 @@ impl FusedStream for PlayerStream { } } -impl From for Mpris { - fn from(value: Connection) -> Self { - Self::new_from_connection(value) - } -} - #[derive(Debug, PartialEq, Eq, Copy, Clone)] /// The [`Player`]'s playback status /// From 2473fff18ec2768604c77610f61064851cbc97ff Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Mon, 14 Oct 2024 18:20:03 +0200 Subject: [PATCH 15/55] Make slight changes to Player Added `Player::bus_name_trimmed()` which strips the prefix and `Player::is_running()` which uses the D-Bus Ping method to see if the player is still connected. Also renamed `Player::has_track_list()` to `supports_track_list()` --- src/player.rs | 25 +++++++++++++++++++++++-- src/proxies.rs | 16 ++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/player.rs b/src/player.rs index ff8d5d1..190e123 100644 --- a/src/player.rs +++ b/src/player.rs @@ -7,7 +7,7 @@ use crate::{ extensions::DurationExt, metadata::{MetadataValue, TrackID}, proxies::{MediaPlayer2Proxy, PlayerProxy}, - LoopStatus, Metadata, Mpris, MprisError, PlaybackStatus, + LoopStatus, Metadata, Mpris, MprisError, PlaybackStatus, MPRIS2_PREFIX, }; pub struct Player { @@ -47,10 +47,31 @@ impl Player { Ok(raw) } + pub async fn is_running(&self) -> Result { + match self.mp2_proxy.ping().await { + Ok(_) => Ok(true), + Err(e) => { + if let zbus::Error::MethodError(ref err_name, _, _) = e { + if err_name.as_str() == "org.freedesktop.DBus.Error.ServiceUnknown" { + Ok(false) + } else { + Err(e.into()) + } + } else { + Err(e.into()) + } + } + } + } + pub fn bus_name(&self) -> &str { self.bus_name.as_str() } + pub fn bus_name_trimmed(&self) -> &str { + self.bus_name().trim_start_matches(MPRIS2_PREFIX) + } + pub async fn quit(&self) -> Result<(), MprisError> { Ok(self.mp2_proxy.quit().await?) } @@ -71,7 +92,7 @@ impl Player { Ok(self.mp2_proxy.desktop_entry().await?) } - pub async fn has_track_list(&self) -> Result { + pub async fn supports_track_list(&self) -> Result { Ok(self.mp2_proxy.has_track_list().await?) } diff --git a/src/proxies.rs b/src/proxies.rs index 97f6aef..8311872 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -54,6 +54,22 @@ pub(crate) trait MediaPlayer2 { fn supported_uri_schemes(&self) -> zbus::Result>; } +impl MediaPlayer2Proxy<'_> { + pub(crate) async fn ping(&self) -> zbus::Result<()> { + self.inner() + .connection() + .call_method( + Some(self.0.destination()), + self.0.path(), + Some("org.freedesktop.DBus.Peer"), + "Ping", + &(), + ) + .await + .and(Ok(())) + } +} + #[proxy( interface = "org.mpris.MediaPlayer2.Player", default_path = "/org/mpris/MediaPlayer2", From d282ebcc2053e5c8fb681137cb4c45572aa33d32 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Tue, 15 Oct 2024 20:26:50 +0200 Subject: [PATCH 16/55] Implement Display for the errors Created a simple macro that adds the Display trait for the Errors. This is just a placeholder for now. --- src/errors.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 3089c05..1693871 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,15 +1,31 @@ +use std::fmt::Display; + /// [`PlaybackStatus`][crate::PlaybackStatus] had an invalid string value. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct InvalidPlaybackStatus(pub(crate) String); /// [`LoopStatus`][crate::LoopStatus] had an invalid string value. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct InvalidLoopStatus(pub(crate) String); /// [`TrackID`][crate::metadata::TrackID] had an invalid ObjectPath. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct InvalidTrackID(pub(crate) String); +macro_rules! impl_display { + ($error:ty) => { + impl Display for $error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } + } + }; +} + +impl_display!(InvalidPlaybackStatus); +impl_display!(InvalidLoopStatus); +impl_display!(InvalidTrackID); + #[derive(Debug, PartialEq, Clone)] pub enum MprisError { /// An error occurred while talking to the D-Bus. From 9aa7ebd4d16c2e4667af2ee204a04233b7767956 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Tue, 15 Oct 2024 20:39:30 +0200 Subject: [PATCH 17/55] Replace Duration and DurationExt Instead of using `Duration` and working around the type conversions now there's `MprisDuration` which will always be between 0 and i64::MAX because the MPRIS spec returns length and position as a i64 but it can't be negative. `MprisDuration` uses a u64 internally to avoid having to deal with negative numbers when doing math operations. For Serde it acts like a i64 since that's the type in the spec but it can be converted to and from i64, u64 and Duration. --- src/duration.rs | 277 +++++++++++++++++++++++++++++++++++++++ src/errors.rs | 20 +++ src/extensions.rs | 39 ------ src/lib.rs | 3 +- src/metadata/metadata.rs | 8 +- src/player.rs | 28 ++-- 6 files changed, 314 insertions(+), 61 deletions(-) create mode 100644 src/duration.rs delete mode 100644 src/extensions.rs diff --git a/src/duration.rs b/src/duration.rs new file mode 100644 index 0000000..8c00cfa --- /dev/null +++ b/src/duration.rs @@ -0,0 +1,277 @@ +use std::{ + ops::{Add, Div, Mul, Sub}, + time::Duration, +}; + +use crate::{errors::InvalidMprisDuration, metadata::MetadataValue}; + +const MAX: u64 = i64::MAX as u64; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord, Default)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(try_from = "i64", into = "i64") +)] +pub struct MprisDuration(u64); + +impl MprisDuration { + pub fn new_from_u64(value: u64) -> Self { + Self(value.clamp(0, MAX)) + } + + pub fn new_from_i64(value: i64) -> Self { + Self(value.clamp(0, i64::MAX) as u64) + } + + pub fn new_max() -> Self { + Self(MAX) + } +} + +impl From for Duration { + fn from(value: MprisDuration) -> Self { + Duration::from_micros(value.0) + } +} + +impl TryFrom for MprisDuration { + type Error = InvalidMprisDuration; + + fn try_from(value: Duration) -> Result { + u64::try_from(value.as_micros()) + .or(Err(InvalidMprisDuration::new_too_big()))? + .try_into() + } +} + +impl From for i64 { + fn from(value: MprisDuration) -> Self { + value.0 as i64 + } +} + +impl From for u64 { + fn from(value: MprisDuration) -> Self { + value.0 + } +} + +impl TryFrom for MprisDuration { + type Error = InvalidMprisDuration; + + fn try_from(value: i64) -> Result { + if value < 0 { + Err(InvalidMprisDuration::new_negative()) + } else { + Ok(Self(value as u64)) + } + } +} + +impl TryFrom for MprisDuration { + type Error = InvalidMprisDuration; + + fn try_from(value: u64) -> Result { + if value > MAX { + Err(InvalidMprisDuration::new_too_big()) + } else { + Ok(Self(value)) + } + } +} + +impl TryFrom for MprisDuration { + type Error = InvalidMprisDuration; + + fn try_from(value: MetadataValue) -> Result { + match value { + MetadataValue::SignedInt(int) => int.try_into(), + MetadataValue::UnsignedInt(int) => int.try_into(), + _ => Err(InvalidMprisDuration( + "unsupported MetadataValue type".to_string(), + )), + } + } +} + +macro_rules! impl_math { + ($trait:ident, $method:ident, $sat:ident) => { + impl $trait for MprisDuration { + type Output = Self; + + fn $method(self, rhs: u64) -> Self::Output { + Self(self.0.$sat(rhs).clamp(0, MAX)) + } + } + + impl $trait<&u64> for MprisDuration { + type Output = Self; + + fn $method(self, rhs: &u64) -> Self::Output { + Self::$method(self, *rhs) + } + } + }; +} + +impl_math!(Mul, mul, saturating_mul); +// Using regular div because of the current MSRV +// Can you even underflow a u64 with div? +impl_math!(Div, div, div); +impl_math!(Add, add, saturating_add); +impl_math!(Sub, sub, saturating_sub); + +#[cfg(test)] +mod mrpis_duration_tests { + use super::*; + + #[test] + fn new() { + assert_eq!(MprisDuration::new_max(), MprisDuration(MAX)); + assert_eq!(MprisDuration::new_from_u64(0), MprisDuration(0)); + assert_eq!(MprisDuration::default(), MprisDuration(0)); + assert_eq!( + MprisDuration::new_from_u64(u64::MAX), + MprisDuration::new_max() + ); + } + + #[test] + fn into_duration() { + assert_eq!( + Duration::from(MprisDuration::new_from_u64(0)), + Duration::from_micros(0) + ); + assert_eq!( + Duration::from(MprisDuration::new_from_u64(123456789)), + Duration::from_micros(123456789) + ); + assert_eq!( + Duration::from(MprisDuration::new_max()), + Duration::from_micros(MAX) + ); + } + + #[test] + fn try_from_duration() { + assert_eq!( + MprisDuration::try_from(Duration::default()), + Ok(MprisDuration::default()) + ); + assert_eq!( + MprisDuration::try_from(Duration::from_micros(MAX)), + Ok(MprisDuration::new_max()) + ); + + assert!(MprisDuration::try_from(Duration::from_micros(MAX + 1)).is_err()); + } + + #[test] + fn into_ints() { + let d = MprisDuration::default(); + assert_eq!(i64::from(d), 0); + assert_eq!(u64::from(d), 0); + let d_max = MprisDuration::new_max(); + assert_eq!(i64::from(d_max), i64::MAX); + assert_eq!(u64::from(d_max), MAX); + } + + #[test] + fn try_from_ints() { + assert!(MprisDuration::try_from(i64::MIN).is_err()); + assert_eq!(MprisDuration::try_from(0_i64), Ok(MprisDuration::default())); + assert_eq!( + MprisDuration::try_from(i64::MAX), + Ok(MprisDuration::new_max()) + ); + assert_eq!(MprisDuration::try_from(0_u64), Ok(MprisDuration::default())); + assert!(MprisDuration::try_from(MAX + 1).is_err()); + } + + #[test] + fn try_from_metadata_value() { + assert!(MprisDuration::try_from(MetadataValue::Boolean(false)).is_err()); + assert!(MprisDuration::try_from(MetadataValue::Float(0.0)).is_err()); + assert!(MprisDuration::try_from(MetadataValue::SignedInt(0)).is_ok()); + assert!(MprisDuration::try_from(MetadataValue::UnsignedInt(0)).is_ok()); + assert!(MprisDuration::try_from(MetadataValue::String(String::new())).is_err()); + assert!(MprisDuration::try_from(MetadataValue::Strings(vec![])).is_err()); + assert!(MprisDuration::try_from(MetadataValue::Unsupported).is_err()); + } + + #[test] + fn math() { + assert_eq!( + MprisDuration::new_from_u64(1) * 10, + MprisDuration::new_from_u64(10) + ); + #[allow(clippy::erasing_op)] + { + assert_eq!( + MprisDuration::new_from_u64(1) * 0, + MprisDuration::new_from_u64(0) + ); + } + assert_eq!(MprisDuration::new_max() * 2, MprisDuration::new_max()); + + assert_eq!( + MprisDuration::new_from_u64(0) / 1, + MprisDuration::new_from_u64(0) + ); + assert_eq!( + MprisDuration::new_from_u64(10) / 3, + MprisDuration::new_from_u64(10 / 3) + ); + assert_eq!( + MprisDuration::new_max() / MAX, + MprisDuration::new_from_u64(1) + ); + assert_eq!( + MprisDuration::new_from_u64(1) / MAX, + MprisDuration::new_from_u64(0) + ); + + assert_eq!( + MprisDuration::new_from_u64(0) + 1, + MprisDuration::new_from_u64(1) + ); + assert_eq!(MprisDuration::new_max() + 1, MprisDuration::new_max()); + + assert_eq!( + MprisDuration::new_from_u64(0) - 1, + MprisDuration::new_from_u64(0) + ); + assert_eq!( + MprisDuration::new_from_u64(10) - 1, + MprisDuration::new_from_u64(9) + ); + } +} + +#[cfg(all(test, feature = "serde"))] +mod mpris_duration_serde_tests { + use super::*; + use serde_test::{assert_de_tokens_error, assert_tokens, Token}; + + const MIN_TOKENS: [Token; 1] = get_tokens(0); + const MAX_TOKENS: [Token; 1] = get_tokens(MAX as i64); + + const fn get_tokens(x: i64) -> [Token; 1] { + [Token::I64(x)] + } + + #[test] + fn ser_and_deser() { + assert_tokens(&MprisDuration::default(), &MIN_TOKENS); + assert_tokens(&MprisDuration::new_max(), &MAX_TOKENS); + } + + #[test] + fn invalid_deser() { + assert_de_tokens_error::( + &get_tokens(-1), + &InvalidMprisDuration::new_negative().0, + ); + } +} diff --git a/src/errors.rs b/src/errors.rs index 1693871..c3c1177 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -12,6 +12,19 @@ pub struct InvalidLoopStatus(pub(crate) String); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct InvalidTrackID(pub(crate) String); +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InvalidMprisDuration(pub(crate) String); + +impl InvalidMprisDuration { + pub(crate) fn new_too_big() -> Self { + Self("can't create MprisDuration, value too big".to_string()) + } + + pub(crate) fn new_negative() -> Self { + Self("can't create MprisDuration, value is negative".to_string()) + } +} + macro_rules! impl_display { ($error:ty) => { impl Display for $error { @@ -25,6 +38,7 @@ macro_rules! impl_display { impl_display!(InvalidPlaybackStatus); impl_display!(InvalidLoopStatus); impl_display!(InvalidTrackID); +impl_display!(InvalidMprisDuration); #[derive(Debug, PartialEq, Clone)] pub enum MprisError { @@ -63,6 +77,12 @@ impl From for MprisError { } } +impl From for MprisError { + fn from(value: InvalidMprisDuration) -> Self { + Self::ParseError(value.0) + } +} + impl From for MprisError { fn from(value: String) -> Self { Self::Miscellaneous(value) diff --git a/src/extensions.rs b/src/extensions.rs deleted file mode 100644 index 7e5ad9b..0000000 --- a/src/extensions.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::time::Duration; - -use crate::MprisError; - -pub(crate) trait DurationExt { - /// Tries to convert the Duration as microseconds to a valid i64 - fn convert_to_micro(self) -> Result; -} - -impl DurationExt for Duration { - fn convert_to_micro(self) -> Result { - i64::try_from(self.as_micros()).map_err(|_| { - MprisError::Miscellaneous( - "could not convert Duration into microseconds, Duration too big".to_string(), - ) - }) - } -} - -#[cfg(test)] -mod duration_ext_tests { - use super::*; - - #[test] - fn valid_convert() { - assert_eq!(Duration::default().convert_to_micro(), Ok(0)); - assert_eq!( - Duration::from_micros(i64::MAX as u64).convert_to_micro(), - Ok(i64::MAX) - ) - } - - #[test] - fn invalid_convert() { - assert!(Duration::from_micros(i64::MAX as u64 + 1) - .convert_to_micro() - .is_err()); - } -} diff --git a/src/lib.rs b/src/lib.rs index 9ac5014..39065cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,8 +9,8 @@ use zbus::{ Connection, }; +mod duration; pub mod errors; -mod extensions; mod metadata; mod player; mod proxies; @@ -18,6 +18,7 @@ mod proxies; use errors::*; use crate::proxies::DBusProxy; +pub use duration::MprisDuration; pub use errors::MprisError; pub use metadata::{Metadata, TrackID}; pub use player::Player; diff --git a/src/metadata/metadata.rs b/src/metadata/metadata.rs index 9202e9b..c28e02c 100644 --- a/src/metadata/metadata.rs +++ b/src/metadata/metadata.rs @@ -1,6 +1,7 @@ -use std::{collections::HashMap, time::Duration}; +use std::collections::HashMap; use super::{MetadataValue, TrackID}; +use crate::MprisDuration; #[derive(Debug, Default, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -18,7 +19,7 @@ pub struct Metadata { pub first_used: Option, pub genres: Option>, pub last_used: Option, - pub length: Option, + pub length: Option, pub lyricists: Option>, pub lyrics: Option, pub title: Option, @@ -85,8 +86,7 @@ impl From> for Metadata { first_used: extract!(raw, "xesam:firstUsed", MetadataValue::into_string), genres: extract!(raw, "xesam:genre", MetadataValue::into_strings), last_used: extract!(raw, "xesam:lastUsed", MetadataValue::into_string), - length: extract!(raw, "mpris:length", MetadataValue::into_u64) - .map(Duration::from_micros), + length: extract!(raw, "mpris:length", |v| MprisDuration::try_from(v).ok()), lyricists: extract!(raw, "xesam:lyricist", MetadataValue::into_strings), lyrics: extract!(raw, "xesam:asText", MetadataValue::into_string), title: extract!(raw, "xesam:title", MetadataValue::into_nonempty_string), diff --git a/src/player.rs b/src/player.rs index 190e123..5b6ac20 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,13 +1,12 @@ -use std::{collections::HashMap, time::Duration}; +use std::collections::HashMap; use futures_util::join; use zbus::{names::BusName, Connection}; use crate::{ - extensions::DurationExt, - metadata::{MetadataValue, TrackID}, + metadata::MetadataValue, proxies::{MediaPlayer2Proxy, PlayerProxy}, - LoopStatus, Metadata, Mpris, MprisError, PlaybackStatus, MPRIS2_PREFIX, + LoopStatus, Metadata, Mpris, MprisDuration, MprisError, PlaybackStatus, TrackID, MPRIS2_PREFIX, }; pub struct Player { @@ -160,35 +159,30 @@ impl Player { Ok(self.player_proxy.seek(offset_in_microseconds).await?) } - pub async fn seek_forwards(&self, offset: Duration) -> Result<(), MprisError> { - Ok(self.player_proxy.seek(offset.convert_to_micro()?).await?) + pub async fn seek_forwards(&self, offset: MprisDuration) -> Result<(), MprisError> { + Ok(self.player_proxy.seek(offset.into()).await?) } - pub async fn seek_backwards(&self, offset: Duration) -> Result<(), MprisError> { - Ok(self - .player_proxy - .seek(-(offset.convert_to_micro()?)) - .await?) + pub async fn seek_backwards(&self, offset: MprisDuration) -> Result<(), MprisError> { + Ok(self.player_proxy.seek(-i64::from(offset)).await?) } pub async fn can_seek(&self) -> Result { Ok(self.player_proxy.can_seek().await?) } - pub async fn get_position(&self) -> Result { - Ok(Duration::from_micros( - self.player_proxy.position().await? as u64, - )) + pub async fn get_position(&self) -> Result { + Ok(self.player_proxy.position().await?.try_into()?) } pub async fn set_position( &self, track_id: &TrackID, - position: Duration, + position: MprisDuration, ) -> Result<(), MprisError> { Ok(self .player_proxy - .set_position(&track_id.get_object_path(), position.convert_to_micro()?) + .set_position(&track_id.get_object_path(), position.into()) .await?) } From 5bb67f0a2973c9aa91743f452b615cb04f589458 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Wed, 16 Oct 2024 23:08:08 +0200 Subject: [PATCH 18/55] Fix clippy complaints --- src/metadata/values.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/metadata/values.rs b/src/metadata/values.rs index 6a9d9ec..d7235f4 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -6,7 +6,7 @@ use zbus::zvariant::Value; * * See https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ */ -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum MetadataValue { Boolean(bool), @@ -35,7 +35,7 @@ impl MetadataValue { pub fn into_i64(self) -> Option { match self { MetadataValue::SignedInt(i) => Some(i), - MetadataValue::UnsignedInt(i) => Some(0i64.saturating_add_unsigned(i)), + MetadataValue::UnsignedInt(i) => Some(i.clamp(0, i64::MAX as u64) as i64), _ => None, } } @@ -72,10 +72,10 @@ impl<'a> From> for MetadataValue { Value::Bool(v) => MetadataValue::Boolean(v), Value::I16(v) => MetadataValue::SignedInt(v as i64), Value::I32(v) => MetadataValue::SignedInt(v as i64), - Value::I64(v) => MetadataValue::SignedInt(v as i64), + Value::I64(v) => MetadataValue::SignedInt(v), Value::U16(v) => MetadataValue::UnsignedInt(v as u64), Value::U32(v) => MetadataValue::UnsignedInt(v as u64), - Value::U64(v) => MetadataValue::UnsignedInt(v as u64), + Value::U64(v) => MetadataValue::UnsignedInt(v), Value::U8(v) => MetadataValue::UnsignedInt(v as u64), Value::F64(v) => MetadataValue::Float(v), @@ -86,7 +86,7 @@ impl<'a> From> for MetadataValue { Value::Array(a) if a.full_signature() == "as" => { let mut strings = Vec::with_capacity(a.len()); - for v in a.into_iter() { + for v in a.iter() { if let Value::Str(s) = v { strings.push(s.to_string()); } From 134b28db7848521da4576183ce437527bc89e32d Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Wed, 16 Oct 2024 23:10:40 +0200 Subject: [PATCH 19/55] Implement conversion traits for MetadataValue Added `From for MetadataValue` and `TryFrom for T` for all of the variants of MetadataValue. --- src/errors.rs | 4 ++ src/metadata/values.rs | 129 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/src/errors.rs b/src/errors.rs index c3c1177..82e50c6 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -25,6 +25,9 @@ impl InvalidMprisDuration { } } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InvalidMetadataValue(pub(crate) String); + macro_rules! impl_display { ($error:ty) => { impl Display for $error { @@ -39,6 +42,7 @@ impl_display!(InvalidPlaybackStatus); impl_display!(InvalidLoopStatus); impl_display!(InvalidTrackID); impl_display!(InvalidMprisDuration); +impl_display!(InvalidMetadataValue); #[derive(Debug, PartialEq, Clone)] pub enum MprisError { diff --git a/src/metadata/values.rs b/src/metadata/values.rs index d7235f4..1f6d1b2 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -1,5 +1,7 @@ use zbus::zvariant::Value; +use crate::errors::InvalidMetadataValue; + /* * Subset of DBus data types that are commonly used in MPRIS metadata, and a boolean variant as it * seems likely to be used in some custom metadata. @@ -104,6 +106,133 @@ impl<'a> From> for MetadataValue { } } +impl From for MetadataValue { + fn from(value: bool) -> Self { + Self::Boolean(value) + } +} + +impl From for MetadataValue { + fn from(value: f64) -> Self { + Self::Float(value) + } +} + +impl From for MetadataValue { + fn from(value: i64) -> Self { + Self::SignedInt(value) + } +} + +impl From for MetadataValue { + fn from(value: u64) -> Self { + Self::UnsignedInt(value) + } +} + +impl From for MetadataValue { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl From> for MetadataValue { + fn from(value: Vec) -> Self { + Self::Strings(value) + } +} + +impl From for MetadataValue { + fn from(value: super::TrackID) -> Self { + Self::String(value.into()) + } +} + +impl From for MetadataValue { + fn from(value: crate::MprisDuration) -> Self { + Self::SignedInt(value.into()) + } +} + +impl TryFrom for bool { + type Error = InvalidMetadataValue; + + fn try_from(value: MetadataValue) -> Result { + match value { + MetadataValue::Boolean(v) => Ok(v), + _ => Err(InvalidMetadataValue( + "expected MetadataValue::Boolean".to_string(), + )), + } + } +} + +impl TryFrom for f64 { + type Error = InvalidMetadataValue; + + fn try_from(value: MetadataValue) -> Result { + match value { + MetadataValue::Float(v) => Ok(v), + _ => Err(InvalidMetadataValue( + "expected MetadataValue::Float".to_string(), + )), + } + } +} + +impl TryFrom for i64 { + type Error = InvalidMetadataValue; + + fn try_from(value: MetadataValue) -> Result { + match value { + MetadataValue::SignedInt(v) => Ok(v), + _ => Err(InvalidMetadataValue( + "expected MetadataValue::SignedInt".to_string(), + )), + } + } +} + +impl TryFrom for u64 { + type Error = InvalidMetadataValue; + + fn try_from(value: MetadataValue) -> Result { + match value { + MetadataValue::UnsignedInt(v) => Ok(v), + _ => Err(InvalidMetadataValue( + "expected MetadataValue::UnsignedInt".to_string(), + )), + } + } +} + +impl TryFrom for String { + type Error = InvalidMetadataValue; + + fn try_from(value: MetadataValue) -> Result { + match value { + MetadataValue::String(v) => Ok(v), + _ => Err(InvalidMetadataValue( + "expected MetadataValue::String".to_string(), + )), + } + } +} + +impl TryFrom for Vec { + type Error = InvalidMetadataValue; + + fn try_from(value: MetadataValue) -> Result { + match value { + MetadataValue::String(v) => Ok(vec![v]), + MetadataValue::Strings(v) => Ok(v), + _ => Err(InvalidMetadataValue( + "expected MetadataValue::Strings or MetadataValue::String".to_string(), + )), + } + } +} + #[test] fn test_signed_integer_casting() { assert_eq!(MetadataValue::SignedInt(42).into_i64(), Some(42)); From 9b0f940b277e3cbf4e0e2d64e641a2281cf54da4 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Wed, 16 Oct 2024 23:29:56 +0200 Subject: [PATCH 20/55] Implement From String for errors `From` and `From<&str>` have been added in the display macro for errors. --- src/duration.rs | 4 +--- src/errors.rs | 12 ++++++++++++ src/lib.rs | 16 ++++++++-------- src/metadata/track_id.rs | 4 ++-- src/metadata/values.rs | 24 ++++++++++-------------- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/duration.rs b/src/duration.rs index 8c00cfa..3b24730 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -88,9 +88,7 @@ impl TryFrom for MprisDuration { match value { MetadataValue::SignedInt(int) => int.try_into(), MetadataValue::UnsignedInt(int) => int.try_into(), - _ => Err(InvalidMprisDuration( - "unsupported MetadataValue type".to_string(), - )), + _ => Err(InvalidMprisDuration::from("unsupported MetadataValue type")), } } } diff --git a/src/errors.rs b/src/errors.rs index 82e50c6..bb228b8 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -35,6 +35,18 @@ macro_rules! impl_display { write!(f, "{}", self.0) } } + + impl From for $error { + fn from(value: String) -> Self { + Self(value) + } + } + + impl From<&str> for $error { + fn from(value: &str) -> Self { + Self(value.to_string()) + } + } }; } diff --git a/src/lib.rs b/src/lib.rs index 39065cb..aa52be5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,7 +200,7 @@ impl ::std::str::FromStr for PlaybackStatus { "Playing" => Ok(Self::Playing), "Paused" => Ok(Self::Paused), "Stopped" => Ok(Self::Stopped), - _ => Err(InvalidPlaybackStatus(string.to_owned())), + _ => Err(InvalidPlaybackStatus::from(string)), } } } @@ -246,7 +246,7 @@ impl ::std::str::FromStr for LoopStatus { "None" => Ok(LoopStatus::None), "Track" => Ok(LoopStatus::Track), "Playlist" => Ok(LoopStatus::Playlist), - _ => Err(InvalidLoopStatus(string.to_owned())), + _ => Err(InvalidLoopStatus::from(string)), } } } @@ -282,15 +282,15 @@ mod status_enums_tests { fn invalid_playback_status() { assert_eq!( "".parse::(), - Err(InvalidPlaybackStatus("".into())) + Err(InvalidPlaybackStatus::from("")) ); assert_eq!( "playing".parse::(), - Err(InvalidPlaybackStatus("playing".into())) + Err(InvalidPlaybackStatus::from("playing")) ); assert_eq!( "wrong".parse::(), - Err(InvalidPlaybackStatus("wrong".into())) + Err(InvalidPlaybackStatus::from("wrong")) ); } @@ -310,14 +310,14 @@ mod status_enums_tests { #[test] fn invalid_loop_status() { - assert_eq!("".parse::(), Err(InvalidLoopStatus("".into()))); + assert_eq!("".parse::(), Err(InvalidLoopStatus::from(""))); assert_eq!( "track".parse::(), - Err(InvalidLoopStatus("track".into())) + Err(InvalidLoopStatus::from("track")) ); assert_eq!( "wrong".parse::(), - Err(InvalidLoopStatus("wrong".into())) + Err(InvalidLoopStatus::from("wrong")) ); } diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index 3a6b1dd..c7fb801 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -100,7 +100,7 @@ impl TryFrom<&OwnedValue> for TrackID { match value.deref() { Value::Str(s) => s.as_str().try_into(), Value::ObjectPath(path) => Ok(Self(path.to_string())), - _ => Err(InvalidTrackID(String::from("not a String or ObjectPath"))), + _ => Err(InvalidTrackID::from("not a String or ObjectPath")), } } } @@ -112,7 +112,7 @@ impl<'a> TryFrom> for TrackID { match value { Value::Str(s) => s.as_str().try_into(), Value::ObjectPath(path) => Ok(Self(path.to_string())), - _ => Err(InvalidTrackID(String::from("not a String or ObjectPath"))), + _ => Err(InvalidTrackID::from("not a String or ObjectPath")), } } } diff --git a/src/metadata/values.rs b/src/metadata/values.rs index 1f6d1b2..b115657 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -160,8 +160,8 @@ impl TryFrom for bool { fn try_from(value: MetadataValue) -> Result { match value { MetadataValue::Boolean(v) => Ok(v), - _ => Err(InvalidMetadataValue( - "expected MetadataValue::Boolean".to_string(), + _ => Err(InvalidMetadataValue::from( + "expected MetadataValue::Boolean", )), } } @@ -173,9 +173,7 @@ impl TryFrom for f64 { fn try_from(value: MetadataValue) -> Result { match value { MetadataValue::Float(v) => Ok(v), - _ => Err(InvalidMetadataValue( - "expected MetadataValue::Float".to_string(), - )), + _ => Err(InvalidMetadataValue::from("expected MetadataValue::Float")), } } } @@ -186,8 +184,8 @@ impl TryFrom for i64 { fn try_from(value: MetadataValue) -> Result { match value { MetadataValue::SignedInt(v) => Ok(v), - _ => Err(InvalidMetadataValue( - "expected MetadataValue::SignedInt".to_string(), + _ => Err(InvalidMetadataValue::from( + "expected MetadataValue::SignedInt", )), } } @@ -199,8 +197,8 @@ impl TryFrom for u64 { fn try_from(value: MetadataValue) -> Result { match value { MetadataValue::UnsignedInt(v) => Ok(v), - _ => Err(InvalidMetadataValue( - "expected MetadataValue::UnsignedInt".to_string(), + _ => Err(InvalidMetadataValue::from( + "expected MetadataValue::UnsignedInt", )), } } @@ -212,9 +210,7 @@ impl TryFrom for String { fn try_from(value: MetadataValue) -> Result { match value { MetadataValue::String(v) => Ok(v), - _ => Err(InvalidMetadataValue( - "expected MetadataValue::String".to_string(), - )), + _ => Err(InvalidMetadataValue::from("expected MetadataValue::String")), } } } @@ -226,8 +222,8 @@ impl TryFrom for Vec { match value { MetadataValue::String(v) => Ok(vec![v]), MetadataValue::Strings(v) => Ok(v), - _ => Err(InvalidMetadataValue( - "expected MetadataValue::Strings or MetadataValue::String".to_string(), + _ => Err(InvalidMetadataValue::from( + "expected MetadataValue::Strings or MetadataValue::String", )), } } From 96685422b71ec8116660464d5a45140ac846f053 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Fri, 18 Oct 2024 22:09:17 +0200 Subject: [PATCH 21/55] Use raw_medatada instead of the struct No point in wasting time converting it since we don't actually care what the data is --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index aa52be5..b59f6ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,7 +79,7 @@ impl Mpris { if first_paused.is_none() && player_status == PlaybackStatus::Paused { first_paused.replace(player); - } else if first_with_track.is_none() && !player.metadata().await?.is_empty() { + } else if first_with_track.is_none() && !player.raw_metadata().await?.is_empty() { first_with_track.replace(player); } else if first_found.is_none() { first_found.replace(player); From 0a8db93db45e55e198ad019f8b897dcf0ff25f68 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Fri, 18 Oct 2024 22:16:56 +0200 Subject: [PATCH 22/55] Improve MetadataValue conversions The integer types will now try to covert to the other integer type without losing any data when using the `TryFrom` trait. `TryFrom` for String will now also work for `MetadataValue::Strings` if there's only one String inside. --- src/metadata/values.rs | 114 +++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 33 deletions(-) diff --git a/src/metadata/values.rs b/src/metadata/values.rs index b115657..4860003 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -184,8 +184,15 @@ impl TryFrom for i64 { fn try_from(value: MetadataValue) -> Result { match value { MetadataValue::SignedInt(v) => Ok(v), + MetadataValue::UnsignedInt(v) => { + if v <= i64::MAX as u64 { + Ok(v as i64) + } else { + Err(InvalidMetadataValue::from("value too big for i64")) + } + } _ => Err(InvalidMetadataValue::from( - "expected MetadataValue::SignedInt", + "expected MetadataValue::SignedInt or MetadataValue::UnsignedInt", )), } } @@ -197,8 +204,15 @@ impl TryFrom for u64 { fn try_from(value: MetadataValue) -> Result { match value { MetadataValue::UnsignedInt(v) => Ok(v), + MetadataValue::SignedInt(v) => { + if v >= 0 { + Ok(v as u64) + } else { + Err(InvalidMetadataValue::from("value is negative")) + } + } _ => Err(InvalidMetadataValue::from( - "expected MetadataValue::UnsignedInt", + "expected MetadataValue::SignedInt or MetadataValue::UnsignedInt", )), } } @@ -210,7 +224,18 @@ impl TryFrom for String { fn try_from(value: MetadataValue) -> Result { match value { MetadataValue::String(v) => Ok(v), - _ => Err(InvalidMetadataValue::from("expected MetadataValue::String")), + MetadataValue::Strings(mut v) => { + if v.len() == 1 { + Ok(v.pop().unwrap()) + } else { + Err(InvalidMetadataValue::from( + "MetadataValue::Strings contains more than 1 String", + )) + } + } + _ => Err(InvalidMetadataValue::from( + "expected MetadataValue::Strings or MetadataValue::String", + )), } } } @@ -229,35 +254,58 @@ impl TryFrom for Vec { } } -#[test] -fn test_signed_integer_casting() { - assert_eq!(MetadataValue::SignedInt(42).into_i64(), Some(42)); - assert_eq!(MetadataValue::SignedInt(-42).into_i64(), Some(-42)); - assert_eq!(MetadataValue::UnsignedInt(42).into_i64(), Some(42)); - assert_eq!(MetadataValue::Boolean(true).into_i64(), None); - - assert_eq!( - MetadataValue::UnsignedInt(u64::MAX).into_i64(), - Some(i64::MAX) - ); -} - -#[test] -fn test_unsigned_integer_casting() { - assert_eq!(MetadataValue::SignedInt(42).into_u64(), Some(42)); - assert_eq!(MetadataValue::SignedInt(-42).into_u64(), Some(0)); - assert_eq!(MetadataValue::UnsignedInt(42).into_u64(), Some(42)); - assert_eq!(MetadataValue::Boolean(true).into_u64(), None); - - assert_eq!( - MetadataValue::SignedInt(i64::MAX).into_u64(), - Some(i64::MAX as u64) - ); - - assert_eq!(MetadataValue::SignedInt(i64::MIN).into_u64(), Some(0)); +#[cfg(test)] +mod metadata_value_integer_tests { + use super::*; + + #[test] + fn test_signed_integer_casting() { + assert_eq!( + MetadataValue::SignedInt(i64::MIN).into_i64(), + Some(i64::MIN) + ); + assert_eq!(MetadataValue::SignedInt(0).into_i64(), Some(0_i64)); + assert_eq!( + MetadataValue::SignedInt(i64::MAX).into_i64(), + Some(i64::MAX) + ); + assert_eq!(MetadataValue::UnsignedInt(0).into_i64(), Some(0_i64)); + assert_eq!( + MetadataValue::UnsignedInt(u64::MAX).into_i64(), + Some(i64::MAX) + ); + + assert_eq!(MetadataValue::SignedInt(i64::MIN).try_into(), Ok(i64::MIN)); + assert_eq!(MetadataValue::SignedInt(0_i64).try_into(), Ok(0_i64)); + assert_eq!(MetadataValue::SignedInt(i64::MAX).try_into(), Ok(i64::MAX)); + assert_eq!(MetadataValue::UnsignedInt(0).try_into(), Ok(0_i64)); + assert!(i64::try_from(MetadataValue::UnsignedInt(u64::MAX)).is_err()); + } - assert_eq!( - MetadataValue::UnsignedInt(u64::MAX).into_u64(), - Some(u64::MAX) - ); + #[test] + fn test_unsigned_integer_casting() { + assert_eq!(MetadataValue::SignedInt(i64::MIN).into_u64(), Some(0_u64)); + assert_eq!(MetadataValue::SignedInt(0).into_u64(), Some(0_u64)); + assert_eq!( + MetadataValue::SignedInt(i64::MAX).into_u64(), + Some(i64::MAX as u64) + ); + assert_eq!(MetadataValue::UnsignedInt(0).into_u64(), Some(0_u64)); + assert_eq!( + MetadataValue::UnsignedInt(u64::MAX).into_u64(), + Some(u64::MAX) + ); + + assert!(u64::try_from(MetadataValue::SignedInt(i64::MIN)).is_err()); + assert_eq!(MetadataValue::SignedInt(0).try_into(), Ok(0_u64)); + assert_eq!( + MetadataValue::SignedInt(i64::MAX).try_into(), + Ok(i64::MAX as u64) + ); + assert_eq!(MetadataValue::UnsignedInt(0).try_into(), Ok(0_u64)); + assert_eq!( + MetadataValue::UnsignedInt(u64::MAX).try_into(), + Ok(u64::MAX) + ); + } } From 29d8ee4b43658027f878282090291baaf9badeff Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sat, 19 Oct 2024 18:26:33 +0200 Subject: [PATCH 23/55] Rewrite the Metadata struct The whole struct is now generated through the `gen_metadata_struct` macro. This is done to avoid having to copy and paste the fields and keys around while also making it easy to change the fields. The macro generates a couple of methods and traits for the struct which are documented on the macro itself. Additionally `From` has been changed to `TryFrom` because if there's a problem with the type the conversion should fail instead of silently changing the values. --- src/errors.rs | 10 + src/lib.rs | 2 +- src/metadata.rs | 2 +- src/metadata/metadata.rs | 433 +++++++++++++++++++++++++++++++-------- src/player.rs | 2 +- 5 files changed, 360 insertions(+), 89 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index bb228b8..05bb02e 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -28,6 +28,9 @@ impl InvalidMprisDuration { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct InvalidMetadataValue(pub(crate) String); +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InvalidMetadata(pub(crate) String); + macro_rules! impl_display { ($error:ty) => { impl Display for $error { @@ -55,6 +58,7 @@ impl_display!(InvalidLoopStatus); impl_display!(InvalidTrackID); impl_display!(InvalidMprisDuration); impl_display!(InvalidMetadataValue); +impl_display!(InvalidMetadata); #[derive(Debug, PartialEq, Clone)] pub enum MprisError { @@ -99,6 +103,12 @@ impl From for MprisError { } } +impl From for MprisError { + fn from(value: InvalidMetadata) -> Self { + Self::ParseError(value.0) + } +} + impl From for MprisError { fn from(value: String) -> Self { Self::Miscellaneous(value) diff --git a/src/lib.rs b/src/lib.rs index b59f6ca..cb63640 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ use errors::*; use crate::proxies::DBusProxy; pub use duration::MprisDuration; pub use errors::MprisError; -pub use metadata::{Metadata, TrackID}; +pub use metadata::{Metadata, MetadataIter, MetadataValue, TrackID}; pub use player::Player; pub(crate) const MPRIS2_PREFIX: &str = "org.mpris.MediaPlayer2."; diff --git a/src/metadata.rs b/src/metadata.rs index 043c0d0..5c55901 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -2,6 +2,6 @@ mod metadata; mod track_id; mod values; -pub use self::metadata::Metadata; +pub use self::metadata::{Metadata, MetadataIter}; pub use self::track_id::TrackID; pub use self::values::MetadataValue; diff --git a/src/metadata/metadata.rs b/src/metadata/metadata.rs index c28e02c..f4e88dc 100644 --- a/src/metadata/metadata.rs +++ b/src/metadata/metadata.rs @@ -1,100 +1,361 @@ -use std::collections::HashMap; +use std::{collections::HashMap, iter::FusedIterator}; use super::{MetadataValue, TrackID}; -use crate::MprisDuration; - -#[derive(Debug, Default, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Metadata { - pub album_artists: Option>, - pub album_name: Option, - pub art_url: Option, - pub artists: Option>, - pub audio_bpm: Option, - pub auto_rating: Option, - pub comments: Option>, - pub composers: Option>, - pub content_created: Option, - pub disc_number: Option, - pub first_used: Option, - pub genres: Option>, - pub last_used: Option, - pub length: Option, - pub lyricists: Option>, - pub lyrics: Option, - pub title: Option, - pub track_id: Option, - pub track_number: Option, - pub url: Option, - pub use_count: Option, - pub user_rating: Option, +use crate::{errors::InvalidMetadata, MprisDuration}; + +type RawMetadata = HashMap; + +/// Macro that auto implements useful things for Metadata without needing to repeat the fields every time +/// while preserving documentation for the fields +/// +/// The generated things are: +/// - `Metadata::new()` +/// - `Metadata::is_empty()` +/// - `Metadata::get_metadata_key()` which lets you get the key for a given field as a str +/// - `TryFrom> for Metadata` +/// - `Metadata::from_raw_lossy()` which is similar to the above but wrong types just get discarded +/// - `From for HashMap` +/// - `IntoIterator for Metadata` +/// +/// The macro expects a structure like this +/// ```text +/// #[derive(Debug, Clone, Default, PartialEq)] +/// struct Example { +/// "key" => key_field: Vec, +/// "prefix:otherKey" => other_key_field: String, +/// [...] +/// "last_key" => last_key_field: f64, +/// field_name_for_hashmap, +/// } +/// ``` +macro_rules! gen_metadata_struct { + ($(#[$struct_meta:meta])* + struct $name:ident { + $($(#[$field_meta:meta])* + $key:literal => $field:ident : $type:ty),*, $others_name:ident $(,)? + }) => { + + // Creates the actual struct + $(#[$struct_meta])* + pub struct $name { + $( + $(#[$field_meta])* + #[doc=""] + #[doc=stringify!($key)] + pub $field: Option<$type> + ),*, + pub $others_name: RawMetadata, + } + + impl $name { + pub fn new() -> Self { + Self { + $($field: None),*, + $others_name: HashMap::new(), + } + } + + pub fn is_empty(&self) -> bool { + $(self.$field.is_none())&&* + && self.$others_name.is_empty() + } + + pub fn get_metadata_key(&self, field: &str) -> Option<&str> { + match field { + $(stringify!($field) => Some($key)),*, + _ => None + } + } + + pub fn from_raw_lossy(mut raw: RawMetadata) -> Self { + Self { + $($field: raw.remove($key).and_then(|v| <$type>::try_from(v).ok())),*, + $others_name: raw + } + } + } + + impl IntoIterator for $name { + type Item = (String, Option); + type IntoIter = MetadataIter; + + fn into_iter(mut self) -> Self::IntoIter { + // Turns the fields into Vec<&'static str, Option> with they key as the str + let fields = vec![ + $(($key, self.$field.take().map(MetadataValue::from))),* + ]; + MetadataIter::new(fields, self.$others_name) + } + } + + // From for HashMap + // Simply adds the fields to the HashMap using the specified key + impl From<$name> for RawMetadata { + fn from(mut value: $name) -> Self { + let mut map = value.$others_name; + $(if let Some(v) = value.$field.take() { + map.insert(String::from($key), MetadataValue::from(v)); + })* + map + } + } + + // TryFrom> for Metadata + // Removes the given key from the HashMap tries to turn it into the target type. + // Fails if MetadataValue is of the wrong type for the field or if mpris:trackid" is missing + impl TryFrom for $name { + type Error = InvalidMetadata; + fn try_from(mut raw: RawMetadata) -> Result { + if raw.is_empty() { + return Ok(Self::new()); + } else if !raw.contains_key("mpris:trackid") { + return Err(InvalidMetadata::from("metadata doesn't contain the mpris:trackid key")); + } + + Ok(Self { + $( + $field: { + match raw.remove($key).map(<$type>::try_from) { + Some(v) => Some(v.map_err(|e| InvalidMetadata::from(format!("{} for {}", e.0, $key)))?), + None => None, + } + } + ),*, + $others_name: raw + }) + } + } +}} + +gen_metadata_struct!( + #[derive(Debug, Clone, Default, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + struct Metadata { + "xesam:albumArtist" => album_artists: Vec, + "xesam:album" => album_name: String, + "mpris:artUrl" => art_url: String, + "xesam:artist" => artists: Vec, + "xesam:audioBPM" => audio_bpm: u64, + "xesam:autoRating" => auto_rating: f64, + "xesam:comment" => comments: Vec, + "xesam:composer" => composers: Vec, + "xesam:contentCreated" => content_created: String, + "xesam:discNumber" => disc_number: u64, + "xesam:firstUsed" => first_used: String, + "xesam:genre" => genres: Vec, + "xesam:lastUsed" => last_used: String, + "mpris:length" => length: MprisDuration, + "xesam:lyricist" => lyricists: Vec, + "xesam:asText" => lyrics: String, + "xesam:title" => title: String, + "mpris:trackid" => track_id: TrackID, + "xesam:trackNumber" => track_number: u64, + "xesam:url" => url: String, + "xesam:useCount" => use_count: u64, + "xesam:userRating" => user_rating: f64, + others, + } +); + +#[derive(Debug)] +pub struct MetadataIter { + values: std::vec::IntoIter<(&'static str, Option)>, + map: std::collections::hash_map::IntoIter, } -impl Metadata { - pub fn is_empty(&self) -> bool { - self.album_artists.is_none() - && self.album_name.is_none() - && self.art_url.is_none() - && self.artists.is_none() - && self.audio_bpm.is_none() - && self.auto_rating.is_none() - && self.comments.is_none() - && self.composers.is_none() - && self.content_created.is_none() - && self.disc_number.is_none() - && self.first_used.is_none() - && self.genres.is_none() - && self.last_used.is_none() - && self.length.is_none() - && self.lyricists.is_none() - && self.lyrics.is_none() - && self.title.is_none() - && self.track_id.is_none() - && self.track_number.is_none() - && self.url.is_none() - && self.url.is_none() - && self.user_rating.is_none() +impl MetadataIter { + fn new(fields: Vec<(&'static str, Option)>, map: RawMetadata) -> Self { + Self { + values: fields.into_iter(), + map: map.into_iter(), + } } } -macro_rules! extract { - ($hash:ident, $key:expr, $f:expr) => { - extract(&mut $hash, $key, $f) - }; +impl Iterator for MetadataIter { + type Item = (String, Option); + + fn next(&mut self) -> Option { + match self.values.next() { + Some((k, v)) => Some((k.to_string(), v)), + None => self.map.next().map(|(k, v)| (k, Some(v))), + } + } + + fn size_hint(&self) -> (usize, Option) { + let l = self.values.len() + self.map.len(); + (l, Some(l)) + } } -fn extract(raw: &mut HashMap, key: &str, f: F) -> Option -where - F: FnOnce(MetadataValue) -> Option, -{ - raw.remove(key).and_then(f) +impl ExactSizeIterator for MetadataIter {} +impl FusedIterator for MetadataIter {} + +#[cfg(test)] +mod metadata_tests { + use super::*; + + #[test] + fn empty_new_default() { + let empty = Metadata { + album_artists: None, + album_name: None, + art_url: None, + artists: None, + audio_bpm: None, + auto_rating: None, + comments: None, + composers: None, + content_created: None, + disc_number: None, + first_used: None, + genres: None, + last_used: None, + length: None, + lyricists: None, + lyrics: None, + title: None, + track_id: None, + track_number: None, + url: None, + use_count: None, + user_rating: None, + others: HashMap::new(), + }; + assert_eq!(empty, Metadata::default()); + assert_eq!(empty, Metadata::new()) + } + + #[test] + fn is_empty() { + let mut m = Metadata::new(); + assert!(m.is_empty()); + + let mut field = m.clone(); + field.disc_number = Some(0); + assert!(!field.is_empty()); + + m.others + .insert("test".to_string(), MetadataValue::Boolean(false)); + assert!(!m.is_empty()); + + m.others.remove("test"); + assert!(m.is_empty()); + } + + #[test] + fn default_back_and_forth() { + let original = Metadata::new(); + assert_eq!( + Metadata::try_from(RawMetadata::from(original.clone())), + Ok(original) + ) + } + + #[test] + fn try_from_raw() { + let raw_metadata: RawMetadata = HashMap::from_iter([ + ("xesam:albumArtist".to_string(), vec![String::new()].into()), + ("xesam:album".to_string(), String::new().into()), + ("mpris:artUrl".to_string(), String::new().into()), + ("xesam:artist".to_string(), vec![String::new()].into()), + ("xesam:audioBPM".to_string(), 0_i64.into()), + ("xesam:autoRating".to_string(), 0.0.into()), + ("xesam:comment".to_string(), vec![String::new()].into()), + ("xesam:composer".to_string(), vec![String::new()].into()), + ("xesam:contentCreated".to_string(), String::new().into()), + ("xesam:discNumber".to_string(), 0_i64.into()), + ("xesam:firstUsed".to_string(), String::new().into()), + ("xesam:genre".to_string(), vec![String::new()].into()), + ("xesam:lastUsed".to_string(), String::new().into()), + ("mpris:length".to_string(), MprisDuration::default().into()), + ("xesam:lyricist".to_string(), vec![String::new()].into()), + ("xesam:asText".to_string(), String::new().into()), + ("xesam:title".to_string(), String::new().into()), + ("mpris:trackid".to_string(), TrackID::no_track().into()), + ("xesam:trackNumber".to_string(), 0_i64.into()), + ("xesam:url".to_string(), String::new().into()), + ("xesam:useCount".to_string(), 0_i64.into()), + ("xesam:userRating".to_string(), 0.0.into()), + ("other".to_string(), MetadataValue::Unsupported), + ]); + let meta = Metadata::try_from(raw_metadata); + let manual_meta = Metadata { + album_artists: Some(vec![String::new()]), + album_name: Some(String::new()), + art_url: Some(String::new()), + artists: Some(vec![String::new()]), + audio_bpm: Some(0), + auto_rating: Some(0.0), + comments: Some(vec![String::new()]), + composers: Some(vec![String::new()]), + content_created: Some(String::new()), + disc_number: Some(0), + first_used: Some(String::new()), + genres: Some(vec![String::new()]), + last_used: Some(String::new()), + length: Some(MprisDuration::default()), + lyricists: Some(vec![String::new()]), + lyrics: Some(String::new()), + title: Some(String::new()), + track_id: Some(TrackID::no_track()), + track_number: Some(0), + url: Some(String::new()), + use_count: Some(0), + user_rating: Some(0.0), + others: HashMap::from_iter([(String::from("other"), MetadataValue::Unsupported)]), + }; + + assert_eq!(meta, Ok(manual_meta)); + } + + #[test] + fn try_from_raw_fail() { + let mut map = HashMap::new(); + + // Wrong type + map.insert("xesam:autoRating".to_string(), true.into()); + let m = Metadata::try_from(map.clone()); + assert!(m.is_err()); + + // Correct type but no TrackID + map.insert("xesam:autoRating".to_string(), 0.0.into()); + let m = Metadata::try_from(map.clone()); + assert!(m.is_err()); + + map.insert("mpris:trackid".to_string(), TrackID::no_track().into()); + let m = Metadata::try_from(map); + assert!(m.is_ok()); + } + + #[test] + fn equality() { + let mut first = Metadata::new(); + first.auto_rating = Some(0.0); + first.others.insert(String::from("test"), true.into()); + + let mut second = Metadata::new(); + second.auto_rating = Some(0.0); + assert_ne!(first, second.clone()); + + second.others.insert(String::from("test"), true.into()); + assert_eq!(first, second); + } } -impl From> for Metadata { - fn from(mut raw: HashMap) -> Self { - Metadata { - album_artists: extract!(raw, "xesam:albumArtist", MetadataValue::into_strings), - album_name: extract!(raw, "xesam:album", MetadataValue::into_nonempty_string), - art_url: extract!(raw, "mpris:artUrl", MetadataValue::into_nonempty_string), - artists: extract!(raw, "xesam:artist", MetadataValue::into_strings), - audio_bpm: extract!(raw, "xesam:audioBPM", MetadataValue::into_u64), - auto_rating: extract!(raw, "xesam:autoRating", MetadataValue::into_float), - comments: extract!(raw, "xesam:comment", MetadataValue::into_strings), - composers: extract!(raw, "xesam:composer", MetadataValue::into_strings), - content_created: extract!(raw, "xesam:contentCreated", MetadataValue::into_string), - disc_number: extract!(raw, "xesam:discNumber", MetadataValue::into_u64), - first_used: extract!(raw, "xesam:firstUsed", MetadataValue::into_string), - genres: extract!(raw, "xesam:genre", MetadataValue::into_strings), - last_used: extract!(raw, "xesam:lastUsed", MetadataValue::into_string), - length: extract!(raw, "mpris:length", |v| MprisDuration::try_from(v).ok()), - lyricists: extract!(raw, "xesam:lyricist", MetadataValue::into_strings), - lyrics: extract!(raw, "xesam:asText", MetadataValue::into_string), - title: extract!(raw, "xesam:title", MetadataValue::into_nonempty_string), - track_id: extract!(raw, "mpris:trackid", |v| TrackID::try_from(v).ok()), - track_number: extract!(raw, "xesam:trackNumber", MetadataValue::into_u64), - url: extract!(raw, "xesam:url", MetadataValue::into_nonempty_string), - use_count: extract!(raw, "xesam:useCount", MetadataValue::into_u64), - user_rating: extract!(raw, "xesam:userRating", MetadataValue::into_float), +#[cfg(test)] +mod metadata_iterator_tests { + use super::*; + + #[test] + fn empty() { + let iter = Metadata::new().into_iter(); + let (left, right) = iter.size_hint(); + assert_eq!(Some(left), right); + assert_eq!(left, 22); + + for (_, v) in iter { + assert!(v.is_none()); } } } diff --git a/src/player.rs b/src/player.rs index 5b6ac20..85498c8 100644 --- a/src/player.rs +++ b/src/player.rs @@ -36,7 +36,7 @@ impl Player { } pub async fn metadata(&self) -> Result { - Ok(self.raw_metadata().await?.into()) + Ok(self.raw_metadata().await?.try_into()?) } pub async fn raw_metadata(&self) -> Result, MprisError> { From 60a4d3e4f8ef8800e827ebe18cfdd74ad1d6e866 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sat, 19 Oct 2024 18:47:44 +0200 Subject: [PATCH 24/55] Remove unneeded MprisValue methods Most of them are just `try_from().ok()` anyway --- src/metadata/values.rs | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/src/metadata/values.rs b/src/metadata/values.rs index 4860003..cba8396 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -21,16 +21,9 @@ pub enum MetadataValue { } impl MetadataValue { - pub fn into_string(self) -> Option { - if let MetadataValue::String(s) = self { - Some(s) - } else { - None - } - } - pub fn into_nonempty_string(self) -> Option { - self.into_string() + String::try_from(self) + .ok() .and_then(|s| if s.is_empty() { None } else { Some(s) }) } @@ -50,22 +43,6 @@ impl MetadataValue { _ => None, } } - - pub fn into_float(self) -> Option { - if let MetadataValue::Float(f) = self { - Some(f) - } else { - None - } - } - - pub fn into_strings(self) -> Option> { - match self { - MetadataValue::Strings(v) => Some(v), - MetadataValue::String(s) => Some(vec![s]), - _ => None, - } - } } impl<'a> From> for MetadataValue { From 939e85f65bb5c634fc3d8ed1a7425947386e980a Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sat, 19 Oct 2024 18:59:36 +0200 Subject: [PATCH 25/55] More TrackID tests and make method public --- src/metadata/track_id.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index c7fb801..77dc1d1 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -30,7 +30,7 @@ impl TrackID { self.0.as_str() } - pub(crate) fn get_object_path(&self) -> ObjectPath { + pub fn get_object_path(&self) -> ObjectPath { // Safe because we checked the string at creation ObjectPath::from_str_unchecked(&self.0) } @@ -168,6 +168,35 @@ impl From for String { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn no_track() { + let track = TrackID::no_track(); + let manual = TrackID(NO_TRACK.into()); + + assert!(track.is_no_track()); + assert!(manual.is_no_track()); + assert_eq!(track, manual); + } + + #[test] + fn valid_track_id() { + assert!(TrackID::try_from("/").is_ok()); + assert!(TrackID::try_from("/some/path").is_ok()); + } + + #[test] + fn invalid_track_id() { + assert!(TrackID::try_from("").is_err()); + assert!(TrackID::try_from("//some/path").is_err()); + assert!(TrackID::try_from("/some.path").is_err()); + assert!(TrackID::try_from("path").is_err()); + } +} + #[cfg(all(test, feature = "serde"))] mod serde_tests { use super::*; From 91d5d4ee32885449e88698cd462f4d7f9502717c Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Wed, 23 Oct 2024 20:52:30 +0200 Subject: [PATCH 26/55] Implement Playlists interface --- src/errors.rs | 28 ++++- src/lib.rs | 6 +- src/player.rs | 87 +++++++++++-- src/playlist.rs | 316 ++++++++++++++++++++++++++++++++++++++++++++++++ src/proxies.rs | 42 ++++++- 5 files changed, 458 insertions(+), 21 deletions(-) create mode 100644 src/playlist.rs diff --git a/src/errors.rs b/src/errors.rs index 05bb02e..e668699 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -31,6 +31,12 @@ pub struct InvalidMetadataValue(pub(crate) String); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct InvalidMetadata(pub(crate) String); +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InvalidPlaylist(pub(crate) String); + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InvalidPlaylistOrdering(pub(crate) String); + macro_rules! impl_display { ($error:ty) => { impl Display for $error { @@ -59,6 +65,8 @@ impl_display!(InvalidTrackID); impl_display!(InvalidMprisDuration); impl_display!(InvalidMetadataValue); impl_display!(InvalidMetadata); +impl_display!(InvalidPlaylist); +impl_display!(InvalidPlaylistOrdering); #[derive(Debug, PartialEq, Clone)] pub enum MprisError { @@ -69,13 +77,19 @@ pub enum MprisError { /// This means that the [`Player`][crate::Player] replied with unexpected data. ParseError(String), + /// The player doesn't implement the required interface/method/signal + Unsupported, + /// Some other unexpected error occurred. Miscellaneous(String), } impl From for MprisError { fn from(value: zbus::Error) -> Self { - MprisError::DbusError(value) + match value { + zbus::Error::InterfaceNotFound | zbus::Error::Unsupported => Self::Unsupported, + _ => todo!(), + } } } @@ -109,6 +123,18 @@ impl From for MprisError { } } +impl From for MprisError { + fn from(value: InvalidPlaylistOrdering) -> Self { + Self::ParseError(value.0) + } +} + +impl From for MprisError { + fn from(value: InvalidPlaylist) -> Self { + Self::ParseError(value.0) + } +} + impl From for MprisError { fn from(value: String) -> Self { Self::Miscellaneous(value) diff --git a/src/lib.rs b/src/lib.rs index cb63640..2412547 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ mod duration; pub mod errors; mod metadata; mod player; +mod playlist; mod proxies; use errors::*; @@ -22,10 +23,11 @@ pub use duration::MprisDuration; pub use errors::MprisError; pub use metadata::{Metadata, MetadataIter, MetadataValue, TrackID}; pub use player::Player; +pub use playlist::{Playlist, PlaylistOrdering}; pub(crate) const MPRIS2_PREFIX: &str = "org.mpris.MediaPlayer2."; -type PlayerFuture = Pin> + Send + Sync>>; +type PlayerFuture = Pin> + Send>>; pub struct Mpris { connection: Connection, @@ -177,7 +179,7 @@ impl FusedStream for PlayerStream { } } -#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] /// The [`Player`]'s playback status /// /// See: [MPRIS2 specification about `PlaybackStatus`][playback_status] diff --git a/src/player.rs b/src/player.rs index 85498c8..64e77b3 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,18 +1,20 @@ use std::collections::HashMap; -use futures_util::join; +use futures_util::try_join; use zbus::{names::BusName, Connection}; use crate::{ metadata::MetadataValue, - proxies::{MediaPlayer2Proxy, PlayerProxy}, - LoopStatus, Metadata, Mpris, MprisDuration, MprisError, PlaybackStatus, TrackID, MPRIS2_PREFIX, + proxies::{MediaPlayer2Proxy, PlayerProxy, PlaylistsProxy}, + LoopStatus, Metadata, Mpris, MprisDuration, MprisError, PlaybackStatus, Playlist, + PlaylistOrdering, TrackID, MPRIS2_PREFIX, }; pub struct Player { bus_name: BusName<'static>, mp2_proxy: MediaPlayer2Proxy<'static>, player_proxy: PlayerProxy<'static>, + playlist_proxy: Option>, } impl Player { @@ -24,17 +26,29 @@ impl Player { connection: Connection, bus_name: BusName<'static>, ) -> Result { - let (mp2_proxy, player_proxy) = join!( + let (mp2_proxy, player_proxy, playlist_proxy) = try_join!( MediaPlayer2Proxy::new(&connection, bus_name.clone()), - PlayerProxy::new(&connection, bus_name.clone()) - ); + PlayerProxy::new(&connection, bus_name.clone()), + PlaylistsProxy::new(&connection, bus_name.clone()), + )?; + + let playlist = playlist_proxy.playlist_count().await.is_ok(); Ok(Player { bus_name, - mp2_proxy: mp2_proxy?, - player_proxy: player_proxy?, + mp2_proxy, + player_proxy, + playlist_proxy: if playlist { Some(playlist_proxy) } else { None }, }) } + pub async fn supports_track_list(&self) -> Result { + Ok(self.mp2_proxy.has_track_list().await?) + } + + pub fn supports_playlist_interface(&self) -> bool { + self.playlist_proxy.is_some() + } + pub async fn metadata(&self) -> Result { Ok(self.raw_metadata().await?.try_into()?) } @@ -91,10 +105,6 @@ impl Player { Ok(self.mp2_proxy.desktop_entry().await?) } - pub async fn supports_track_list(&self) -> Result { - Ok(self.mp2_proxy.has_track_list().await?) - } - pub async fn identity(&self) -> Result { Ok(self.mp2_proxy.identity().await?) } @@ -236,12 +246,65 @@ impl Player { pub async fn set_volume(&self, volume: f64) -> Result<(), MprisError> { Ok(self.player_proxy.set_volume(volume).await?) } + + fn check_playlist_support(&self) -> Result<&PlaylistsProxy, MprisError> { + match &self.playlist_proxy { + Some(proxy) => Ok(proxy), + None => Err(MprisError::Unsupported), + } + } + + pub async fn activate_playlist(&self, playlist: &Playlist) -> Result<(), MprisError> { + Ok(self + .check_playlist_support()? + .activate_playlist(&playlist.get_id()) + .await?) + } + + pub async fn get_playlists( + &self, + start_index: u32, + max_count: u32, + order: PlaylistOrdering, + reverse_order: bool, + ) -> Result, MprisError> { + Ok(self + .check_playlist_support()? + .get_playlists(start_index, max_count, order.as_str_value(), reverse_order) + .await? + .into_iter() + .map(Playlist::from) + .collect()) + } + + pub async fn active_playlist(&self) -> Result, MprisError> { + let result = self.check_playlist_support()?.active_playlist().await?; + if result.0 { + Ok(Some(Playlist::from(result.1))) + } else { + Ok(None) + } + } + + pub async fn orderings(&self) -> Result, MprisError> { + let result = self.check_playlist_support()?.orderings().await?; + let mut orderings = Vec::with_capacity(result.len()); + for s in result { + orderings.push(s.parse()?); + } + Ok(orderings) + } + + pub async fn playlist_count(&self) -> Result { + Ok(self.check_playlist_support()?.playlist_count().await?) + } } impl std::fmt::Debug for Player { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Player") .field("bus_name", &self.bus_name()) + .field("playlist_proxy", &self.playlist_proxy.is_some()) .finish() } } diff --git a/src/playlist.rs b/src/playlist.rs new file mode 100644 index 0000000..6105ff7 --- /dev/null +++ b/src/playlist.rs @@ -0,0 +1,316 @@ +#[cfg(feature = "serde")] +use serde::Serializer; +use zbus::zvariant::{ObjectPath, OwnedObjectPath}; + +use crate::{InvalidPlaylist, InvalidPlaylistOrdering}; + +#[derive(Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Playlist { + #[cfg_attr( + feature = "serde", + serde(serialize_with = "serialize_owned_object_path") + )] + id: OwnedObjectPath, + name: String, + #[cfg_attr(feature = "serde", serde(default))] + icon: Option, +} + +impl Playlist { + pub fn new(id: String, name: String, icon: Option) -> Result { + match OwnedObjectPath::try_from(id) { + Ok(o) => Ok(Self { id: o, name, icon }), + Err(e) => Err(InvalidPlaylist::from(e.to_string())), + } + } + + pub fn new_from_object_path(id: OwnedObjectPath, name: String, icon: Option) -> Self { + Self { id, name, icon } + } + + pub fn get_name(&self) -> &str { + &self.name + } + + pub fn get_icon(&self) -> Option<&str> { + self.icon.as_deref() + } + + pub fn get_id(&self) -> ObjectPath { + self.id.as_ref() + } + + pub fn get_id_as_str(&self) -> &str { + self.id.as_str() + } +} + +impl std::fmt::Debug for Playlist { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Playlist") + .field("id", &self.get_id_as_str()) + .field("name", &self.name) + .field("icon", &self.icon) + .finish() + } +} + +#[cfg(feature = "serde")] +pub(crate) fn serialize_owned_object_path( + object: &OwnedObjectPath, + ser: S, +) -> Result +where + S: Serializer, +{ + ser.serialize_str(object.as_str()) +} + +impl From<(OwnedObjectPath, String, String)> for Playlist { + fn from(value: (OwnedObjectPath, String, String)) -> Self { + let icon = if value.2.is_empty() { + None + } else { + Some(value.2) + }; + Self { + id: value.0, + name: value.1, + icon, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// Specifies the ordering of returned playlists +pub enum PlaylistOrdering { + /// Alphabetical ordering by name, ascending. + Alphabetical, /* Alphabetical */ + + /// Ordering by creation date, oldest first. + CreationDate, /* Created */ + + /// Ordering by last modified date, oldest first. + ModifiedDate, /* Modified */ + + ///Ordering by date of last playback, oldest first. + LastPlayDate, /* Played */ + + /// A user-defined ordering. + /// + /// Some media players may allow users to order playlists as they wish. This ordering allows playlists to be retreived in that order. + UserDefined, /* User */ +} + +impl PlaylistOrdering { + pub fn as_str_value(&self) -> &str { + match self { + PlaylistOrdering::Alphabetical => "Alphabetical", + PlaylistOrdering::CreationDate => "Created", + PlaylistOrdering::ModifiedDate => "Modified", + PlaylistOrdering::LastPlayDate => "Played", + PlaylistOrdering::UserDefined => "User", + } + } +} + +impl std::fmt::Display for PlaylistOrdering { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + PlaylistOrdering::Alphabetical => "Alphabetical", + PlaylistOrdering::CreationDate => "CreationDate", + PlaylistOrdering::ModifiedDate => "ModifiedDate", + PlaylistOrdering::LastPlayDate => "LastPlayDate", + PlaylistOrdering::UserDefined => "UserDefined", + } + ) + } +} + +impl std::str::FromStr for PlaylistOrdering { + type Err = InvalidPlaylistOrdering; + + fn from_str(s: &str) -> Result { + match s { + "Alphabetical" => Ok(Self::Alphabetical), + "Created" => Ok(Self::CreationDate), + "Modified" => Ok(Self::ModifiedDate), + "Played" => Ok(Self::LastPlayDate), + "User" => Ok(Self::UserDefined), + _ => Err(InvalidPlaylistOrdering::from( + r#"expected "Alphabetical", "Created", "Modified", "Played" or "User""#, + )), + } + } +} + +#[cfg(test)] +mod playlist_ordering_tests { + use super::*; + + #[test] + fn parsing() { + assert_eq!("Alphabetical".parse(), Ok(PlaylistOrdering::Alphabetical)); + assert_eq!("Created".parse(), Ok(PlaylistOrdering::CreationDate)); + assert_eq!("Modified".parse(), Ok(PlaylistOrdering::ModifiedDate)); + assert_eq!("Played".parse(), Ok(PlaylistOrdering::LastPlayDate)); + assert_eq!("User".parse(), Ok(PlaylistOrdering::UserDefined)); + + assert!("alphabetical".parse::().is_err()); + assert!("created".parse::().is_err()); + assert!("modified".parse::().is_err()); + assert!("played".parse::().is_err()); + assert!("user".parse::().is_err()); + assert!("wrong".parse::().is_err()); + assert!("".parse::().is_err()) + } + + #[test] + fn as_str() { + assert_eq!( + PlaylistOrdering::Alphabetical.as_str_value(), + "Alphabetical" + ); + assert_eq!(PlaylistOrdering::CreationDate.as_str_value(), "Created"); + assert_eq!(PlaylistOrdering::ModifiedDate.as_str_value(), "Modified"); + assert_eq!(PlaylistOrdering::LastPlayDate.as_str_value(), "Played"); + assert_eq!(PlaylistOrdering::UserDefined.as_str_value(), "User"); + } + + #[test] + fn disaply() { + assert_eq!(&PlaylistOrdering::Alphabetical.to_string(), "Alphabetical"); + assert_eq!(&PlaylistOrdering::CreationDate.to_string(), "CreationDate"); + assert_eq!(&PlaylistOrdering::LastPlayDate.to_string(), "LastPlayDate"); + assert_eq!(&PlaylistOrdering::UserDefined.to_string(), "UserDefined"); + + } +} + +#[cfg(test)] +mod playlist_tests { + use super::*; + + #[test] + fn new() { + let manual = Playlist { + id: ObjectPath::from_string_unchecked(String::from("/valid/path")).into(), + name: String::from("TestName"), + icon: Some(String::from("TestIcon")), + }; + let new = Playlist::new( + String::from("/valid/path"), + String::from("TestName"), + Some(String::from("TestIcon")), + ); + assert_eq!(new, Ok(manual)); + } + + #[test] + fn gets() { + let mut new = Playlist::new_from_object_path( + ObjectPath::from_string_unchecked(String::from("/valid/path")).into(), + String::from("TestName"), + Some(String::from("TestIcon")), + ); + assert_eq!(new.get_name(), "TestName"); + assert_eq!(new.get_icon(), Some("TestIcon")); + assert_eq!(new.get_id(), ObjectPath::from_str_unchecked("/valid/path")); + assert_eq!(new.get_id_as_str(), "/valid/path"); + + new.icon = None; + assert_eq!(new.get_icon(), None); + } +} + +#[cfg(all(test, feature = "serde"))] +mod playlist_serde_tests { + use super::*; + use serde_test::{assert_de_tokens, assert_de_tokens_error, assert_tokens, Token}; + + #[test] + fn serialization() { + let mut playlist = Playlist::new_from_object_path( + ObjectPath::from_string_unchecked(String::from("/valid/path")).into(), + String::from("TestName"), + Some(String::from("TestIcon")), + ); + assert_tokens( + &playlist, + &[ + Token::Struct { + name: "Playlist", + len: 3, + }, + Token::Str("id"), + Token::String("/valid/path"), + Token::Str("name"), + Token::String("TestName"), + Token::Str("icon"), + Token::Some, + Token::String("TestIcon"), + Token::StructEnd, + ], + ); + + playlist.icon = None; + assert_tokens( + &playlist, + &[ + Token::Struct { + name: "Playlist", + len: 3, + }, + Token::Str("id"), + Token::String("/valid/path"), + Token::Str("name"), + Token::String("TestName"), + Token::Str("icon"), + Token::None, + Token::StructEnd, + ], + ); + } + + #[test] + fn deser_default() { + let playlist = Playlist::new_from_object_path( + ObjectPath::from_str_unchecked("/valid/path").into(), + String::from("TestName"), + None, + ); + assert_de_tokens( + &playlist, + &[ + Token::Struct { + name: "Playlist", + len: 3, + }, + Token::Str("id"), + Token::String("/valid/path"), + Token::Str("name"), + Token::String("TestName"), + Token::StructEnd, + ], + ); + } + + #[test] + fn deser_invalid_path() { + assert_de_tokens_error::( + &[ + Token::Struct { + name: "Playlist", + len: 3, + }, + Token::Str("id"), + Token::String("invalid/path"), + ], + "invalid value: character `i`, expected /", + ); + } +} diff --git a/src/proxies.rs b/src/proxies.rs index 8311872..82faa3e 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use zbus::proxy; -use zbus::zvariant::Value; +use zbus::zvariant::{ObjectPath, OwnedObjectPath, Value}; #[proxy( default_service = "org.freedesktop.DBus", @@ -98,11 +98,7 @@ pub(crate) trait Player { fn seek(&self, offset: i64) -> zbus::Result<()>; /// SetPosition method - fn set_position( - &self, - track_id: &zbus::zvariant::ObjectPath<'_>, - position: i64, - ) -> zbus::Result<()>; + fn set_position(&self, track_id: &ObjectPath<'_>, position: i64) -> zbus::Result<()>; /// Stop method fn stop(&self) -> zbus::Result<()>; @@ -186,3 +182,37 @@ pub(crate) trait Player { #[zbus(property)] fn set_volume(&self, value: f64) -> zbus::Result<()>; } + +#[proxy( + interface = "org.mpris.MediaPlayer2.Playlists", + default_path = "/org/mpris/MediaPlayer2", + gen_blocking = false +)] +pub(crate) trait Playlists { + /// ActivatePlaylist method + fn activate_playlist(&self, playlist_id: &ObjectPath<'_>) -> zbus::Result<()>; + + /// GetPlaylists method + fn get_playlists( + &self, + index: u32, + max_count: u32, + order: &str, + reverse_order: bool, + ) -> zbus::Result>; + + #[zbus(signal)] + fn playlist_changed(&self) -> zbus::Result>; + + /// ActivePlaylist property + #[zbus(property)] + fn active_playlist(&self) -> zbus::Result<(bool, (OwnedObjectPath, String, String))>; + + /// Orderings property + #[zbus(property)] + fn orderings(&self) -> zbus::Result>; + + /// PlaylistCount property + #[zbus(property)] + fn playlist_count(&self) -> zbus::Result; +} From 6470e95e70257b081a6c075acef8d4423d290b16 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Thu, 24 Oct 2024 17:01:04 +0200 Subject: [PATCH 27/55] Update TrackID Switched TrackID to holding `OwnedObjectPath` directly and changed the conversion traits. All of them now check for the "/org/mpris" prefix and you can no longer convert from borrowed types (outside of &str). And more tests --- src/metadata/track_id.rs | 209 +++++++++++++++++++++++++++++---------- src/player.rs | 2 +- src/playlist.rs | 1 - 3 files changed, 158 insertions(+), 54 deletions(-) diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index 77dc1d1..bc7449b 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -7,10 +7,10 @@ use crate::errors::InvalidTrackID; const NO_TRACK: &str = "/org/mpris/MediaPlayer2/TrackList/NoTrack"; -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -pub struct TrackID(String); +#[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))] +pub struct TrackID(OwnedObjectPath); impl TrackID { pub fn new(id: String) -> Result { @@ -19,7 +19,9 @@ impl TrackID { pub fn no_track() -> Self { // We know it's a valid path so it's safe to skip the check - Self(NO_TRACK.into()) + Self(OwnedObjectPath::from( + ObjectPath::from_static_str_unchecked(NO_TRACK), + )) } pub fn is_no_track(&self) -> bool { @@ -30,9 +32,33 @@ impl TrackID { self.0.as_str() } - pub fn get_object_path(&self) -> ObjectPath { - // Safe because we checked the string at creation - ObjectPath::from_str_unchecked(&self.0) + pub fn as_object_path(&self) -> ObjectPath { + self.0.as_ref() + } +} + +fn check_start(s: T) -> Result +where + T: Deref, +{ + if s.starts_with("/org/mpris") && s.deref() != NO_TRACK { + Err(InvalidTrackID::from( + "TrackID can't start with \"/org/mpris\"", + )) + } else { + Ok(s) + } +} + +impl Ord for TrackID { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.as_str().cmp(other.as_str()) + } +} + +impl PartialOrd for TrackID { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } } @@ -40,8 +66,8 @@ impl TryFrom<&str> for TrackID { type Error = InvalidTrackID; fn try_from(value: &str) -> Result { - match ObjectPath::try_from(value) { - Ok(_) => Ok(Self(value.to_string())), + match OwnedObjectPath::try_from(check_start(value)?) { + Ok(o) => Ok(Self(o)), Err(e) => { if let zbus::zvariant::Error::Message(s) = e { Err(InvalidTrackID(s)) @@ -59,8 +85,8 @@ impl TryFrom for TrackID { type Error = InvalidTrackID; fn try_from(value: String) -> Result { - match ObjectPath::try_from(value.as_str()) { - Ok(_) => Ok(Self(value)), + match OwnedObjectPath::try_from(check_start(value)?) { + Ok(o) => Ok(Self(o)), Err(e) => { if let zbus::zvariant::Error::Message(s) = e { Err(InvalidTrackID(s)) @@ -80,6 +106,9 @@ impl TryFrom for TrackID { fn try_from(value: MetadataValue) -> Result { match value { MetadataValue::String(s) => s.try_into(), + MetadataValue::Strings(mut s) if s.len() == 1 => { + s.pop().expect("length should be 1").try_into() + } _ => Err(InvalidTrackID(String::from("not a string"))), } } @@ -89,55 +118,38 @@ impl TryFrom for TrackID { type Error = InvalidTrackID; fn try_from(value: OwnedValue) -> Result { - Self::try_from(&value) - } -} - -impl TryFrom<&OwnedValue> for TrackID { - type Error = InvalidTrackID; - - fn try_from(value: &OwnedValue) -> Result { - match value.deref() { - Value::Str(s) => s.as_str().try_into(), - Value::ObjectPath(path) => Ok(Self(path.to_string())), - _ => Err(InvalidTrackID::from("not a String or ObjectPath")), - } + Self::try_from(Value::from(value)) } } -impl<'a> TryFrom> for TrackID { +impl TryFrom> for TrackID { type Error = InvalidTrackID; fn try_from(value: Value) -> Result { match value { - Value::Str(s) => s.as_str().try_into(), - Value::ObjectPath(path) => Ok(Self(path.to_string())), + Value::Str(s) => Self::try_from(s.as_str()), + Value::ObjectPath(path) => Self::try_from(path), _ => Err(InvalidTrackID::from("not a String or ObjectPath")), } } } -impl From for TrackID { - fn from(value: OwnedObjectPath) -> Self { - Self(value.to_string()) - } -} +impl TryFrom for TrackID { + type Error = InvalidTrackID; -impl From<&OwnedObjectPath> for TrackID { - fn from(value: &OwnedObjectPath) -> Self { - Self(value.to_string()) + fn try_from(value: OwnedObjectPath) -> Result { + match check_start(value.as_str()) { + Ok(_) => Ok(Self(value)), + Err(e) => Err(e), + } } } -impl From> for TrackID { - fn from(value: ObjectPath) -> Self { - Self(value.to_string()) - } -} +impl TryFrom> for TrackID { + type Error = InvalidTrackID; -impl From<&ObjectPath<'_>> for TrackID { - fn from(value: &ObjectPath) -> Self { - Self(value.to_string()) + fn try_from(value: ObjectPath) -> Result { + Ok(Self(check_start(value)?.into())) } } @@ -150,21 +162,20 @@ impl Deref for TrackID { } impl From for ObjectPath<'_> { - fn from(val: TrackID) -> Self { - // We used a ObjectPath when creating TrackID so it's safe to skip the check - ObjectPath::from_string_unchecked(val.0) + fn from(value: TrackID) -> Self { + value.0.into_inner() } } impl From for OwnedObjectPath { - fn from(val: TrackID) -> Self { - OwnedObjectPath::from(ObjectPath::from(val)) + fn from(value: TrackID) -> Self { + value.0 } } impl From for String { fn from(value: TrackID) -> Self { - value.0 + value.0.to_string() } } @@ -175,17 +186,52 @@ mod tests { #[test] fn no_track() { let track = TrackID::no_track(); - let manual = TrackID(NO_TRACK.into()); + let manual = TrackID(OwnedObjectPath::from( + ObjectPath::from_static_str_unchecked(NO_TRACK), + )); assert!(track.is_no_track()); assert!(manual.is_no_track()); assert_eq!(track, manual); } + #[test] + fn check_start_test() { + assert!(check_start("/some/path").is_ok()); + assert!(check_start("A").is_ok()); + assert!(check_start("").is_ok()); + assert!(check_start("/org/mpris").is_err()); + assert!(check_start("/org/mpris/more/path").is_err()); + assert!(check_start(NO_TRACK).is_ok()); + } + #[test] fn valid_track_id() { - assert!(TrackID::try_from("/").is_ok()); - assert!(TrackID::try_from("/some/path").is_ok()); + assert_eq!( + TrackID::try_from("/"), + Ok(TrackID(OwnedObjectPath::from( + ObjectPath::from_str_unchecked("/") + ))) + ); + assert_eq!( + TrackID::try_from("/some/path"), + Ok(TrackID(OwnedObjectPath::from( + ObjectPath::from_str_unchecked("/some/path") + ))) + ); + + assert_eq!( + TrackID::try_from("/".to_string()), + Ok(TrackID(OwnedObjectPath::from( + ObjectPath::from_str_unchecked("/") + ))) + ); + assert_eq!( + TrackID::try_from("/some/path".to_string()), + Ok(TrackID(OwnedObjectPath::from( + ObjectPath::from_str_unchecked("/some/path") + ))) + ); } #[test] @@ -194,6 +240,65 @@ mod tests { assert!(TrackID::try_from("//some/path").is_err()); assert!(TrackID::try_from("/some.path").is_err()); assert!(TrackID::try_from("path").is_err()); + assert!(TrackID::try_from("/org/mpris").is_err()); + + assert!(TrackID::try_from("".to_string()).is_err()); + assert!(TrackID::try_from("//some/path".to_string()).is_err()); + assert!(TrackID::try_from("/some.path".to_string()).is_err()); + assert!(TrackID::try_from("path".to_string()).is_err()); + assert!(TrackID::try_from("/org/mpris".to_string()).is_err()); + } + + #[test] + fn from_object_path() { + assert_eq!( + TrackID::try_from(ObjectPath::from_str_unchecked("/valid/path")), + Ok(TrackID(OwnedObjectPath::from( + ObjectPath::from_str_unchecked("/valid/path") + ))) + ); + assert_eq!( + TrackID::try_from(OwnedObjectPath::from(ObjectPath::from_str_unchecked( + "/valid/path" + ))), + Ok(TrackID(OwnedObjectPath::from( + ObjectPath::from_str_unchecked("/valid/path") + ))) + ); + + assert!(TrackID::try_from(ObjectPath::from_str_unchecked("/org/mpris")).is_err()); + assert!( + TrackID::try_from(OwnedObjectPath::from(ObjectPath::from_str_unchecked( + "/org/mpris" + ))) + .is_err() + ); + } + + #[test] + fn from_metadata_value() { + assert!(TrackID::try_from(MetadataValue::Boolean(true)).is_err()); + assert!(TrackID::try_from(MetadataValue::Float(0.0)).is_err()); + assert!(TrackID::try_from(MetadataValue::SignedInt(0)).is_err()); + assert!(TrackID::try_from(MetadataValue::UnsignedInt(0)).is_err()); + assert_eq!( + TrackID::try_from(MetadataValue::String(String::from("/valid/path"))), + Ok(TrackID(OwnedObjectPath::from( + ObjectPath::from_str_unchecked("/valid/path") + ))) + ); + assert!(TrackID::try_from(MetadataValue::Strings(vec![])).is_err()); + assert_eq!( + TrackID::try_from(MetadataValue::Strings(vec![String::from("/valid/path")])), + Ok(TrackID(OwnedObjectPath::from( + ObjectPath::from_str_unchecked("/valid/path") + ))) + ); + assert!( + TrackID::try_from(MetadataValue::Strings(vec![String::from("/valid/path"); 2])) + .is_err() + ); + assert!(TrackID::try_from(MetadataValue::Unsupported).is_err()); } } diff --git a/src/player.rs b/src/player.rs index 64e77b3..9c0a0b2 100644 --- a/src/player.rs +++ b/src/player.rs @@ -192,7 +192,7 @@ impl Player { ) -> Result<(), MprisError> { Ok(self .player_proxy - .set_position(&track_id.get_object_path(), position.into()) + .set_position(&track_id.as_object_path(), position.into()) .await?) } diff --git a/src/playlist.rs b/src/playlist.rs index 6105ff7..ba69a9e 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -187,7 +187,6 @@ mod playlist_ordering_tests { assert_eq!(&PlaylistOrdering::CreationDate.to_string(), "CreationDate"); assert_eq!(&PlaylistOrdering::LastPlayDate.to_string(), "LastPlayDate"); assert_eq!(&PlaylistOrdering::UserDefined.to_string(), "UserDefined"); - } } From 7e6465a72f8bfe6e2ece2f712a526852613c85bf Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Fri, 25 Oct 2024 17:03:06 +0200 Subject: [PATCH 28/55] Add small improvements `AsRef for TrackID` to make it easier to use `TackID` as an argument for the Proxy and `From for MetadataValue` --- src/metadata/track_id.rs | 6 ++++++ src/metadata/values.rs | 8 +++++++- src/player.rs | 2 +- src/proxies.rs | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index bc7449b..c259885 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -37,6 +37,12 @@ impl TrackID { } } +impl AsRef for TrackID { + fn as_ref(&self) -> &OwnedObjectPath { + &self.0 + } +} + fn check_start(s: T) -> Result where T: Deref, diff --git a/src/metadata/values.rs b/src/metadata/values.rs index cba8396..6661033 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -1,4 +1,4 @@ -use zbus::zvariant::Value; +use zbus::zvariant::{OwnedValue, Value}; use crate::errors::InvalidMetadataValue; @@ -45,6 +45,12 @@ impl MetadataValue { } } +impl From for MetadataValue { + fn from(value: OwnedValue) -> Self { + Self::from(Value::from(value)) + } +} + impl<'a> From> for MetadataValue { fn from(value: Value) -> Self { match value { diff --git a/src/player.rs b/src/player.rs index 9c0a0b2..6ac5606 100644 --- a/src/player.rs +++ b/src/player.rs @@ -192,7 +192,7 @@ impl Player { ) -> Result<(), MprisError> { Ok(self .player_proxy - .set_position(&track_id.as_object_path(), position.into()) + .set_position(track_id.as_ref(), position.into()) .await?) } diff --git a/src/proxies.rs b/src/proxies.rs index 82faa3e..75b34d7 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -98,7 +98,7 @@ pub(crate) trait Player { fn seek(&self, offset: i64) -> zbus::Result<()>; /// SetPosition method - fn set_position(&self, track_id: &ObjectPath<'_>, position: i64) -> zbus::Result<()>; + fn set_position(&self, track_id: &OwnedObjectPath, position: i64) -> zbus::Result<()>; /// Stop method fn stop(&self) -> zbus::Result<()>; From b38f6eb33f53da6f16d044a26e6b4d2cee16f305 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Fri, 25 Oct 2024 17:07:42 +0200 Subject: [PATCH 29/55] Implement TrackList interface --- src/player.rs | 88 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/proxies.rs | 65 ++++++++++++++++++++++++++++++++++++- 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/src/player.rs b/src/player.rs index 6ac5606..4a02dcf 100644 --- a/src/player.rs +++ b/src/player.rs @@ -5,7 +5,7 @@ use zbus::{names::BusName, Connection}; use crate::{ metadata::MetadataValue, - proxies::{MediaPlayer2Proxy, PlayerProxy, PlaylistsProxy}, + proxies::{MediaPlayer2Proxy, PlayerProxy, PlaylistsProxy, TrackListProxy}, LoopStatus, Metadata, Mpris, MprisDuration, MprisError, PlaybackStatus, Playlist, PlaylistOrdering, TrackID, MPRIS2_PREFIX, }; @@ -15,6 +15,7 @@ pub struct Player { mp2_proxy: MediaPlayer2Proxy<'static>, player_proxy: PlayerProxy<'static>, playlist_proxy: Option>, + track_list_proxy: Option>, } impl Player { @@ -26,18 +27,25 @@ impl Player { connection: Connection, bus_name: BusName<'static>, ) -> Result { - let (mp2_proxy, player_proxy, playlist_proxy) = try_join!( + let (mp2_proxy, player_proxy, playlist_proxy, track_list_proxy) = try_join!( MediaPlayer2Proxy::new(&connection, bus_name.clone()), PlayerProxy::new(&connection, bus_name.clone()), PlaylistsProxy::new(&connection, bus_name.clone()), + TrackListProxy::new(&connection, bus_name.clone()), )?; let playlist = playlist_proxy.playlist_count().await.is_ok(); + let track_list = track_list_proxy.can_edit_tracks().await.is_ok(); Ok(Player { bus_name, mp2_proxy, player_proxy, playlist_proxy: if playlist { Some(playlist_proxy) } else { None }, + track_list_proxy: if track_list { + Some(track_list_proxy) + } else { + None + }, }) } @@ -49,6 +57,10 @@ impl Player { self.playlist_proxy.is_some() } + pub fn supports_track_list_interface(&self) -> bool { + self.track_list_proxy.is_some() + } + pub async fn metadata(&self) -> Result { Ok(self.raw_metadata().await?.try_into()?) } @@ -298,12 +310,84 @@ impl Player { pub async fn playlist_count(&self) -> Result { Ok(self.check_playlist_support()?.playlist_count().await?) } + + fn check_track_list_support(&self) -> Result<&TrackListProxy, MprisError> { + match &self.track_list_proxy { + Some(proxy) => Ok(proxy), + None => Err(MprisError::Unsupported), + } + } + + pub async fn can_edit_tracks(&self) -> Result { + Ok(self.check_track_list_support()?.can_edit_tracks().await?) + } + + pub async fn tracks(&self) -> Result, MprisError> { + let result = self.check_track_list_support()?.tracks().await?; + let mut track_ids = Vec::with_capacity(result.len()); + for r in result { + track_ids.push(TrackID::try_from(r)?); + } + Ok(track_ids) + } + + pub async fn add_track( + &self, + url: &str, + after_track: Option<&TrackID>, + set_as_current: bool, + ) -> Result<(), MprisError> { + let after = if let Some(track_id) = after_track { + track_id + } else { + &TrackID::no_track() + }; + Ok(self + .check_track_list_support()? + .add_track(url, after.as_ref(), set_as_current) + .await?) + } + + pub async fn remove_track(&self, track_id: &TrackID) -> Result<(), MprisError> { + Ok(self + .check_track_list_support()? + .remove_track(track_id.as_ref()) + .await?) + } + + pub async fn go_to(&self, track_id: &TrackID) -> Result<(), MprisError> { + Ok(self + .check_track_list_support()? + .go_to(track_id.as_ref()) + .await?) + } + + pub async fn get_tracks_metadata( + &self, + tracks: &[TrackID], + ) -> Result, MprisError> { + let result = self + .check_track_list_support()? + .get_tracks_metadata(&tracks.iter().map(|t| t.as_ref()).collect::>()) + .await?; + + let mut metadata = Vec::with_capacity(tracks.len()); + for meta in result { + let raw: HashMap = meta + .into_iter() + .map(|(k, v)| (k, MetadataValue::from(v))) + .collect(); + metadata.push(Metadata::try_from(raw)?); + } + Ok(metadata) + } } impl std::fmt::Debug for Player { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Player") .field("bus_name", &self.bus_name()) + .field("track_list", &self.track_list_proxy.is_some()) .field("playlist_proxy", &self.playlist_proxy.is_some()) .finish() } diff --git a/src/proxies.rs b/src/proxies.rs index 75b34d7..eed34b7 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use zbus::proxy; -use zbus::zvariant::{ObjectPath, OwnedObjectPath, Value}; +use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value}; #[proxy( default_service = "org.freedesktop.DBus", @@ -216,3 +216,66 @@ pub(crate) trait Playlists { #[zbus(property)] fn playlist_count(&self) -> zbus::Result; } + +#[proxy( + interface = "org.mpris.MediaPlayer2.TrackList", + default_path = "/org/mpris/MediaPlayer2", + gen_blocking = false +)] +pub trait TrackList { + /// AddTrack method + fn add_track( + &self, + uri: &str, + after_track: &OwnedObjectPath, + set_as_current: bool, + ) -> zbus::Result<()>; + + /// GetTracksMetadata method + fn get_tracks_metadata( + &self, + track_ids: &[&OwnedObjectPath], + ) -> zbus::Result>>; + + /// GoTo method + fn go_to(&self, track_id: &OwnedObjectPath) -> zbus::Result<()>; + + /// RemoveTrack method + fn remove_track(&self, track_id: &OwnedObjectPath) -> zbus::Result<()>; + + /// TrackAdded signal + #[zbus(signal)] + fn track_added( + &self, + metadata: HashMap<&str, Value<'_>>, + after_track: ObjectPath<'_>, + ) -> zbus::Result<()>; + + /// TrackListReplaced signal + #[zbus(signal)] + fn track_list_replaced( + &self, + track_ids: Vec>, + current_track: ObjectPath<'_>, + ) -> zbus::Result<()>; + + /// TrackMetadataChanged signal + #[zbus(signal)] + fn track_metadata_changed( + &self, + track_id: ObjectPath<'_>, + metadata: HashMap<&str, Value<'_>>, + ) -> zbus::Result<()>; + + /// TrackRemoved signal + #[zbus(signal)] + fn track_removed(&self, track_id: ObjectPath<'_>) -> zbus::Result<()>; + + /// CanEditTracks property + #[zbus(property)] + fn can_edit_tracks(&self) -> zbus::Result; + + /// Tracks property + #[zbus(property(emits_changed_signal = "invalidates"))] + fn tracks(&self) -> zbus::Result>; +} From d2bb214099a36e7b4056d66fc53b40961bcd9613 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Fri, 25 Oct 2024 17:58:45 +0200 Subject: [PATCH 30/55] Add check for TrackID.is_no_track() in go_to() and remove_track() --- src/errors.rs | 11 +++++++++++ src/player.rs | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/src/errors.rs b/src/errors.rs index e668699..6398c59 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -80,10 +80,21 @@ pub enum MprisError { /// The player doesn't implement the required interface/method/signal Unsupported, + /// One of the given arguments has an invalid value + InvalidArgument(String), + /// Some other unexpected error occurred. Miscellaneous(String), } +impl MprisError { + pub(crate) fn track_id_is_no_track() -> Self { + Self::InvalidArgument( + "/org/mpris/MediaPlayer2/TrackList/NoTrack is not a valid value".to_owned(), + ) + } +} + impl From for MprisError { fn from(value: zbus::Error) -> Self { match value { diff --git a/src/player.rs b/src/player.rs index 4a02dcf..3451b9a 100644 --- a/src/player.rs +++ b/src/player.rs @@ -349,6 +349,9 @@ impl Player { } pub async fn remove_track(&self, track_id: &TrackID) -> Result<(), MprisError> { + if track_id.is_no_track() { + return Err(MprisError::track_id_is_no_track()); + } Ok(self .check_track_list_support()? .remove_track(track_id.as_ref()) @@ -356,6 +359,9 @@ impl Player { } pub async fn go_to(&self, track_id: &TrackID) -> Result<(), MprisError> { + if track_id.is_no_track() { + return Err(MprisError::track_id_is_no_track()); + } Ok(self .check_track_list_support()? .go_to(track_id.as_ref()) From 47f302def3c8c6cfd672c5ca35247962b5dcb9a8 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Fri, 25 Oct 2024 18:00:54 +0200 Subject: [PATCH 31/55] Improve Metadata serde --- src/metadata/metadata.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/metadata/metadata.rs b/src/metadata/metadata.rs index f4e88dc..fa165bf 100644 --- a/src/metadata/metadata.rs +++ b/src/metadata/metadata.rs @@ -129,7 +129,11 @@ macro_rules! gen_metadata_struct { gen_metadata_struct!( #[derive(Debug, Clone, Default, PartialEq)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(into = "RawMetadata", try_from = "RawMetadata") + ) + ] struct Metadata { "xesam:albumArtist" => album_artists: Vec, "xesam:album" => album_name: String, From bca17ed02c42f8e064757733cc083a4a0ece06cb Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Fri, 25 Oct 2024 18:32:29 +0200 Subject: [PATCH 32/55] Add lint rules Added the lint rules that were present before which meant that a Debug implementation for `Mpris` and `PlayerStream` had to be added. Also added a way to get to the `Connection` and `Executor` from `Mpris`. --- src/lib.rs | 40 +++++++++++++++++++++++++++++++++++++++- src/player.rs | 4 ++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2412547..ea89a4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,17 @@ -use std::fmt::Display; +// #![warn(missing_docs)] +#![deny( + missing_debug_implementations, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unsafe_code, + unreachable_pub, + unstable_features, + unused_import_braces, + unused_qualifications +)] + +use std::fmt::{Debug, Display}; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; @@ -29,6 +42,7 @@ pub(crate) const MPRIS2_PREFIX: &str = "org.mpris.MediaPlayer2."; type PlayerFuture = Pin> + Send>>; +#[derive(Clone)] pub struct Mpris { connection: Connection, dbus_proxy: DBusProxy<'static>, @@ -53,6 +67,14 @@ impl Mpris { }) } + pub fn get_connection(&self) -> Connection { + self.connection.clone() + } + + pub fn get_executor(&self) -> &'static zbus::Executor { + self.connection.executor() + } + pub async fn find_first(&self) -> Result, MprisError> { match self.all_player_bus_names().await?.into_iter().next() { Some(bus) => Ok(Some( @@ -133,6 +155,14 @@ impl Mpris { } } +impl Debug for Mpris { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Mpris") + .field("connection", &"zbus::Connection") + .finish_non_exhaustive() + } +} + pub struct PlayerStream { futures: Vec, } @@ -179,6 +209,14 @@ impl FusedStream for PlayerStream { } } +impl Debug for PlayerStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PlayerStream") + .field("players_left", &self.futures.len()) + .finish() + } +} + #[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] /// The [`Player`]'s playback status /// diff --git a/src/player.rs b/src/player.rs index 3451b9a..806c132 100644 --- a/src/player.rs +++ b/src/player.rs @@ -394,7 +394,7 @@ impl std::fmt::Debug for Player { f.debug_struct("Player") .field("bus_name", &self.bus_name()) .field("track_list", &self.track_list_proxy.is_some()) - .field("playlist_proxy", &self.playlist_proxy.is_some()) - .finish() + .field("playlist", &self.playlist_proxy.is_some()) + .finish_non_exhaustive() } } From 30cbe0842be90dad7880587be654532dd00109ae Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Fri, 25 Oct 2024 21:49:24 +0200 Subject: [PATCH 33/55] Change PlayerStream Instead of holding a `Vec` of futures it now holds a `VecDeque` of bus names, a`Connection` and the currently worked on future (if any). This allows for a much more useful `Debug` output and prevents cloning things unless they are needed. Also added `Clone` for `Player` and made the `Debug` formatting for `Mpris` and `PlayerStream` skip the `Connection` fields because it's a lot of noise and if needed you can easily just debug print the connection directly. --- src/lib.rs | 65 ++++++++++++++++++++++++++++++++------------------- src/player.rs | 1 + 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ea89a4a..6ea9b8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ // #![warn(missing_docs)] +#![warn(clippy::print_stdout)] #![deny( missing_debug_implementations, missing_copy_implementations, @@ -11,6 +12,7 @@ unused_qualifications )] +use std::collections::VecDeque; use std::fmt::{Debug, Display}; use std::future::Future; use std::pin::Pin; @@ -151,33 +153,32 @@ impl Mpris { pub async fn into_stream(&self) -> Result { let buses = self.all_player_bus_names().await?; - Ok(PlayerStream::new(&self.connection, buses)) + Ok(PlayerStream::new(self.connection.clone(), buses)) } } impl Debug for Mpris { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Mpris") - .field("connection", &"zbus::Connection") + .field("connection", &format_args!("Connection {{ .. }}")) .finish_non_exhaustive() } } pub struct PlayerStream { - futures: Vec, + connection: Connection, + buses: VecDeque>, + cur_future: Option, } impl PlayerStream { - pub fn new(connection: &Connection, buses: Vec>) -> Self { - let mut futures: Vec = Vec::with_capacity(buses.len()); - for fut in buses - .into_iter() - .rev() - .map(|bus_name| Box::pin(Player::new_from_connection(connection.clone(), bus_name))) - { - futures.push(fut); + pub fn new(connection: Connection, buses: Vec>) -> Self { + let buses = VecDeque::from(buses); + Self { + connection, + buses, + cur_future: None, } - Self { futures } } } @@ -185,34 +186,50 @@ impl Stream for PlayerStream { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.futures.last_mut() { - Some(last) => match last.as_mut().poll(cx) { - Poll::Ready(result) => { - self.futures.pop(); - Poll::Ready(Some(result)) - } - Poll::Pending => Poll::Pending, - }, - None => Poll::Ready(None), + loop { + match self.cur_future.as_mut() { + Some(fut) => match fut.as_mut().poll(cx) { + Poll::Ready(player) => { + self.cur_future = None; + self.buses.pop_front(); + return Poll::Ready(Some(player)); + } + Poll::Pending => return Poll::Pending, + }, + None => match self.buses.front() { + Some(bus) => { + self.cur_future = Some(Box::pin(Player::new_from_connection( + self.connection.clone(), + bus.clone(), + ))) + } + None => return Poll::Ready(None), + }, + } } } fn size_hint(&self) -> (usize, Option) { - let l = self.futures.len(); + let l = self.buses.len(); (l, Some(l)) } } impl FusedStream for PlayerStream { fn is_terminated(&self) -> bool { - self.futures.is_empty() + self.buses.is_empty() } } impl Debug for PlayerStream { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PlayerStream") - .field("players_left", &self.futures.len()) + .field("connection", &format_args!("Connection {{ .. }}")) + .field("buses", &self.buses) + .field( + "cur_future", + &self.cur_future.as_ref().map(|_| &self.buses[0]), + ) .finish() } } diff --git a/src/player.rs b/src/player.rs index 806c132..9feade0 100644 --- a/src/player.rs +++ b/src/player.rs @@ -10,6 +10,7 @@ use crate::{ PlaylistOrdering, TrackID, MPRIS2_PREFIX, }; +#[derive(Clone)] pub struct Player { bus_name: BusName<'static>, mp2_proxy: MediaPlayer2Proxy<'static>, From 9a43c1a5a1252b33962f0dbc005a1db55a68a74b Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sun, 27 Oct 2024 18:57:05 +0100 Subject: [PATCH 34/55] Add more implementations to MprisDuration Added ops traits (with tests) for `Self` and `Duration`. The ops tests now also use a macro. Renamed the argument names for `new_from_u64()` and `new_from_i64()` to make it more obvious what the value means. Added `new_from_duration()`, `as_u64()` and `as_i64()` --- src/duration.rs | 158 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 114 insertions(+), 44 deletions(-) diff --git a/src/duration.rs b/src/duration.rs index 3b24730..537f647 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -16,17 +16,28 @@ const MAX: u64 = i64::MAX as u64; pub struct MprisDuration(u64); impl MprisDuration { - pub fn new_from_u64(value: u64) -> Self { - Self(value.clamp(0, MAX)) + pub fn new_from_u64(micros: u64) -> Self { + Self(micros.clamp(0, MAX)) } - pub fn new_from_i64(value: i64) -> Self { - Self(value.clamp(0, i64::MAX) as u64) + pub fn new_from_i64(micros: i64) -> Self { + Self(micros.clamp(0, i64::MAX) as u64) + } + + pub fn new_from_duration(duration: Duration) -> Self { + Self(duration.as_micros().clamp(0, MAX as u128) as u64) } pub fn new_max() -> Self { Self(MAX) } + pub fn as_u64(&self) -> u64 { + self.0 + } + + pub fn as_i64(&self) -> i64 { + self.0 as i64 + } } impl From for Duration { @@ -110,6 +121,38 @@ macro_rules! impl_math { Self::$method(self, *rhs) } } + + impl $trait for MprisDuration { + type Output = Self; + + fn $method(self, rhs: MprisDuration) -> Self::Output { + Self::$method(self, rhs.as_u64()) + } + } + + impl $trait<&MprisDuration> for MprisDuration { + type Output = Self; + + fn $method(self, rhs: &MprisDuration) -> Self::Output { + Self::$method(self, rhs.as_u64()) + } + } + + impl $trait for MprisDuration { + type Output = Self; + + fn $method(self, rhs: Duration) -> Self::Output { + Self::$method(self, rhs.as_micros().clamp(0, MAX as u128) as u64) + } + } + + impl $trait<&Duration> for MprisDuration { + type Output = Self; + + fn $method(self, rhs: &Duration) -> Self::Output { + Self::$method(self, rhs.as_micros().clamp(0, MAX as u128) as u64) + } + } }; } @@ -197,53 +240,80 @@ mod mrpis_duration_tests { assert!(MprisDuration::try_from(MetadataValue::Strings(vec![])).is_err()); assert!(MprisDuration::try_from(MetadataValue::Unsupported).is_err()); } +} - #[test] - fn math() { - assert_eq!( - MprisDuration::new_from_u64(1) * 10, - MprisDuration::new_from_u64(10) - ); - #[allow(clippy::erasing_op)] - { +#[cfg(test)] +mod ops_tests { + use super::*; + + macro_rules! gen_ops_tests { + ($type:expr) => { + assert_eq!( + MprisDuration::new_from_u64(1) * $type(10_u64), + MprisDuration::new_from_u64(10) + ); + #[allow(clippy::erasing_op)] + { + assert_eq!( + MprisDuration::new_from_u64(1) * $type(0_u64), + MprisDuration::new_from_u64(0) + ); + } assert_eq!( - MprisDuration::new_from_u64(1) * 0, + MprisDuration::new_max() * $type(2_u64), + MprisDuration::new_max() + ); + + assert_eq!( + MprisDuration::new_from_u64(0) / $type(1_u64), + MprisDuration::new_from_u64(0) + ); + assert_eq!( + MprisDuration::new_from_u64(10) / $type(3_u64), + MprisDuration::new_from_u64(10 / 3) + ); + assert_eq!( + MprisDuration::new_max() / $type(MAX), + MprisDuration::new_from_u64(1) + ); + assert_eq!( + MprisDuration::new_from_u64(1) / $type(MAX), MprisDuration::new_from_u64(0) ); - } - assert_eq!(MprisDuration::new_max() * 2, MprisDuration::new_max()); - assert_eq!( - MprisDuration::new_from_u64(0) / 1, - MprisDuration::new_from_u64(0) - ); - assert_eq!( - MprisDuration::new_from_u64(10) / 3, - MprisDuration::new_from_u64(10 / 3) - ); - assert_eq!( - MprisDuration::new_max() / MAX, - MprisDuration::new_from_u64(1) - ); - assert_eq!( - MprisDuration::new_from_u64(1) / MAX, - MprisDuration::new_from_u64(0) - ); + assert_eq!( + MprisDuration::new_from_u64(0) + $type(1_u64), + MprisDuration::new_from_u64(1) + ); + assert_eq!( + MprisDuration::new_max() + $type(1_u64), + MprisDuration::new_max() + ); - assert_eq!( - MprisDuration::new_from_u64(0) + 1, - MprisDuration::new_from_u64(1) - ); - assert_eq!(MprisDuration::new_max() + 1, MprisDuration::new_max()); + assert_eq!( + MprisDuration::new_from_u64(0) - $type(1_u64), + MprisDuration::new_from_u64(0) + ); + assert_eq!( + MprisDuration::new_from_u64(10) - $type(1_u64), + MprisDuration::new_from_u64(9) + ); + }; + } - assert_eq!( - MprisDuration::new_from_u64(0) - 1, - MprisDuration::new_from_u64(0) - ); - assert_eq!( - MprisDuration::new_from_u64(10) - 1, - MprisDuration::new_from_u64(9) - ); + #[test] + fn u64() { + gen_ops_tests!(u64::from); + } + + #[test] + fn mpris_duration() { + gen_ops_tests!(MprisDuration::new_from_u64); + } + + #[test] + fn duration() { + gen_ops_tests!(Duration::from_micros); } } From a842d35311cfdea539c4f3e409f82c55afee149b Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sun, 27 Oct 2024 21:02:55 +0100 Subject: [PATCH 35/55] Improve errors mod All of the errors right now are the same so they can easily be generated with a macro. Also publicly imported `zbus::Error` so that it's included in the documentation. --- src/errors.rs | 85 +++++++++++++++++++++------------------------------ 1 file changed, 35 insertions(+), 50 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 6398c59..9799384 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,44 +1,19 @@ use std::fmt::Display; -/// [`PlaybackStatus`][crate::PlaybackStatus] had an invalid string value. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InvalidPlaybackStatus(pub(crate) String); +pub use zbus::Error; + +macro_rules! generate_error { + ($error:ident, $source:ident) => { + #[doc=concat!("Error for when [`", + stringify!($source), + "`](crate::", + stringify!($source), + ") ", + "failed to be created." + )] + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $error(pub(crate) String); -/// [`LoopStatus`][crate::LoopStatus] had an invalid string value. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InvalidLoopStatus(pub(crate) String); - -/// [`TrackID`][crate::metadata::TrackID] had an invalid ObjectPath. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InvalidTrackID(pub(crate) String); - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InvalidMprisDuration(pub(crate) String); - -impl InvalidMprisDuration { - pub(crate) fn new_too_big() -> Self { - Self("can't create MprisDuration, value too big".to_string()) - } - - pub(crate) fn new_negative() -> Self { - Self("can't create MprisDuration, value is negative".to_string()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InvalidMetadataValue(pub(crate) String); - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InvalidMetadata(pub(crate) String); - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InvalidPlaylist(pub(crate) String); - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InvalidPlaylistOrdering(pub(crate) String); - -macro_rules! impl_display { - ($error:ty) => { impl Display for $error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) @@ -59,19 +34,29 @@ macro_rules! impl_display { }; } -impl_display!(InvalidPlaybackStatus); -impl_display!(InvalidLoopStatus); -impl_display!(InvalidTrackID); -impl_display!(InvalidMprisDuration); -impl_display!(InvalidMetadataValue); -impl_display!(InvalidMetadata); -impl_display!(InvalidPlaylist); -impl_display!(InvalidPlaylistOrdering); +generate_error!(InvalidPlaybackStatus, PlaybackStatus); +generate_error!(InvalidLoopStatus, LoopStatus); +generate_error!(InvalidTrackID, TrackID); +generate_error!(InvalidMprisDuration, MprisDuration); +generate_error!(InvalidMetadataValue, MetadataValue); +generate_error!(InvalidMetadata, Metadata); +generate_error!(InvalidPlaylist, Playlist); +generate_error!(InvalidPlaylistOrdering, PlaylistOrdering); + +impl InvalidMprisDuration { + pub(crate) fn new_too_big() -> Self { + Self("can't create MprisDuration, value too big".to_string()) + } + + pub(crate) fn new_negative() -> Self { + Self("can't create MprisDuration, value is negative".to_string()) + } +} #[derive(Debug, PartialEq, Clone)] pub enum MprisError { /// An error occurred while talking to the D-Bus. - DbusError(zbus::Error), + DbusError(Error), /// Failed to parse an enum from a string value received from the [`Player`][crate::Player]. /// This means that the [`Player`][crate::Player] replied with unexpected data. @@ -95,10 +80,10 @@ impl MprisError { } } -impl From for MprisError { - fn from(value: zbus::Error) -> Self { +impl From for MprisError { + fn from(value: Error) -> Self { match value { - zbus::Error::InterfaceNotFound | zbus::Error::Unsupported => Self::Unsupported, + Error::InterfaceNotFound | Error::Unsupported => Self::Unsupported, _ => todo!(), } } From 28309d5584cee52325d7314b652835c324684d8a Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sun, 27 Oct 2024 21:16:40 +0100 Subject: [PATCH 36/55] Prevent formatting --- src/errors.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 9799384..68292e4 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -2,15 +2,18 @@ use std::fmt::Display; pub use zbus::Error; +#[rustfmt::skip] macro_rules! generate_error { ($error:ident, $source:ident) => { - #[doc=concat!("Error for when [`", - stringify!($source), - "`](crate::", - stringify!($source), - ") ", - "failed to be created." - )] + #[doc=concat!( + "Error for when [`", + stringify!($source), + "`](crate::", + stringify!($source), + ") ", + "failed to be created." + ) + ] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct $error(pub(crate) String); From 89f591a6396d23dd348d6d6fd2e8c5c3034b622f Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Mon, 28 Oct 2024 21:13:49 +0100 Subject: [PATCH 37/55] Make small changes Added `Metadata::is_valid()`. `Metadata::get_metadata_key()` is no longer a method. Added `PlaylistOrdering::as_str()` that returns the struct variant name as &str and the `Display` now also uses it. Returned `ObjectPath` now have a lifetime to make it more obvious that they are borrowed. --- src/metadata/metadata.rs | 21 ++++++++++++++++++++- src/metadata/track_id.rs | 2 +- src/playlist.rs | 25 ++++++++++++------------- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/metadata/metadata.rs b/src/metadata/metadata.rs index fa165bf..0cb8824 100644 --- a/src/metadata/metadata.rs +++ b/src/metadata/metadata.rs @@ -11,6 +11,7 @@ type RawMetadata = HashMap; /// The generated things are: /// - `Metadata::new()` /// - `Metadata::is_empty()` +/// - `Metadata::is_valid()' /// - `Metadata::get_metadata_key()` which lets you get the key for a given field as a str /// - `TryFrom> for Metadata` /// - `Metadata::from_raw_lossy()` which is similar to the above but wrong types just get discarded @@ -60,7 +61,15 @@ macro_rules! gen_metadata_struct { && self.$others_name.is_empty() } - pub fn get_metadata_key(&self, field: &str) -> Option<&str> { + pub fn is_valid(&self) -> bool { + if self.is_empty() { + true + } else { + self.track_id.is_some() + } + } + + pub fn get_metadata_key(field: &str) -> Option<&str> { match field { $(stringify!($field) => Some($key)),*, _ => None @@ -247,6 +256,16 @@ mod metadata_tests { assert!(m.is_empty()); } + #[test] + fn is_valid() { + let mut meta = Metadata::new(); + assert!(meta.is_valid()); + meta.album_name = Some(String::from("Album Name")); + assert!(!meta.is_valid()); + meta.track_id = Some(TrackID::no_track()); + assert!(meta.is_valid()); + } + #[test] fn default_back_and_forth() { let original = Metadata::new(); diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index c259885..c960961 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -32,7 +32,7 @@ impl TrackID { self.0.as_str() } - pub fn as_object_path(&self) -> ObjectPath { + pub fn as_object_path(&self) -> ObjectPath<'_> { self.0.as_ref() } } diff --git a/src/playlist.rs b/src/playlist.rs index ba69a9e..2c8b38b 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -37,7 +37,7 @@ impl Playlist { self.icon.as_deref() } - pub fn get_id(&self) -> ObjectPath { + pub fn get_id(&self) -> ObjectPath<'_> { self.id.as_ref() } @@ -113,21 +113,20 @@ impl PlaylistOrdering { PlaylistOrdering::UserDefined => "User", } } + pub fn as_str(&self) -> &str { + match self { + PlaylistOrdering::Alphabetical => "Alphabetical", + PlaylistOrdering::CreationDate => "CreationDate", + PlaylistOrdering::ModifiedDate => "ModifiedDate", + PlaylistOrdering::LastPlayDate => "LastPlayDate", + PlaylistOrdering::UserDefined => "UserDefined", + } + } } impl std::fmt::Display for PlaylistOrdering { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - PlaylistOrdering::Alphabetical => "Alphabetical", - PlaylistOrdering::CreationDate => "CreationDate", - PlaylistOrdering::ModifiedDate => "ModifiedDate", - PlaylistOrdering::LastPlayDate => "LastPlayDate", - PlaylistOrdering::UserDefined => "UserDefined", - } - ) + write!(f, "{}", self.as_str()) } } @@ -182,7 +181,7 @@ mod playlist_ordering_tests { } #[test] - fn disaply() { + fn display() { assert_eq!(&PlaylistOrdering::Alphabetical.to_string(), "Alphabetical"); assert_eq!(&PlaylistOrdering::CreationDate.to_string(), "CreationDate"); assert_eq!(&PlaylistOrdering::LastPlayDate.to_string(), "LastPlayDate"); From 1732618e122f3c3440de127b43dec8757f4a9b3d Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Mon, 28 Oct 2024 23:44:16 +0100 Subject: [PATCH 38/55] Fix unfinished code in errors --- src/errors.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/errors.rs b/src/errors.rs index 68292e4..c3bc8fd 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -87,7 +87,7 @@ impl From for MprisError { fn from(value: Error) -> Self { match value { Error::InterfaceNotFound | Error::Unsupported => Self::Unsupported, - _ => todo!(), + _ => Self::DbusError(value), } } } From 6944039e7ee3e03e6a8ee17efda8cb3c5335967c Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Tue, 29 Oct 2024 23:28:42 +0100 Subject: [PATCH 39/55] Small clean-ups `Player` no longer needs a reference to `Mpris` for `new()`. Added missing fullscreen related methods on the proxy and removed a method that isn't part of MPRIS. Added argument checks for `set_position()` and `set_playback_rate()` --- src/lib.rs | 12 ++++-------- src/player.rs | 36 +++++++++++++++++++++++------------- src/proxies.rs | 12 +++++++++--- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6ea9b8d..e8be07d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,9 +79,7 @@ impl Mpris { pub async fn find_first(&self) -> Result, MprisError> { match self.all_player_bus_names().await?.into_iter().next() { - Some(bus) => Ok(Some( - Player::new_from_connection(self.connection.clone(), bus).await?, - )), + Some(bus) => Ok(Some(Player::new(self.connection.clone(), bus).await?)), None => Ok(None), } } @@ -132,7 +130,7 @@ impl Mpris { let bus_names = self.all_player_bus_names().await?; let mut players = Vec::with_capacity(bus_names.len()); for player_name in bus_names { - players.push(Player::new_from_connection(self.connection.clone(), player_name).await?); + players.push(Player::new(self.connection.clone(), player_name).await?); } Ok(players) } @@ -198,10 +196,8 @@ impl Stream for PlayerStream { }, None => match self.buses.front() { Some(bus) => { - self.cur_future = Some(Box::pin(Player::new_from_connection( - self.connection.clone(), - bus.clone(), - ))) + self.cur_future = + Some(Box::pin(Player::new(self.connection.clone(), bus.clone()))) } None => return Poll::Ready(None), }, diff --git a/src/player.rs b/src/player.rs index 9feade0..2e2c0c6 100644 --- a/src/player.rs +++ b/src/player.rs @@ -6,8 +6,8 @@ use zbus::{names::BusName, Connection}; use crate::{ metadata::MetadataValue, proxies::{MediaPlayer2Proxy, PlayerProxy, PlaylistsProxy, TrackListProxy}, - LoopStatus, Metadata, Mpris, MprisDuration, MprisError, PlaybackStatus, Playlist, - PlaylistOrdering, TrackID, MPRIS2_PREFIX, + LoopStatus, Metadata, MprisDuration, MprisError, PlaybackStatus, Playlist, PlaylistOrdering, + TrackID, MPRIS2_PREFIX, }; #[derive(Clone)] @@ -20,11 +20,7 @@ pub struct Player { } impl Player { - pub async fn new(mpris: &Mpris, bus_name: BusName<'static>) -> Result { - Player::new_from_connection(mpris.connection.clone(), bus_name).await - } - - pub(crate) async fn new_from_connection( + pub async fn new( connection: Connection, bus_name: BusName<'static>, ) -> Result { @@ -130,6 +126,18 @@ impl Player { Ok(self.mp2_proxy.supported_uri_schemes().await?) } + pub async fn get_fullscreen(&self) -> Result { + Ok(self.mp2_proxy.fullscreen().await?) + } + + pub async fn set_fullscreen(&self, value: bool) -> Result<(), MprisError> { + Ok(self.mp2_proxy.set_fullscreen(value).await?) + } + + pub async fn can_set_fullscreen(&self) -> Result { + Ok(self.mp2_proxy.can_set_fullscreen().await?) + } + pub async fn can_control(&self) -> Result { Ok(self.player_proxy.can_control().await?) } @@ -174,10 +182,6 @@ impl Player { Ok(self.player_proxy.stop().await?) } - pub async fn stop_after_current(&self) -> Result<(), MprisError> { - Ok(self.player_proxy.stop_after_current().await?) - } - pub async fn seek(&self, offset_in_microseconds: i64) -> Result<(), MprisError> { Ok(self.player_proxy.seek(offset_in_microseconds).await?) } @@ -203,6 +207,9 @@ impl Player { track_id: &TrackID, position: MprisDuration, ) -> Result<(), MprisError> { + if track_id.is_no_track() { + return Err(MprisError::track_id_is_no_track()); + } Ok(self .player_proxy .set_position(track_id.as_ref(), position.into()) @@ -241,6 +248,9 @@ impl Player { } pub async fn set_playback_rate(&self, rate: f64) -> Result<(), MprisError> { + if rate == 0.0 { + return Err(MprisError::InvalidArgument("rate can't be 0.0".to_string())); + } Ok(self.player_proxy.set_rate(rate).await?) } @@ -334,7 +344,7 @@ impl Player { pub async fn add_track( &self, - url: &str, + uri: &str, after_track: Option<&TrackID>, set_as_current: bool, ) -> Result<(), MprisError> { @@ -345,7 +355,7 @@ impl Player { }; Ok(self .check_track_list_support()? - .add_track(url, after.as_ref(), set_as_current) + .add_track(uri, after.as_ref(), set_as_current) .await?) } diff --git a/src/proxies.rs b/src/proxies.rs index eed34b7..422960d 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -52,6 +52,15 @@ pub(crate) trait MediaPlayer2 { /// SupportedUriSchemes property #[zbus(property)] fn supported_uri_schemes(&self) -> zbus::Result>; + + #[zbus(property)] + fn fullscreen(&self) -> zbus::Result; + + #[zbus(property)] + fn set_fullscreen(&self, value: bool) -> zbus::Result<()>; + + #[zbus(property)] + fn can_set_fullscreen(&self) -> zbus::Result; } impl MediaPlayer2Proxy<'_> { @@ -103,9 +112,6 @@ pub(crate) trait Player { /// Stop method fn stop(&self) -> zbus::Result<()>; - /// StopAfterCurrent method - fn stop_after_current(&self) -> zbus::Result<()>; - /// Seeked signal #[zbus(signal)] fn seeked(&self, position: i64) -> zbus::Result<()>; From ce1abbbd46974947ebc6484930266f57a6fcdbf2 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Wed, 30 Oct 2024 17:29:48 +0100 Subject: [PATCH 40/55] Add documentation Everything has been documented. NO_TRACK is now a associated constant in `TrackID` and it's public. `RawMetadata` was made public. --- src/duration.rs | 48 +++ src/errors.rs | 16 +- src/lib.rs | 184 +++++++- src/metadata.rs | 4 +- src/metadata/metadata.rs | 170 +++++++- src/metadata/track_id.rs | 36 +- src/metadata/values.rs | 53 ++- src/player.rs | 884 ++++++++++++++++++++++++++++++++++++++- src/playlist.rs | 57 ++- 9 files changed, 1405 insertions(+), 47 deletions(-) diff --git a/src/duration.rs b/src/duration.rs index 537f647..bc5ce43 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -7,6 +7,38 @@ use crate::{errors::InvalidMprisDuration, metadata::MetadataValue}; const MAX: u64 = i64::MAX as u64; +/// A Mpris specific version of [`Duration`]. +/// +/// Contains a time duration in microseconds that's a non-negative [`i64`]. Technically the MPRIS +/// spec allows for time durations to be negative even though a song can't have a negative +/// length/position but this type doesn't allow them. +/// +/// ## Creation +/// +/// The simplest way to create this type is to create a [`Duration`] and covert it into it. +/// ``` +/// use std::time::Duration; +/// use mpris::MprisDuration; +/// +/// let m_dur = MprisDuration::new_from_duration(Duration::from_secs(10)); +/// ``` +/// +/// All of the [`TryFrom`] implementations for [`MprisDuration`] will fail if the value can't be +/// losslessly converted to a valid [`MprisDuration`]. In case you just want to create a valid +/// [`MprisDuration`] and don't are about about being lossless you can use the `new_from_*` methods. +/// ``` +/// use std::time::Duration; +/// use mpris::MprisDuration; +/// +/// let m_dur = MprisDuration::new_from_i64(-42_i64); +/// assert_eq!(m_dur.as_u64(), 0); +/// +/// let dur_big = MprisDuration::new_from_duration(Duration::from_secs(u64::MAX)); +/// assert_eq!(dur_big.as_i64(), i64::MAX) +/// ``` +/// ## Ops +/// [`MprisDuration`] implements [`Add`], [`Sub`], [`Mul`] and [`Div`] for itself, [`Duration`] and +/// [`u64`]. The values will stay valid. #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord, Default)] #[cfg_attr( feature = "serde", @@ -16,25 +48,41 @@ const MAX: u64 = i64::MAX as u64; pub struct MprisDuration(u64); impl MprisDuration { + /// Lossily creates a new valid [`MprisDuration`] from a [`u64`]. + /// + /// If the value is too big it will be reduced to [`i64::MAX`]. If you want a lossless + /// conversion that can fail you should use [`TryFrom`]. pub fn new_from_u64(micros: u64) -> Self { Self(micros.clamp(0, MAX)) } + /// Lossily creates a new valid [`MprisDuration`] from a [`i64`]. + /// + /// Negative values will be changed to `0`. If you want a lossless conversion that can fail you + /// should use [`TryFrom`]. pub fn new_from_i64(micros: i64) -> Self { Self(micros.clamp(0, i64::MAX) as u64) } + /// Lossily creates a new valid [`MprisDuration`] from a [`Duration`]. + /// + /// If [`Duration`]'s values as microseconds is too big it will be reduced to [`i64::MAX`]. If + /// you want a lossless conversion that can fail you should use [`TryFrom`]. pub fn new_from_duration(duration: Duration) -> Self { Self(duration.as_micros().clamp(0, MAX as u128) as u64) } + /// Creates a new [`MprisDuration`] with the biggest possible value pub fn new_max() -> Self { Self(MAX) } + + /// Returns a [`u64`] equal to the [`MprisDuration`]'s value as microseconds pub fn as_u64(&self) -> u64 { self.0 } + /// Returns a [`i64`] equal to the [`MprisDuration`]'s value as microseconds pub fn as_i64(&self) -> i64 { self.0 as i64 } diff --git a/src/errors.rs b/src/errors.rs index c3bc8fd..d30fa87 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,3 +1,5 @@ +//! The module containing all of the errors. + use std::fmt::Display; pub use zbus::Error; @@ -56,19 +58,25 @@ impl InvalidMprisDuration { } } +/// The main error type for this library, created when when interacting with a +/// [`Player`](crate::Player). +/// +/// This enum contains variants for all of the various ways that something can go wrong. The 2 +/// errors you'll encounter the most are [`DbusError`](Self::DbusError) and +/// [`ParseError`](Self::ParseError). #[derive(Debug, PartialEq, Clone)] pub enum MprisError { /// An error occurred while talking to the D-Bus. DbusError(Error), - /// Failed to parse an enum from a string value received from the [`Player`][crate::Player]. - /// This means that the [`Player`][crate::Player] replied with unexpected data. + /// Failed to parse a string received from the [`Player`][crate::Player]. This means that the + /// [`Player`][crate::Player] replied with unexpected data. ParseError(String), - /// The player doesn't implement the required interface/method/signal + /// The player doesn't implement the required interface/method/signal. Unsupported, - /// One of the given arguments has an invalid value + /// One of the given arguments has an invalid value. InvalidArgument(String), /// Some other unexpected error occurred. diff --git a/src/lib.rs b/src/lib.rs index e8be07d..6ef874d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ -// #![warn(missing_docs)] -#![warn(clippy::print_stdout)] +#![warn(clippy::print_stdout, missing_docs, clippy::todo)] #![deny( missing_debug_implementations, missing_copy_implementations, @@ -12,6 +11,81 @@ unused_qualifications )] +//! +//! # mpris +//! +//! `mpris` is a library for dealing with [MPRIS2][spec]-compatible media players over D-Bus. +//! +//! This would mostly apply to the Linux-ecosystem which is a heavy user of D-Bus. +//! +//! ## Getting started +//! +//! Some hints on how to use this library: +//! +//! 1. Look at the examples under `examples/` in the repo +//! 2. Look at the [`Mpris`] struct +//! 3. Get the first player and make it start playing: +//! ```no_run +//! use mpris::Mpris; +//! +//! #[async_std::main] +//! async fn main() { +//! let mpris = Mpris::new().await.expect("couldn't connect to D-Bus"); +//! let player = match mpris.find_first().await { +//! Ok(result) => match result { +//! Some(player) => player, +//! None => { +//! println!("No player found"); +//! return; +//! } +//! }, +//! Err(err) => { +//! println!("Error occured: {:?}", err); +//! return; +//! } +//! }; +//! match player.play().await { +//! Ok(_) => println!("Made the player play"), +//! Err(err) => println!("Error occured: {:?}", err), +//! } +//! } +//! ``` +//! +//! ## Runtime compatibility +//! +//! The examples will be using [`async_std`][std] but this library does not require a specific +//! runtime. When used with a runtime that isn't [`Tokio`][tokio] it will spawn a thread for the +//! background tasks. If you want to prevent that you should "tick" the internal executor with your +//! runtime like this: +//! +//! ```no_run +//! use mpris::Mpris; +//! use zbus::connection::Builder; +//! +//! #[async_std::main] +//! async fn main() { +//! let conn = Builder::session() +//! .unwrap() +//! .internal_executor(false) // The important part +//! .build() +//! .await +//! .unwrap(); +//! let c = conn.clone(); +//! async_std::task::spawn(async move { +//! loop { +//! c.executor().tick().await; +//! } +//! }); +//! let mpris = Mpris::new_from_connection(conn).await.unwrap(); +//! +//! // The rest of your code here +//! } +//! ``` +//! +//! [spec]: https://specifications.freedesktop.org/mpris-spec/latest/ +//! [std]: https://docs.rs/async-std/latest/async_std/ +//! [tokio]: https://docs.rs/tokio/latest/tokio/ + use std::collections::VecDeque; use std::fmt::{Debug, Display}; use std::future::Future; @@ -26,7 +100,7 @@ use zbus::{ mod duration; pub mod errors; -mod metadata; +pub mod metadata; mod player; mod playlist; mod proxies; @@ -35,8 +109,10 @@ use errors::*; use crate::proxies::DBusProxy; pub use duration::MprisDuration; +#[doc(inline)] pub use errors::MprisError; -pub use metadata::{Metadata, MetadataIter, MetadataValue, TrackID}; +#[doc(inline)] +pub use metadata::{Metadata, MetadataValue, TrackID}; pub use player::Player; pub use playlist::{Playlist, PlaylistOrdering}; @@ -44,6 +120,20 @@ pub(crate) const MPRIS2_PREFIX: &str = "org.mpris.MediaPlayer2."; type PlayerFuture = Pin> + Send>>; +/// The main struct of the library. Used to find [`Player`]s on a D-Bus connection. +/// +/// All find methods first sort alphabetically by the players' [well known Bus Name][busname] and if +/// any of the [`Player`]s fails to initialize they will immediately return with an [`Err`]. If you +/// want to get all of the [`Player`]s even if one fails to initialize you should use +/// [`into_stream()`][Self::into_stream] and handle the errors. +/// +/// # Find methods return types +/// - [Ok]\([Some]\([Player]\)\): No error happened and a [`Player`] was found +/// - [Ok]\([None]\): No error happened but no [`Player`] was found +/// - [Err]\([MprisError]\): Error occurred while searching, most likely while +/// communicating with the D-Bus server +/// +/// [busname]: https://dbus.freedesktop.org/doc/dbus-tutorial.html#bus-names #[derive(Clone)] pub struct Mpris { connection: Connection, @@ -51,6 +141,10 @@ pub struct Mpris { } impl Mpris { + /// Creates a new [`Mpris`] struct by connecting to the session D-Bus server. + /// + /// Use [`new_from_connection`](Self::new_from_connection) if you want to provide the D-Bus + /// connection yourself. pub async fn new() -> Result { let connection = Connection::session().await?; let dbus_proxy = DBusProxy::new(&connection).await?; @@ -61,6 +155,12 @@ impl Mpris { }) } + /// Creates a new [`Mpris`] struct with the given connection. + /// + /// See [here](crate#runtime-compatibility) for why you would want to use a custom + /// [`Connection`]. + /// + /// Use [`new`](Self::new) if you don't have a need to provide the D-Bus connection yourself. pub async fn new_from_connection(connection: Connection) -> Result { let dbus_proxy = DBusProxy::new(&connection).await?; Ok(Self { @@ -69,14 +169,19 @@ impl Mpris { }) } + /// Gets the [`Connection`] that is used. pub fn get_connection(&self) -> Connection { self.connection.clone() } - pub fn get_executor(&self) -> &'static zbus::Executor { + // Will be used later + #[allow(dead_code)] + /// Gets the internal executor for the [`Connection`]. Can be used to spawn tasks. + pub(crate) fn get_executor(&self) -> &'static zbus::Executor { self.connection.executor() } + /// Returns the first found [`Player`] regardless of state. pub async fn find_first(&self) -> Result, MprisError> { match self.all_player_bus_names().await?.into_iter().next() { Some(bus) => Ok(Some(Player::new(self.connection.clone(), bus).await?)), @@ -84,6 +189,12 @@ impl Mpris { } } + /// Tries to find the "active" [`Player`] in the connection. + /// + /// This method will try to determine which player a user is most likely to use. First it will + /// look for a player with the playback status [`Playing`](PlaybackStatus::Playing), then for a + /// [`Paused`](PlaybackStatus::Paused), then one with any track metadata, after that it will + /// just return the first it finds. pub async fn find_active(&self) -> Result, MprisError> { let mut players = self.into_stream().await?; if players.is_terminated() { @@ -113,6 +224,12 @@ impl Mpris { Ok(first_paused.or(first_with_track).or(first_found)) } + /// Looks for a [`Player`] by it's MPRIS [`Identity`][identity] (case insensitive). + /// + /// See also [`Player::identity()`]. + /// + /// [identity]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:Identity pub async fn find_by_name(&self, name: &str) -> Result, MprisError> { let mut players = self.into_stream().await?; if players.is_terminated() { @@ -126,6 +243,9 @@ impl Mpris { Ok(None) } + /// Finds all available [`Player`]s in the connection. + /// + /// Will return an empty [`Vec`] if there are no players. pub async fn all_players(&self) -> Result, MprisError> { let bus_names = self.all_player_bus_names().await?; let mut players = Vec::with_capacity(bus_names.len()); @@ -135,6 +255,7 @@ impl Mpris { Ok(players) } + /// Gets all of the BusNames that start with the [`MPRIS2_PREFIX`] async fn all_player_bus_names(&self) -> Result>, MprisError> { let mut names: Vec = self .dbus_proxy @@ -149,6 +270,9 @@ impl Mpris { Ok(names) } + /// Creates a [`PlayerStream`] which implements the [`Stream`] trait. + /// + /// For more details see [`PlayerStream`]'s documentation. pub async fn into_stream(&self) -> Result { let buses = self.all_player_bus_names().await?; Ok(PlayerStream::new(self.connection.clone(), buses)) @@ -163,6 +287,32 @@ impl Debug for Mpris { } } +/// Lazily returns the [`Player`]s on the connection. +/// +/// Implements the [`Stream`] trait which is the async version of [`Iterator`]. It is recommended to +/// use the [`futures_util`] or [`futures_lite`][lite] crate which provide useful traits for streams. +/// +/// Note that [`PlayerStream`] will only yield the [`Player`]s that were present when it was created. +/// +/// ```no_run +/// use futures_util::StreamExt; +/// use mpris::Mpris; +/// +/// #[async_std::main] +/// async fn main() { +/// let mpris = Mpris::new().await.unwrap(); +/// let mut stream = mpris.into_stream().await.unwrap(); +/// +/// while let Some(result) = stream.next().await { +/// match result { +/// Ok(player) => {}, // Do something with Player +/// Err(err) => {}, // Deal with the error +/// } +/// } +/// } +/// ``` +/// +/// [lite]: https://docs.rs/futures-lite/latest/futures_lite/ pub struct PlayerStream { connection: Connection, buses: VecDeque>, @@ -170,6 +320,10 @@ pub struct PlayerStream { } impl PlayerStream { + /// Creates a new [`PlayerStream`]. + /// + /// There should be no need to use this directly and instead you should use + /// [`Mpris::into_stream`]. pub fn new(connection: Connection, buses: Vec>) -> Self { let buses = VecDeque::from(buses); Self { @@ -230,12 +384,13 @@ impl Debug for PlayerStream { } } -#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] -/// The [`Player`]'s playback status +/// The [`Player`]'s playback status. /// /// See: [MPRIS2 specification about `PlaybackStatus`][playback_status] /// -/// [playback_status]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Enum:Playback_Status +/// [playback_status]: +/// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Enum:Playback_Status +#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] pub enum PlaybackStatus { /// A track is currently playing. Playing, @@ -259,6 +414,7 @@ impl ::std::str::FromStr for PlaybackStatus { } impl PlaybackStatus { + /// Returns it's value as a &[str] pub fn as_str(&self) -> &str { match self { PlaybackStatus::Playing => "Playing", @@ -274,20 +430,21 @@ impl Display for PlaybackStatus { } } -#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] /// A [`Player`]'s looping status. /// /// See: [MPRIS2 specification about `Loop_Status`][loop_status] /// -/// [loop_status]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Enum:Loop_Status +/// [loop_status]: +/// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Enum:Loop_Status +#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] pub enum LoopStatus { - /// The playback will stop when there are no more tracks to play + /// The playback will stop when there are no more tracks to play. None, - /// The current track will start again from the begining once it has finished playing + /// The current track will start again from the beginning once it has finished playing. Track, - /// The playback loops through a list of tracks + /// The playback loops through a list of tracks. Playlist, } @@ -305,6 +462,7 @@ impl ::std::str::FromStr for LoopStatus { } impl LoopStatus { + /// Returns it's value as a &[str] pub fn as_str(&self) -> &str { match self { LoopStatus::None => "None", diff --git a/src/metadata.rs b/src/metadata.rs index 5c55901..50f9174 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -1,7 +1,9 @@ +//! The module containing the metadata related types. +#[allow(clippy::module_inception)] mod metadata; mod track_id; mod values; -pub use self::metadata::{Metadata, MetadataIter}; +pub use self::metadata::{Metadata, MetadataIter, RawMetadata}; pub use self::track_id::TrackID; pub use self::values::MetadataValue; diff --git a/src/metadata/metadata.rs b/src/metadata/metadata.rs index 0cb8824..14518a1 100644 --- a/src/metadata/metadata.rs +++ b/src/metadata/metadata.rs @@ -3,7 +3,8 @@ use std::{collections::HashMap, iter::FusedIterator}; use super::{MetadataValue, TrackID}; use crate::{errors::InvalidMetadata, MprisDuration}; -type RawMetadata = HashMap; +/// The type of the metadata returned from [`Player::raw_metadata()`][crate::Player::raw_metadata]. +pub type RawMetadata = HashMap; /// Macro that auto implements useful things for Metadata without needing to repeat the fields every time /// while preserving documentation for the fields @@ -42,13 +43,17 @@ macro_rules! gen_metadata_struct { $( $(#[$field_meta])* #[doc=""] - #[doc=stringify!($key)] - pub $field: Option<$type> + #[doc=concat!("The `", stringify!($key), "` field from the guidelines.")] + pub $field: Option<$type> ),*, + /// The rest of the metadata not specified in the guidelines. pub $others_name: RawMetadata, } impl $name { + /// Creates a new empty [`Metadata`]. + /// + /// Same as using `Metadata::default()`. pub fn new() -> Self { Self { $($field: None),*, @@ -56,11 +61,15 @@ macro_rules! gen_metadata_struct { } } + /// Checks if it contains any metadata. pub fn is_empty(&self) -> bool { $(self.$field.is_none())&&* && self.$others_name.is_empty() } + /// Checks if it's valid. + /// + /// See [here][Self#validity] for the requirements. pub fn is_valid(&self) -> bool { if self.is_empty() { true @@ -69,6 +78,16 @@ macro_rules! gen_metadata_struct { } } + /// Gets the field name as specified in the [guide] for the given [`Metadata`] field. + /// + /// Returns [`None`] if such a [`Metadata`] field doesn't exist. + /// ``` + /// use mpris::Metadata; + /// + /// assert_eq!(Metadata::get_metadata_key("lyrics"), Some("xesam:asText")); + /// assert_eq!(Metadata::get_metadata_key("invalid_field"), None); + /// ``` + /// [guide]: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ pub fn get_metadata_key(field: &str) -> Option<&str> { match field { $(stringify!($field) => Some($key)),*, @@ -76,6 +95,42 @@ macro_rules! gen_metadata_struct { } } + /// Lossily converts from [`RawMetadata`] into [`Metadata`] + /// + /// Similar to [TryFrom]<[RawMetadata]> but the requirements mentioned + /// [here][Self#validity] are not checked so it can't fail. If a field mentioned in the + /// [guidelines] has the wrong type it will be turned into [`None`]. This is useful when + /// used together with [`Player::raw_metadata()`][crate::Player::raw_metadata] in the + /// case your player doesn't follow the [guidelines] and for example sends over some + /// data in the wrong type. + /// ```no_run + /// use mpris::{Metadata, Mpris}; + /// + /// async_std::task::block_on(async { + /// let mpris = Mpris::new().await.unwrap(); + /// let player = mpris.find_first().await.unwrap().unwrap(); + /// let meta = Metadata::from_raw_lossy(player.raw_metadata().await.unwrap()); + /// }) + /// ``` + /// + /// ``` + /// use std::collections::HashMap; + /// use mpris::{Metadata, metadata::RawMetadata}; + /// + /// let mut raw_meta: RawMetadata = HashMap::new(); + /// // Wrong type for title + /// raw_meta.insert(String::from("xesam:title"), 0_u64.into()); + /// raw_meta.insert(String::from("xesam:comment"), String::from("Some comment").into()); + /// let wrong_meta = Metadata::from_raw_lossy(raw_meta); + /// + /// // Creates fine even though the field had the wrong type and track_id is not present + /// assert!(!wrong_meta.is_valid()); + /// assert!(wrong_meta.track_id.is_none()); + /// // Wrong value got turned into None + /// assert!(wrong_meta.title.is_none()) + /// ``` + /// + /// [guidelines]: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ pub fn from_raw_lossy(mut raw: RawMetadata) -> Self { Self { $($field: raw.remove($key).and_then(|v| <$type>::try_from(v).ok())),*, @@ -137,39 +192,142 @@ macro_rules! gen_metadata_struct { }} gen_metadata_struct!( + /// A struct that represents metadata for a track. + /// + /// It follows the [MPRIS v2 metadata guidelines][guide]. It can be obtained from + /// [`Player::metadata()`][crate::Player::metadata] but it can also be created from any + /// [`RawMetadata`] that meets the requirements mentioned below. The metadata fields included in + /// the guidelines are assigned to struct fields for easier access and are type checked while + /// all other metadata fields are held in the [`others`][Self::others] field as [`RawMetadata`]. + /// + /// # Validity + /// + /// For [`Metadata`] to be valid it has to be empty or it needs to at least contain the + /// `"mpris:trackid"` field ([`track_id`][Metadata::track_id]) which has to be a valid + /// [`TrackID`]. All the other fields are optional but they need to be the right type. + /// [TryFrom]<[RawMetadata]> will fail if these requirements are not met. The + /// [`others`][Self::others] field is not checked in any way. + /// + /// ``` + /// use std::collections::HashMap; + /// use mpris::{Metadata, TrackID, metadata::RawMetadata}; + /// + /// let mut raw_meta: RawMetadata = HashMap::new(); + /// + /// // Empty is valid + /// assert!(Metadata::try_from(raw_meta.clone()).is_ok()); + /// + /// // Adding any fields without adding track_id will fail + /// raw_meta.insert(String::from("some_field"), String::from("Some value").into()); + /// assert!(Metadata::try_from(raw_meta.clone()).is_err()); + /// + /// // A valid track_id is present but a field from the guidelines has a wrong type + /// raw_meta.insert( + /// String::from("mpris:trackid"), + /// TrackID::try_from("/valid/path").unwrap().into(), + /// ); + /// raw_meta.insert(String::from("xesam:trackNumber"), String::new().into()); + /// assert!(Metadata::try_from(raw_meta.clone()).is_err()); + /// + /// // If we remove the invalid type it will be valid + /// raw_meta.remove("xesam:trackNumber"); + /// assert!(Metadata::try_from(raw_meta).is_ok()); + /// ``` + /// + /// # Miscellaneous features + /// + /// - Can be turned into [`RawMetadata`] using [Into]<[RawMetadata]> + /// - Implements [`IntoIterator`], see [`MetadataIter`] for details + /// - Can be lossily converted from [`RawMetadata`] by using + /// [`from_raw_lossy()`][Self::from_raw_lossy] + /// + /// [guide]: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ + /// [object_path]: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path #[derive(Debug, Clone, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), - serde(into = "RawMetadata", try_from = "RawMetadata") - ) - ] + serde(into = "RawMetadata", try_from = "RawMetadata"))] struct Metadata { + /// The album artist(s). "xesam:albumArtist" => album_artists: Vec, + /// The album name. "xesam:album" => album_name: String, + /// The location of an image representing the track or album. Clients should not assume this + /// will continue to exist when the media player stops giving out the URL. "mpris:artUrl" => art_url: String, + /// The track artist(s). "xesam:artist" => artists: Vec, + /// The speed of the music, in beats per minute. "xesam:audioBPM" => audio_bpm: u64, + /// An automatically-generated rating, based on things such as how often it has been played. + /// This should be in the range 0.0 to 1.0. "xesam:autoRating" => auto_rating: f64, + /// A (list of) freeform comment(s). "xesam:comment" => comments: Vec, + /// The composer(s) of the track. "xesam:composer" => composers: Vec, + /// When the track was created. Usually only the year component will be useful. "xesam:contentCreated" => content_created: String, + /// The disc number on the album that this track is from. "xesam:discNumber" => disc_number: u64, + /// When the track was first played. "xesam:firstUsed" => first_used: String, + /// The genre(s) of the track. "xesam:genre" => genres: Vec, + /// When the track was last played. "xesam:lastUsed" => last_used: String, + /// The duration of the track in microseconds. "mpris:length" => length: MprisDuration, + /// The lyricist(s) of the track. "xesam:lyricist" => lyricists: Vec, + /// The track lyrics. "xesam:asText" => lyrics: String, + /// The track title. "xesam:title" => title: String, + /// A unique identity for this track within the context of an MPRIS object. "mpris:trackid" => track_id: TrackID, + /// The track number on the album disc. "xesam:trackNumber" => track_number: u64, + /// The location of the media file. "xesam:url" => url: String, + /// The number of times the track has been played. "xesam:useCount" => use_count: u64, + /// A user-specified rating. This should be in the range 0.0 to 1.0. "xesam:userRating" => user_rating: f64, others, } ); +/// [`Iterator`] over the fields of [`Metadata`]. +/// +/// Yields the field name as a [`String`] and [Option]<[MetadataValue]> containing the +/// value if present. The [`RawMetadata`] from the [`others`][Metadata::others] field is also +/// included and values from it will always be [Some]\([MetadataValue]\). +/// ``` +/// use mpris::Metadata; +/// +/// let meta = Metadata::new(); +/// for (field, value) in meta { +/// // Do something +/// } +/// ``` +/// **Note**: the field names from this iterator are the actual field names of the [`Metadata`] +/// struct. If you want the field names from the [guidelines] you can use +/// [`get_metadata_key()`][Metadata::get_metadata_key] together with this iterator: +/// ``` +/// use mpris::Metadata; +/// +/// let meta = Metadata::new(); +/// for (field, value) in meta { +/// let field_key = match Metadata::get_metadata_key(&field) { +/// Some(s) => s.to_string(), +/// None => field, +/// }; +/// // Do something +/// } +/// ``` +/// +/// [guidelines]: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ #[derive(Debug)] pub struct MetadataIter { values: std::vec::IntoIter<(&'static str, Option)>, diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index c960961..cdb0226 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -5,33 +5,55 @@ use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value}; use super::MetadataValue; use crate::errors::InvalidTrackID; -const NO_TRACK: &str = "/org/mpris/MediaPlayer2/TrackList/NoTrack"; - +/// A struct that represents a valid MPRIS Track_Id. +/// +/// > Unique track identifier. +/// > If the media player implements the TrackList interface and allows the same track to appear +/// > multiple times in the tracklist, this must be unique within the scope of the tracklist. +/// +/// This type is checked on creation. It must be a [valid D-Bus object path][object_path] and it +/// can't begin with `"/org/mpris"` besides the special [`NO_TRACK`][Self::NO_TRACK] value which you can get by +/// using [`no_track()`](Self::no_track). +/// +/// See [this link for the details][track_id]. +/// +/// [track_id]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Simple-Type:Track_Id +/// [object_path]: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))] pub struct TrackID(OwnedObjectPath); impl TrackID { + /// The special "NoTrack" value + pub const NO_TRACK: &str = "/org/mpris/MediaPlayer2/TrackList/NoTrack"; + + /// Tries to create a new [`TrackID`] + /// + /// This is the same as using [TryFrom]<[String]> pub fn new(id: String) -> Result { Self::try_from(id) } + /// Creates a [`TrackID`] with the special [`NO_TRACK`][Self::NO_TRACK] value. pub fn no_track() -> Self { // We know it's a valid path so it's safe to skip the check Self(OwnedObjectPath::from( - ObjectPath::from_static_str_unchecked(NO_TRACK), + ObjectPath::from_static_str_unchecked(Self::NO_TRACK), )) } + /// Checks if [`TrackID`] is the special [`NO_TRACK`][Self::NO_TRACK] value. pub fn is_no_track(&self) -> bool { - self.as_str() == NO_TRACK + self.as_str() == Self::NO_TRACK } + /// Gets the D-Bus object path value as a &[`str`]. pub fn as_str(&self) -> &str { self.0.as_str() } + /// Creates a borrowed [`ObjectPath`] for this [`TrackID`]. pub fn as_object_path(&self) -> ObjectPath<'_> { self.0.as_ref() } @@ -47,7 +69,7 @@ fn check_start(s: T) -> Result where T: Deref, { - if s.starts_with("/org/mpris") && s.deref() != NO_TRACK { + if s.starts_with("/org/mpris") && s.deref() != TrackID::NO_TRACK { Err(InvalidTrackID::from( "TrackID can't start with \"/org/mpris\"", )) @@ -193,7 +215,7 @@ mod tests { fn no_track() { let track = TrackID::no_track(); let manual = TrackID(OwnedObjectPath::from( - ObjectPath::from_static_str_unchecked(NO_TRACK), + ObjectPath::from_static_str_unchecked(TrackID::NO_TRACK), )); assert!(track.is_no_track()); @@ -208,7 +230,7 @@ mod tests { assert!(check_start("").is_ok()); assert!(check_start("/org/mpris").is_err()); assert!(check_start("/org/mpris/more/path").is_err()); - assert!(check_start(NO_TRACK).is_ok()); + assert!(check_start(TrackID::NO_TRACK).is_ok()); } #[test] diff --git a/src/metadata/values.rs b/src/metadata/values.rs index 6661033..e40bec0 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -2,14 +2,17 @@ use zbus::zvariant::{OwnedValue, Value}; use crate::errors::InvalidMetadataValue; -/* -* Subset of DBus data types that are commonly used in MPRIS metadata, and a boolean variant as it -* seems likely to be used in some custom metadata. -* -* See https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ -*/ +/// Subset of [DBus data types][dbus_types] that are commonly used in MPRIS metadata. +/// +/// See [this link][meta_spec] for examples of metadata values. +/// +/// Note that 16-bit and 32-bit integers get turned into their 64-bit version for convenience. +/// +/// [dbus_types]: https://dbus.freedesktop.org/doc/dbus-specification.html#type-system +/// [meta_spec]: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ #[derive(Debug, Clone, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[allow(missing_docs)] pub enum MetadataValue { Boolean(bool), Float(f64), @@ -21,12 +24,33 @@ pub enum MetadataValue { } impl MetadataValue { + /// Tries to turn itself into a non-empty [`String`] + /// + /// Will succeed if it's a non-empty [`String`](Self::String) variant or a [`Strings`](Self::Strings) + /// variant which only contains 1 non-empty [`String`] pub fn into_nonempty_string(self) -> Option { String::try_from(self) .ok() .and_then(|s| if s.is_empty() { None } else { Some(s) }) } + /// Tries to turn itself into a [`i64`] + /// + /// Will succeed if it's a [`SignedInt`](Self::SignedInt) or a + /// [`UnsignedInt`](Self::UnsignedInt) variant. Note that in the second case it will change the + /// value so that it fits into a [`i64`]. If you don't want the value to get changed you should + /// use the [`TryInto`] method. + /// ``` + /// use mpris::MetadataValue; + /// + /// let m = MetadataValue::UnsignedInt(u64::MAX); + /// // into_i64 decreases the number so that it fits + /// assert_eq!(m.into_i64(), Some(i64::MAX)); + /// + /// let m = MetadataValue::UnsignedInt(u64::MAX); + /// // TryFrom fails if it's too big + /// assert!(i64::try_from(m).is_err()); + /// ``` pub fn into_i64(self) -> Option { match self { MetadataValue::SignedInt(i) => Some(i), @@ -35,6 +59,23 @@ impl MetadataValue { } } + /// Tries to turn itself into a [`u64`] + /// + /// Will succeed if it's a [`UnsignedInt`](Self::UnsignedInt) or a + /// [`SignedInt`](Self::SignedInt) variant. Note that in the second case it will change the + /// value to `0` if it's negative. If you don't want the value to get changed you should use the + /// [`TryInto`] method. + /// ``` + /// use mpris::MetadataValue; + /// + /// let m = MetadataValue::SignedInt(-1); + /// // into_u64 changes negative numbers to 0 + /// assert_eq!(m.into_u64(), Some(0)); + /// + /// let m = MetadataValue::SignedInt(-1); + /// // TryFrom fails if it's negative + /// assert!(u64::try_from(m).is_err()); + /// ``` pub fn into_u64(self) -> Option { match self { MetadataValue::SignedInt(i) if i < 0 => Some(0), diff --git a/src/player.rs b/src/player.rs index 2e2c0c6..aec17f4 100644 --- a/src/player.rs +++ b/src/player.rs @@ -4,12 +4,213 @@ use futures_util::try_join; use zbus::{names::BusName, Connection}; use crate::{ - metadata::MetadataValue, + metadata::{MetadataValue, RawMetadata}, proxies::{MediaPlayer2Proxy, PlayerProxy, PlaylistsProxy, TrackListProxy}, LoopStatus, Metadata, MprisDuration, MprisError, PlaybackStatus, Playlist, PlaylistOrdering, TrackID, MPRIS2_PREFIX, }; +/// Struct that represents a player connected to the D-Bus server. Can be used to query and control +/// a player. +/// +/// The easiest way to create a [`Player`] is to use one of [`Mpris`][crate::Mpris]'s find methods, +/// see the [crate documentation][crate#getting-started] for examples. +/// +/// # Bus Name +/// +/// ## Well-know +/// +/// The [`Player`] created through [`Mpris`][crate::Mpris] is bound to it's "well-known" Bus Name +/// and not it's "unique" Bus Name (see [here][bus] for details) which means that an instance of +/// [`Player`] can still be used even after the player restarts. +/// ```no_run +/// use mpris::Mpris; +/// +/// #[async_std::main] +/// async fn main() { +/// let mpris = Mpris::new().await.unwrap(); +/// let vlc = mpris +/// .find_by_name("VLC media player") +/// .await +/// .unwrap() +/// .unwrap(); +/// // VLC restarts here +/// vlc.play().await.expect("will not panic"); +/// } +/// ``` +/// +/// ## Unique +/// +/// If you want to create a [`Player`] that's bound to a unique name you can use +/// [`new()`][Player::new] directly. In that case the instance of [`Player`] will stop working if +/// the player disconnects from the D-Bus for any reason. +/// ```no_run +/// use mpris::{Mpris, Player}; +/// use zbus::names::BusName; +/// #[async_std::main] +/// async fn main() { +/// // To create the connection +/// let mpris = Mpris::new().await.unwrap(); +/// // A "unique" Bus Name +/// let unique_name = BusName::try_from(":1.123").unwrap(); +/// let vlc = Player::new(mpris.get_connection(), unique_name) +/// .await +/// .unwrap(); +/// +/// // VLC restarts here +/// vlc.play().await.expect("will panic") +/// } +/// ``` +/// +/// # Interfaces +/// +/// The [MPRIS specification][spec] contains 4 interfaces: +/// +/// ## [org.mpris.MediaPlayer2][mp2] +/// +///
Index of methods for this interface +/// +/// ### Methods +/// - `Raise`: [`raise()`][Self::raise] +/// - `Quit`: [`quit()`][Self::quit] +/// +/// ### Properties +/// - `CanQuit`: [`can_quit()`][Self::can_quit] +/// - `CanRaise`: [`can_raise()`][Self::can_raise] +/// - `DesktopEntry`: [`desktop_entry()`][Self::desktop_entry] +/// - `Fullscreen`: [`get_fullscreen()`][Self::get_fullscreen] / +/// [`set_fullscreen()`][Self::set_fullscreen] +/// - `CanSetFullscreen`: [`can_set_fullscreen()`][Self::can_set_fullscreen] +/// - `HasTrackList`: [`has_track_list()`][Self::has_track_list] +/// - `Identify`: [`identity()`][Self::identity] +/// - `SupportedMimeTypes`: [`supported_mime_types()`][Self::supported_mime_types] +/// - `SupportedUriSchemes`: [`supported_uri_schemes()`][Self::supported_uri_schemes] +/// +///
+/// +/// ## [org.mpris.MediaPlayer2.Player][player] +/// +/// > This interface implements the methods for querying and providing basic control over what is +/// > currently playing. +/// +///
Index of methods for this interface +/// +/// ### Methods +/// - `Next`: [`next()`][Self::next] +/// - `Previous`: [`previous()`][Self::previous] +/// - `Play`: [`play()`][Self::play] +/// - `Pause`: [`pause()`][Self::pause] +/// - `PlayPause`: [`play_pause()`][Self::play_pause] +/// - `Stop`: [`stop()`][Self::stop] +/// - `Seek`: [`seek()`][Self::seek] / [`seek_forwards()`][Self::seek_forwards] / +/// [`seek_backwards()`][Self::seek_backwards] +/// - `SetPosition`: [`set_position()`][Self::set_position] +/// - `OpenUri`: [`open_uri()`][Self::open_uri] +/// +/// ### Properties +/// - `CanControl`: [`can_control()`][Self::can_control] +/// - `CanGoNext`: [`can_go_next()`][Self::can_go_next] +/// - `CanGoPrevious`: [`can_go_previous()`][Self::can_go_previous] +/// - `CanPlay`: [`can_play()`][Self::can_play] +/// - `CanPause`: [`can_pause()`][Self::can_pause] +/// - `CanSeek`: [`can_seek()`][Self::can_seek] +/// - `LoopStatus`: [`get_loop_status()`][Self::get_loop_status] / +/// [`set_loop_status()`][Self::set_loop_status] +/// - `MaximumRate`: [`maximum_rate()`][Self::maximum_rate] +/// - `MinimumRate`: [`minimum_rate()`][Self::minimum_rate] +/// - `Metadata`: [`metadata()`][Self::metadata] / [`raw_metadata()`][Self::raw_metadata] +/// - `PlaybackStatus`: [`playback_status()`][Self::playback_status] +/// - `Position`: [`get_position()`][Self::get_position] +/// - `Rate`: [`get_playback_rate()`][Self::get_playback_rate] / +/// [`set_playback_rate()`][Self::set_playback_rate] +/// - `Shuffle`: [`get_shuffle()`][Self::get_shuffle] / [`set_shuffle()`][Self::set_shuffle] +/// - `Volume`: [`get_volume()`][Self::get_volume] / [`set_volume()`][Self::set_volume] +/// +///
+/// +/// ## [org.mpris.MediaPlayer2.TrackList][tracklist] +/// +/// **This is an optional interface.** +/// +/// > Provides access to a short list of tracks which were recently played or will be played +/// > shortly. This is intended to provide context to the currently-playing track, rather than +/// > giving complete access to the media player's playlist. +/// > +/// > Example use cases are the list of tracks from the same album as the currently playing song or +/// > the Rhythmbox play queue. +/// > +/// > Each track in the tracklist has a unique identifier. The intention is that this uniquely +/// > identifies the track within the scope of the tracklist. In particular, if a media item (a +/// > particular music file, say) occurs twice in the track list, each occurrence should have a +/// > different identifier. If a track is removed from the middle of the playlist, it should not +/// > affect the track ids of any other tracks in the tracklist. +/// > +/// > As a result, the traditional track identifiers of URLs and position in the playlist cannot be +/// > used. Any scheme which satisfies the uniqueness requirements is valid, as clients should not +/// > make any assumptions about the value of the track id beyond the fact that it is a unique +/// > identifier. +/// > +/// > Note that the (memory and processing) burden of implementing the TrackList interface and +/// > maintaining unique track ids for the playlist can be mitigated by only exposing a subset of +/// > the playlist when it is very long (the 20 or so tracks around the currently playing track, for +/// > example). This is a recommended practice as the tracklist interface is not designed to enable +/// > browsing through a large list of tracks, but rather to provide clients with context about the +/// > currently playing track. +/// +///
Index of methods for this interface +/// +/// ### Methods +/// - `AddTrack`: [`add_track()`][Self::add_track] +/// - `GetTracksMetadata`: [`get_tracks_metadata()`][Self::get_tracks_metadata] +/// - `GoTo`: [`go_to()`][Self::go_to] +/// - `RemoveTrack`: [`remove_track()`][Self::remove_track] +/// +/// ### Properties +/// - `CanEditTracks`: [`can_edit_tracks()`][Self::can_edit_tracks] +/// - `Tracks`: [`tracks()`][Self::tracks] +/// +///
+/// +/// ## [org.mpris.MediaPlayer2.Playlists][playlists] +/// +/// **This is an optional interface.** +/// +/// > Provides access to the media player's playlists. +/// +///
Index of methods for this interface +/// +/// ### Methods +/// - `ActivatePlaylist`: [`activate_playlist()`][Self::activate_playlist] +/// - `GetPlaylists`: [`get_playlists()`][Self::get_playlists] +/// +/// ### Properties +/// - `ActivePlaylist`: [`active_playlist()`][Self::active_playlist] +/// - `Orderings`: [`orderings()`][Self::orderings] +/// - `Orderings`: [`playlist_count()`][Self::playlist_count] +/// +///
+/// +/// ## Support checking +/// +/// Interfaces are checked by calling one of their methods only when [`Player`] gets created. This +/// means that if for some reason the player implements an interface at a later point the created +/// [`Player`] will not know about it. +/// +/// Support for the 2 optional interfaces can be checked with: +/// - [`supports_track_list_interface()`][Self::supports_track_list_interface] +/// - [`supports_playlists_interface()`][Self::supports_playlists_interface] +/// +///
Note that just because an interface is implemented it doesn't mean that the +/// player implemented it correctly, even in the case of the required ones. Methods might be missing +/// or it might return wrong data. This library is not trying to correct those mistakes and will +/// simply return an error.
+/// +/// [spec]: https://specifications.freedesktop.org/mpris-spec/latest +/// [mp2]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html +/// [player]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html +/// [tracklist]: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html +/// [playlists]: https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html +/// [bus]: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-bus #[derive(Clone)] pub struct Player { bus_name: BusName<'static>, @@ -20,6 +221,11 @@ pub struct Player { } impl Player { + /// Creates a new [`Player`] for the given [`Connection`] and [`BusName`]. + /// + /// In most cases there is no need to create [`Player`]s directly, instead you should create + /// them through [`Mpris`][crate::Mpris]. Doing it this way however allows you to bind the + /// [`Player`] to a unique Bus Name. See [this][Self#bus-name] for a simple explanation. pub async fn new( connection: Connection, bus_name: BusName<'static>, @@ -46,29 +252,60 @@ impl Player { }) } - pub async fn supports_track_list(&self) -> Result { + /// Returns the player's [`HasTrackList`][track_list] property. + /// + /// **Note**: this property is reported by the player and might not be accurate. A better way to + /// check is [`supports_track_list_interface()`][Self::supports_track_list_interface] + /// + /// [track_list]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:HasTrackList + pub async fn has_track_list(&self) -> Result { Ok(self.mp2_proxy.has_track_list().await?) } - pub fn supports_playlist_interface(&self) -> bool { - self.playlist_proxy.is_some() - } - + /// Checks if the [`Player`] has support for the + /// [`TrackList`][Self#orgmprismediaplayer2tracklist] interface. + /// + /// See the [interfaces section for more details][Self#interfaces] pub fn supports_track_list_interface(&self) -> bool { self.track_list_proxy.is_some() } + /// Checks if the [`Player`] has support for the + /// [`Playlists`][Self#orgmprismediaplayer2playlists] interface. + /// + /// See the [interfaces section for more details][Self#interfaces] + pub fn supports_playlists_interface(&self) -> bool { + self.playlist_proxy.is_some() + } + + /// Queries the player for current metadata. + /// + /// See [`Metadata`] for more information. pub async fn metadata(&self) -> Result { Ok(self.raw_metadata().await?.try_into()?) } - pub async fn raw_metadata(&self) -> Result, MprisError> { + /// Queries the player for current metadata and returns it as is. + /// + /// Similar to [`metadata()`][Self::metadata] but doesn't perform any checks or conversions. See + /// [`Metadata::from_raw_lossy()`] for details. + pub async fn raw_metadata(&self) -> Result { let data = self.player_proxy.metadata().await?; let raw: HashMap = data.into_iter().map(|(k, v)| (k, v.into())).collect(); Ok(raw) } + /// Checks if the player is still connected. + /// + /// Simply pings the player and checks if it responds. + /// + /// # Returns + /// + /// - [Some]\([true]\): player is connected + /// - [Some]\([false]\): player is not connected + /// - [`Err`]: some other issue occurred while trying to ping the player pub async fn is_running(&self) -> Result { match self.mp2_proxy.ping().await { Ok(_) => Ok(true), @@ -86,122 +323,501 @@ impl Player { } } + /// Returns the Bus Name of the [`Player`]. + /// + /// See also: [`bus_name_trimmed()`][Self::bus_name_trimmed]. pub fn bus_name(&self) -> &str { self.bus_name.as_str() } + /// Returns the player name part of the player's D-Bus bus name with the MPRIS2 prefix trimmed. + /// + /// Examples: + /// - `org.mpris.MediaPlayer2.io.github.celluloid_player.Celluloid` -> + /// `io.github.celluloid_player.Celluloid` + /// - `org.mpris.MediaPlayer2.Spotify.` -> `Spotify` + /// - `org.mpris.MediaPlayer2.mpv.instance123` -> `mpv.instance123` + /// + /// See also: [`bus_name()`][Self::bus_name]. pub fn bus_name_trimmed(&self) -> &str { self.bus_name().trim_start_matches(MPRIS2_PREFIX) } + /// Sends a `Quit` signal to the player. + /// + /// > Causes the media player to stop running. + /// > + /// > The media player may refuse to allow clients to shut it down. In this case, the CanQuit + /// > property is false and this method does nothing. + /// + /// See also: [MPRIS2 specification about `Quit`][quit] and [`can_quit()`][Self::can_quit]. + /// + /// [quit]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Method:Quit pub async fn quit(&self) -> Result<(), MprisError> { Ok(self.mp2_proxy.quit().await?) } + /// Queries the player to see if it can be asked to quit. + /// + /// > If false, calling Quit will have no effect, and may raise a NotSupported error. If true, + /// > calling Quit will cause the media application to attempt to quit (although it may still + /// > be prevented from quitting by the user, for example). + /// + /// See also: [MPRIS2 specification about `CanQuit`][can_quit] and [`quit()`][Self::quit]. + /// + /// [can_quit]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:CanQuit pub async fn can_quit(&self) -> Result { Ok(self.mp2_proxy.can_quit().await?) } + /// Send a `Raise` signal to the player. + /// + /// > Brings the media player's user interface to the front using any appropriate mechanism + /// > available. + /// > + /// > The media player may be unable to control how its user interface is displayed, or it may + /// > not have a graphical user interface at all. In this case, the CanRaise property is false + /// > and this method does nothing. + /// + /// See also: [MPRIS2 specification about `Raise`][raise] and [`can_raise()`][Self::can_raise]. + /// + /// [raise]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Method:Raise pub async fn raise(&self) -> Result<(), MprisError> { Ok(self.mp2_proxy.raise().await?) } + /// Queries the player to see if it can be raised or not. + /// + /// > If false, calling Raise will have no effect, and may raise a NotSupported error. If true, + /// > calling Raise will cause the media application to attempt to bring its user interface to + /// > the front, although it may be prevented from doing so (by the window manager, for + /// > example). + /// + /// See also: [MPRIS2 specification about `CanRaise`][can_raise] and [`raise()`][Self::raise] + /// + /// [can_raise]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:CanRaise pub async fn can_raise(&self) -> Result { Ok(self.mp2_proxy.can_raise().await?) } - + /// Returns the player's [`DesktopEntry`][entry] property, if supported. + /// + /// > The basename of an installed .desktop file which complies with the Desktop entry + /// > specification, with the ".desktop" extension stripped. + /// > + /// > Example: The desktop entry file is "/usr/share/applications/vlc.desktop", and this + /// > property contains "vlc" + /// + /// [entry]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:DesktopEntry pub async fn desktop_entry(&self) -> Result { Ok(self.mp2_proxy.desktop_entry().await?) } + /// Returns the player's MPRIS [`Identity`][identity]. + /// + /// > A friendly name to identify the media player to users. + /// > + /// > This should usually match the name found in .desktop files + /// > + /// > (eg: "VLC media player"). + /// + /// [identity]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:Identity pub async fn identity(&self) -> Result { Ok(self.mp2_proxy.identity().await?) } + /// Returns the player's [`SupportedMimeTypes`][mime] property. + /// + /// > The mime-types supported by the media player. + /// > + /// > Mime-types should be in the standard format (eg: audio/mpeg or application/ogg). + /// + /// [mime]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:SupportedMimeTypes pub async fn supported_mime_types(&self) -> Result, MprisError> { Ok(self.mp2_proxy.supported_mime_types().await?) } + /// Returns the player's [`SupportedUriSchemes`][uri] property. + /// + /// > The URI schemes supported by the media player. + /// > + /// > This can be viewed as protocols supported by the player in almost all cases. Almost every + /// > media player will include support for the "file" scheme. Other common schemes are "http" + /// > and "rtsp". + /// > + /// > Note that URI schemes should be lower-case. + /// + /// [uri]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:SupportedUriSchemes pub async fn supported_uri_schemes(&self) -> Result, MprisError> { Ok(self.mp2_proxy.supported_uri_schemes().await?) } + /// Returns the player's [`Fullscreen`][full] property. + /// + ///
This property was added in MPRIS 2.2, and not all players will + /// implement it.
+ /// + /// > Whether the media player is occupying the fullscreen. + /// > + /// > This is typically used for videos. A value of true indicates that the media player is + /// > taking up the full screen. + /// > + /// > Media centre software may well have this value fixed to true + /// + /// See also: [`set_fullscreen()`][Self::set_fullscreen] and + /// [`can_set_fullscreen()`][Self::can_set_fullscreen]. + /// + /// [full]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:Fullscreen pub async fn get_fullscreen(&self) -> Result { Ok(self.mp2_proxy.fullscreen().await?) } + /// Asks the player to set the [`Fullscreen`][full] property. + /// + ///
This property was added in MPRIS 2.2, and not all players will + /// implement it.
+ /// + /// See [`get_fullscreen`][Self::get_fullscreen()] and + /// [`can_set_fullscreen()`][Self::can_set_fullscreen] for more information + /// + /// [full]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:Fullscreen pub async fn set_fullscreen(&self, value: bool) -> Result<(), MprisError> { Ok(self.mp2_proxy.set_fullscreen(value).await?) } + /// Queries the player to see if it can be asked to enter fullscreen. + /// + ///
This property was added in MPRIS 2.2, and not all players will + /// implement it.
+ /// + /// > If false, attempting to set Fullscreen will have no effect, and may raise an error. If + /// > true, attempting to set Fullscreen will not raise an error, and (if it is different from + /// > the current value) will cause the media player to attempt to enter or exit fullscreen + /// > mode. + /// > + /// > Note that the media player may be unable to fulfil the request. In this case, the value + /// > will not change. If the media player knows in advance that it will not be able to fulfil + /// > the request, however, this property should be false. + /// + /// See also: [MPRIS2 specification about `CanSetFullscreen`][can_full], + /// [`get_fullscreen()`][Self::get_fullscreen] and [`set_fullscreen()`][Self::set_fullscreen]. + /// + /// [can_full]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:CanSetFullscreen pub async fn can_set_fullscreen(&self) -> Result { Ok(self.mp2_proxy.can_set_fullscreen().await?) } + /// Queries the player to see if it can be controlled or not. + /// + /// > Whether the media player may be controlled over this interface. + /// > + /// > This property is not expected to change, as it describes an intrinsic capability of the + /// > implementation. + /// > + /// > If this is false, clients should assume that all properties on the + /// > [`org.mpris.MediaPlayer2.Player`][Self#orgmprismediaplayer2player] interface are read-only + /// > (and will raise errors if writing to them is attempted), no methods are implemented and + /// > all other properties starting with "Can" are also false. + /// + /// See also: [MPRIS2 specification about `CanControl`][control]. + /// + /// [control]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanControl pub async fn can_control(&self) -> Result { Ok(self.player_proxy.can_control().await?) } + /// Sends a [`Next`][next] signal to the player. + /// + /// > Skips to the next track in the tracklist. + /// > + /// > If there is no next track (and endless playback and track repeat are both off), stop + /// > playback. + /// > + /// > If playback is paused or stopped, it remains that way. + /// > + /// > If CanGoNext is false, attempting to call this method should have no effect. + /// + /// See also: [`can_go_next()`][Self::can_go_next]. + /// + /// [next]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Next pub async fn next(&self) -> Result<(), MprisError> { Ok(self.player_proxy.next().await?) } + /// Queries the player to see if it can go to next. + /// + /// > Whether the client can call the Next method on this interface and expect the current track + /// > to change. + /// > + /// > If it is unknown whether a call to Next will be successful (for example, when streaming + /// > tracks), this property should be set to true. + /// > + /// > If CanControl is false, this property should also be false. + /// + /// See also: [MPRIS2 specification about `CanGoNext`][can_next] and [`next()`][Self::next]. + /// + /// [can_next]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanGoNext pub async fn can_go_next(&self) -> Result { Ok(self.player_proxy.can_go_next().await?) } + /// Sends a [`Previous`][prev] signal to the player. + /// + /// > Skips to the previous track in the tracklist. + /// > + /// > If there is no previous track (and endless playback and track repeat are both off), stop + /// > playback. + /// > + /// > If playback is paused or stopped, it remains that way. + /// > + /// > If CanGoPrevious is false, attempting to call this method should have no effect. + /// + /// See also: [`can_go_previous()`][Self::can_go_previous]. + /// + /// [prev]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Previous pub async fn previous(&self) -> Result<(), MprisError> { Ok(self.player_proxy.previous().await?) } + /// Queries the player to see if it can go to previous or not. + /// + /// > Whether the client can call the Previous method on this interface and expect the current + /// > track to change. + /// > + /// > If it is unknown whether a call to Previous will be successful (for example, when + /// > streaming tracks), this property should be set to true. + /// > + /// > If CanControl is false, this property should also be false. + /// + /// See also: [MPRIS2 specification about `CanGoPrevious`][can_prev] and [`previous()`][Self::previous]. + /// + /// [can_prev]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanGoPrevious pub async fn can_go_previous(&self) -> Result { Ok(self.player_proxy.can_go_previous().await?) } + /// Sends a [`Play`][play] signal to the player. + /// + /// > Starts or resumes playback. + /// > + /// > If already playing, this has no effect. + /// > + /// > If paused, playback resumes from the current position. + /// > + /// > If there is no track to play, this has no effect. + /// > + /// > If CanPlay is false, attempting to call this method should have no effect. + /// + /// See also: [`can_play()`][Self::can_play]. + /// + /// [play]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Play pub async fn play(&self) -> Result<(), MprisError> { Ok(self.player_proxy.play().await?) } + /// Queries the player to see if it can play. + /// + /// > Whether playback can be started using Play or PlayPause. + /// > + /// > Note that this is related to whether there is a "current track": the value should not depend on whether the track is currently paused or playing. In fact, if a track is currently playing (and CanControl is true), this should be true. + /// > + /// > If CanControl is false, this property should also be false. + /// + /// See also: [MPRIS2 specification about `CanPlay`][can_play] and [`play()`][Self::play]. + /// + /// [can_play]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanPlay pub async fn can_play(&self) -> Result { Ok(self.player_proxy.can_play().await?) } + /// Sends a [`Pause`][pause] signal to the player. + /// + /// > Pauses playback. + /// > + /// > If playback is already paused, this has no effect. + /// > + /// > Calling Play after this should cause playback to start again from the same position. + /// > + /// > If CanPause is false, attempting to call this method should have no effect. + /// + /// See also: [`can_pause()`][Self::can_pause]. + /// + /// [pause]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Pause pub async fn pause(&self) -> Result<(), MprisError> { Ok(self.player_proxy.pause().await?) } + /// Queries the player to see if it can pause. + /// + /// > Whether playback can be paused using Pause or PlayPause. + /// > + /// > Note that this is an intrinsic property of the current track: its value should not depend + /// > on whether the track is currently paused or playing. In fact, if playback is currently + /// > paused (and CanControl is true), this should be true. + /// > + /// > If CanControl is false, this property should also be false. + /// + /// See also: [MPRIS2 specification about `CanPause`][can_pause] and [`pause()`][Self::pause]. + /// + /// [can_pause]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanPause pub async fn can_pause(&self) -> Result { Ok(self.player_proxy.can_pause().await?) } + /// Sends a [`PlayPause`][play_pause] signal to the player. + /// + /// > Pauses playback. + /// > + /// > If playback is already paused, resumes playback. + /// > + /// > If playback is stopped, starts playback. + /// > + /// > If CanPause is false, attempting to call this method should have no effect and raise an + /// > error. + /// + /// See also: [`can_pause()`][Self::can_pause]. + /// + /// [play_pause]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:PlayPause pub async fn play_pause(&self) -> Result<(), MprisError> { Ok(self.player_proxy.play_pause().await?) } + /// Sends a [`Stop`][stop] signal to the player. + /// + /// > Stops playback. + /// > + /// > If playback is already stopped, this has no effect. + /// > + /// > Calling Play after this should cause playback to start again from the beginning of the + /// > track. + /// > + /// > If CanControl is false, attempting to call this method should have no effect and raise an + /// > error. + /// + /// [stop]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Stop pub async fn stop(&self) -> Result<(), MprisError> { Ok(self.player_proxy.stop().await?) } + /// Sends a [`Seek`][seek] signal to the player. + /// + /// > Seeks forward in the current track by the specified number of microseconds. + /// > + /// > A negative value seeks back. If this would mean seeking back further than the start of the + /// > track, the position is set to 0. + /// > + /// > If the value passed in would mean seeking beyond the end of the track, acts like a call to + /// > Next. + /// > + /// > If the CanSeek property is false, this has no effect. + /// + /// See also: [`can_seek()`][Self::can_seek], [`seek_forwards()`][Self::seek_forwards] and + /// [`seek_backwards()`][Self::seek_backwards]. + /// + /// [seek]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Seek pub async fn seek(&self, offset_in_microseconds: i64) -> Result<(), MprisError> { Ok(self.player_proxy.seek(offset_in_microseconds).await?) } + /// Tells the player to seek forwards. + /// + /// Similar to [`seek()`][Self::seek] but can only seek forwards and uses the more convenient + /// [`MprisDuration`] as an argument. + /// + /// See also: [`seek_backwards()`][Self::seek_backwards] pub async fn seek_forwards(&self, offset: MprisDuration) -> Result<(), MprisError> { Ok(self.player_proxy.seek(offset.into()).await?) } + /// Tells the player to seek backwards. + /// + /// Similar to [`seek()`][Self::seek] but can only seek backwards and uses the more convenient + /// [`MprisDuration`] as an argument. + /// + /// See also: [`seek_forwards()`][Self::seek_forwards] pub async fn seek_backwards(&self, offset: MprisDuration) -> Result<(), MprisError> { Ok(self.player_proxy.seek(-i64::from(offset)).await?) } + /// Queries the player to see if it can seek within the media. + /// + /// > Whether the client can control the playback position using Seek and SetPosition. This may + /// > be different for different tracks. + /// > + /// > If CanControl is false, this property should also be false. + /// + /// See also: [MPRIS2 specification about `CanSeek`][can_seek], [`seek()`][Self::seek], + /// [`seek_forwards()`][Self::seek_forwards], [`seek_backwards()`][Self::seek_backwards]. + /// + /// [can_seek]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanSeek pub async fn can_seek(&self) -> Result { Ok(self.player_proxy.can_seek().await?) } + /// Gets the player's MPRIS [`Position`][position] as a [`MprisDuration`] since the start of the + /// media. + /// + /// > The current track position in microseconds, between 0 and the 'mpris:length' metadata + /// > entry (see [`Metadata`][Metadata]). + /// + /// [position]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Position pub async fn get_position(&self) -> Result { Ok(self.player_proxy.position().await?.try_into()?) } + /// Sets the position of the current track to the given position (as a [`MprisDuration`]). + /// + /// Current [`TrackID`] must be provided to avoid race conditions with the player, in case it + /// changes tracks while the signal is being sent. The special + /// [`"NoTrack"`][TrackID::NO_TRACK] value is not allowed. + /// + /// To obtain the current [`TrackID`] you can use [`metadata()`][Self::metadata] + ///```no_run + /// #[async_std::main] + /// async fn main() { + /// use mpris::Mpris; + /// use std::time::Duration; + /// + /// let mpris = Mpris::new().await.unwrap(); + /// let player = mpris.find_active().await.unwrap().unwrap(); + /// let track_id = player.metadata().await.unwrap().track_id.unwrap(); + /// let _ = player + /// .set_position(&track_id, Duration::from_secs(5).try_into().unwrap()) + /// .await; + /// } + ///``` + /// + /// > If the CanSeek property is false, this has no effect. + /// + /// See also: [MPRIS2 specification about `SetPosition`][set_position] and + /// [`can_seek()`][Self::can_seek]. + /// + /// [set_position]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:SetPosition pub async fn set_position( &self, track_id: &TrackID, @@ -216,10 +832,25 @@ impl Player { .await?) } + /// Gets the player's current loop status. + /// + /// See also: [MPRIS2 specification about `LoopStatus`][loop_status] and [`LoopStatus`][LoopStatus]. + /// + /// [loop_status]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:LoopStatus pub async fn get_loop_status(&self) -> Result { Ok(self.player_proxy.loop_status().await?.parse()?) } + /// Sets the loop status of the player. + /// + /// > If CanControl is false, attempting to set this property should have no effect and raise an + /// > error. + /// + /// See also: [MPRIS2 specification about `LoopStatus`][loop_status] and [`LoopStatus`][LoopStatus]. + /// + /// [loop_status]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:LoopStatus pub async fn set_loop_status(&self, loop_status: LoopStatus) -> Result<(), MprisError> { Ok(self .player_proxy @@ -227,26 +858,108 @@ impl Player { .await?) } + /// Gets the player's current playback status. + /// + /// See also: [MPRIS2 specification about `PlaybackStatus`][playback] and + /// [`PlaybackStatus`][PlaybackStatus]. + /// + /// [playback]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:PlaybackStatus pub async fn playback_status(&self) -> Result { Ok(self.player_proxy.playback_status().await?.parse()?) } + /// Signals the player to open the given `uri`. + /// + /// The argument's uri scheme should be in + /// [`supported_uri_schemes()`][Self::supported_uri_schemes] and the mime-type should be in + /// [`supported_mime_types()`][Self::supported_mime_types] but note that this method does not + /// check for that. It's up to you to verify that. + /// + /// > If the playback is stopped, starts playing + /// > + /// > If the uri scheme or the mime-type of the uri to open is not supported, this method does + /// > nothing and may raise an error. In particular, if the list of available uri schemes is + /// > empty, this method may not be implemented. + /// > + /// > Clients should not assume that the Uri has been opened as soon as this method returns. + /// > They should wait until the mpris:trackid field in the Metadata property changes. + /// > + /// > If the media player implements the [TrackList + /// > interface][Self#orgmprismediaplayer2tracklist], then the opened track should be made part + /// > of the tracklist. + /// + /// See also: [MPRIS2 specification about `OpenUri`][uri]. + /// + /// [uri]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:OpenUri pub async fn open_uri(&self, uri: &str) -> Result<(), MprisError> { Ok(self.player_proxy.open_uri(uri).await?) } + /// Gets the minimum allowed value for playback rate. + /// + /// > The minimum value which the Rate property can take. Clients should not attempt to set the + /// > Rate property below this value. + /// > + /// > Note that even if this value is 0.0 or negative, clients should not attempt to set the + /// > Rate property to 0.0. + /// > + /// > This value should always be 1.0 or less. + /// + /// See also: [MPRIS2 specification about `MinimumRate`][min_rate] and + /// [`set_playback_rate()`][Self::set_playback_rate]. + /// + /// [min_rate]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:MinimumRate pub async fn maximum_rate(&self) -> Result { Ok(self.player_proxy.maximum_rate().await?) } + /// Gets the maximum allowed value for playback rate. + /// + /// > The maximum value which the Rate property can take. Clients should not attempt to set the + /// > Rate property above this value. + /// > + /// > This value should always be 1.0 or greater. + /// + /// See also: [MPRIS2 specification about `MaximumRate`][max_rate] and + /// [`set_playback_rate()`][Self::set_playback_rate]. + /// + /// [max_rate]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:MaximumRate pub async fn minimum_rate(&self) -> Result { Ok(self.player_proxy.minimum_rate().await?) } + /// Returns the player's MPRIS (playback) [`rate`][rate] as a factor. + /// + /// 1.0 would mean normal rate, while 2.0 would mean twice the playback speed. + /// + /// See also: [`set_playback_rate()`][Self::set_playback_rate]. + /// + /// [rate]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Rate pub async fn get_playback_rate(&self) -> Result { Ok(self.player_proxy.rate().await?) } + /// Sets the player's MPRIS (playback) [`rate`][rate] as a factor. + /// + /// > The value must fall in the range described by [`MinimumRate`][Self::minimum_rate] and + /// > [`MaximumRate`][Self::maximum_rate], and must not be 0.0. + /// > + /// > Not all values may be accepted by the media player. It is left to media player + /// > implementations to decide how to deal with values they cannot use; they may either ignore + /// > them or pick a "best fit" value. Clients are recommended to only use sensible fractions or + /// > multiples of 1 (eg: 0.5, 0.25, 1.5, 2.0, etc). + /// + /// **Note**: this method does not check if the argument is between the minimum and maximum. + /// + /// See also: [`get_playback_rate()`][Self::get_playback_rate]. + /// + /// [rate]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Rate pub async fn set_playback_rate(&self, rate: f64) -> Result<(), MprisError> { if rate == 0.0 { return Err(MprisError::InvalidArgument("rate can't be 0.0".to_string())); @@ -254,22 +967,56 @@ impl Player { Ok(self.player_proxy.set_rate(rate).await?) } + /// Gets the player's [`Shuffle`][shuffle] property. + /// + /// > A value of false indicates that playback is progressing linearly through a playlist, + /// > while true means playback is progressing through a playlist in some other order. + /// + /// See also: [`set_shuffle()`][Self::set_shuffle]. + /// + /// [shuffle]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Shuffle pub async fn get_shuffle(&self) -> Result { Ok(self.player_proxy.shuffle().await?) } + /// Sets the [`Shuffle`][shuffle] property of the player. + /// + /// > If CanControl is false, attempting to set this property should have no effect and raise an + /// > error. + /// + /// See also: [`get_shuffle()`][Self::get_shuffle]. + /// + /// [shuffle]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Shuffle pub async fn set_shuffle(&self, shuffle: bool) -> Result<(), MprisError> { Ok(self.player_proxy.set_shuffle(shuffle).await?) } + /// Gets the [`Volume`][vol] of the player. + /// + /// See also: [`set_volume()`][Self::set_volume]. + /// + /// [vol]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Volume pub async fn get_volume(&self) -> Result { Ok(self.player_proxy.volume().await?) } + /// Sets the [`Volume`][vol] of the player. + /// + /// Volume should be between 0.0 and 1.0. Above 1.0 is possible, but not + /// recommended. Negative values will be turned into 0.0 + /// + /// See also: [`get_volume()`][Self::get_volume]. + /// + /// [vol]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Volume pub async fn set_volume(&self, volume: f64) -> Result<(), MprisError> { Ok(self.player_proxy.set_volume(volume).await?) } + /// Shortcut to check if `self.playlist_proxy` is Some fn check_playlist_support(&self) -> Result<&PlaylistsProxy, MprisError> { match &self.playlist_proxy { Some(proxy) => Ok(proxy), @@ -277,6 +1024,20 @@ impl Player { } } + /// Signals the player to activate a given [`Playlist`]. + /// + /// > Starts playing the given playlist. + /// > + /// > It is up to the media player whether this completely replaces the current tracklist, or + /// > whether it is merely inserted into the tracklist and the first track starts. For example, + /// > if the media player is operating in a "jukebox" mode, it may just append the playlist to + /// > the list of upcoming tracks, and skip to the first track in the playlist. + /// + /// See also: [MPRIS2 specification about `ActivatePlaylist`][activate] and + /// [`get_playlists()`][Self::get_playlists] + /// + /// [activate]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html#Method:ActivatePlaylist pub async fn activate_playlist(&self, playlist: &Playlist) -> Result<(), MprisError> { Ok(self .check_playlist_support()? @@ -284,6 +1045,17 @@ impl Player { .await?) } + /// Gets the [`Playlist`]s of the player. + /// + /// `start_index` and `max_count` allow for pagination of the playlists in case the player has a + /// lot of them. + /// The given [`PlaylistOrdering`] should be in the return value of + /// [`orderings()`][Self::orderings] but this method does not check for that. + /// + /// See also: [MPRIS2 specification about `GetPlaylists`][get_playlists]. + /// + /// [get_playlists]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html#Method:GetPlaylists pub async fn get_playlists( &self, start_index: u32, @@ -300,6 +1072,16 @@ impl Player { .collect()) } + /// Gets the currently active [`Playlist`] if any. + /// + /// > Note that this may not have a value even after ActivatePlaylist is called with a valid + /// > playlist id as ActivatePlaylist implementations have the option of simply inserting the + /// > contents of the playlist into the current tracklist. + /// + /// See also: [MPRIS2 specification about `ActivePlaylist`][active]. + /// + /// [active]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html#Property:ActivePlaylist pub async fn active_playlist(&self) -> Result, MprisError> { let result = self.check_playlist_support()?.active_playlist().await?; if result.0 { @@ -309,6 +1091,14 @@ impl Player { } } + /// Gets the [`PlaylistOrdering`]s the player supports. + /// + /// > The available orderings. At least one must be offered. + /// + /// See also: [MPRIS2 specification about `Orderings`][orderings]. + /// + /// [orderings]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html#Property:Orderings pub async fn orderings(&self) -> Result, MprisError> { let result = self.check_playlist_support()?.orderings().await?; let mut orderings = Vec::with_capacity(result.len()); @@ -318,10 +1108,17 @@ impl Player { Ok(orderings) } + /// Gets the number of available playlists. + /// + /// See also: [MPRIS2 specification about `PlaylistCount`][count]. + /// + /// [count]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html#Property:PlaylistCount pub async fn playlist_count(&self) -> Result { Ok(self.check_playlist_support()?.playlist_count().await?) } + /// Shortcut to check if `self.track_list_proxy` is Some fn check_track_list_support(&self) -> Result<&TrackListProxy, MprisError> { match &self.track_list_proxy { Some(proxy) => Ok(proxy), @@ -329,10 +1126,28 @@ impl Player { } } + /// Queries the player to see if it allows changes to its `TrackList`. + /// + /// > If false, calling AddTrack or RemoveTrack will have no effect, and may raise a + /// > NotSupported error. + /// + /// See also: [MPRIS2 specification about `CanEditTracks`][can_edit], + /// [`add_track()`][Self::add_track] and [`remove_track()`][Self::remove_track] . + /// + /// [can_edit]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Property:CanEditTracks pub async fn can_edit_tracks(&self) -> Result { Ok(self.check_track_list_support()?.can_edit_tracks().await?) } + /// Gets the tracks in the current `TrackList` + /// + /// > An array which contains the identifier of each track in the tracklist, in order. + /// + /// See also: [MPRIS2 specification about `Tracks`][tracks]. + /// + /// [tracks]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Property:Tracks pub async fn tracks(&self) -> Result, MprisError> { let result = self.check_track_list_support()?.tracks().await?; let mut track_ids = Vec::with_capacity(result.len()); @@ -342,6 +1157,26 @@ impl Player { Ok(track_ids) } + /// Adds a `uri` to the `TrackList` and optionally set it as current. + /// + /// The `uri` argument's uri scheme should be in + /// [`supported_uri_schemes()`][Self::supported_uri_schemes] and the mime-type should be in + /// [`supported_mime_types()`][Self::supported_mime_types] but note that this method does not + /// check for that. It's up to you to verify that. + /// + /// It is placed after the specified [`TrackID`], if supported by the player. If [`None`] is + /// used then it will be inserted as the first song. + /// + /// > If the CanEditTracks property is false, this has no effect. + /// > + /// > Note: Clients should not assume that the track has been added at the time when this method + /// > returns. + /// + /// See also: [MPRIS2 specification about `AddTrack`][add_track] and + /// [`can_edit_tracks()`][Self::can_edit_tracks]. + /// + /// [add_track]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Method:AddTrack pub async fn add_track( &self, uri: &str, @@ -359,6 +1194,22 @@ impl Player { .await?) } + /// Removes an item from the TrackList. + /// + /// The special [`"NoTrack"`][TrackID::NO_TRACK] value is not allowed as the argument. + /// + /// > If the track is not part of this tracklist, this has no effect. + /// > + /// > If the CanEditTracks property is false, this has no effect. + /// > + /// > Note: Clients should not assume that the track has been removed at the time when this + /// > method returns. + /// + /// See also: [MPRIS2 specification about `RemoveTrack`][remove] and + /// [`can_edit_tracks()`][Self::can_edit_tracks]. + /// + /// [remove]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Method:RemoveTrack pub async fn remove_track(&self, track_id: &TrackID) -> Result<(), MprisError> { if track_id.is_no_track() { return Err(MprisError::track_id_is_no_track()); @@ -369,6 +1220,15 @@ impl Player { .await?) } + /// Go to a specific track on the [`Player`]'s `TrackList`. + /// + /// If the given [`TrackID`] is not part of the player's `TrackList` it will have no effect. + /// The special [`"NoTrack"`][TrackID::NO_TRACK] value is not allowed as the argument. + /// + /// See also: [MPRIS2 specification about `GoTo`][go_to]. + /// + /// [go_to]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Method:GoTo pub async fn go_to(&self, track_id: &TrackID) -> Result<(), MprisError> { if track_id.is_no_track() { return Err(MprisError::track_id_is_no_track()); @@ -379,6 +1239,14 @@ impl Player { .await?) } + /// Gets the [`Metadata`] for the given [`TrackID`]s. + /// + /// Will fail if any of the tracks has invalid metadata. + /// + /// See also: [MPRIS2 specification about `GetTracksMetadata`][get_meta]. + /// + /// [get_meta]: + /// https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Method:GetTracksMetadata pub async fn get_tracks_metadata( &self, tracks: &[TrackID], diff --git a/src/playlist.rs b/src/playlist.rs index 2c8b38b..a1c3f65 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -4,6 +4,25 @@ use zbus::zvariant::{ObjectPath, OwnedObjectPath}; use crate::{InvalidPlaylist, InvalidPlaylistOrdering}; +/// A data structure describing a playlist. +/// +/// It represents a [Playlist][playlist] type from the [MediaPlayer2.Playlists][interface] interface. +/// It contains: +/// - the unique id of the playlist: a [valid D-Bus object path][object_path] which, unlike +/// [`TrackID`][crate::TrackID], is not tied to the player's current track list and should +/// stay the same even if the playlist gets edited +/// - the name of the playlist +/// - an optional icon url +/// +/// It can be obtain from [`Player::active_playlist()`][crate::Player::active_playlist] and +/// [`Player::get_playlists()`][crate::Player::get_playlists]. +/// +/// **Note**: currently the name and icon url will not get updated if they get changed. If they +/// need to be up to date you should fetch the playlists again. +/// +/// [interface]: https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html +/// [playlist]: https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html#Struct:Playlist +/// [object_path]: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path #[derive(Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Playlist { @@ -11,6 +30,7 @@ pub struct Playlist { feature = "serde", serde(serialize_with = "serialize_owned_object_path") )] + /// Unique playlist identifier id: OwnedObjectPath, name: String, #[cfg_attr(feature = "serde", serde(default))] @@ -18,29 +38,55 @@ pub struct Playlist { } impl Playlist { + /// Tries to create a new [`Playlist`]. + /// + /// The MPRIS spec does not provide a way to create new playlists so you can't use this to + /// create a new one but creating [`Playlist`] manually is useful for example when you save the + /// id to a file and use that to quickly change to a playlist without the need to fetch them + /// first. + /// + /// Will return [`Err`] if the given `id` is not a valid Object Path. See the [struct + /// documentation for details][Playlist]. + /// + /// **Note**: the icon [`String`] should be a valid url but it is not actually checked + /// + /// See also [`new_from_object_path()`][Self::new_from_object_path] pub fn new(id: String, name: String, icon: Option) -> Result { match OwnedObjectPath::try_from(id) { Ok(o) => Ok(Self { id: o, name, icon }), Err(e) => Err(InvalidPlaylist::from(e.to_string())), } } - + /// Creates a new [`Playlist`] + /// + /// Almost the same as [`new()`][Self::new] but uses a [`OwnedObjectPath`] instead of a + /// [`String`] so it can't fail. pub fn new_from_object_path(id: OwnedObjectPath, name: String, icon: Option) -> Self { Self { id, name, icon } } + /// Gets the name of the playlist + /// + /// **Note**: as mentioned in the struct documentation this value might not be correct if the + /// player changed the name of this playlist. pub fn get_name(&self) -> &str { &self.name } + /// Gets the icon url if present + /// + /// **Note**: as mentioned in the struct documentation this value might not be correct if the + /// player changed the icon of this playlist. pub fn get_icon(&self) -> Option<&str> { self.icon.as_deref() } + /// Gets the `id` as a borrowed [`ObjectPath`] pub fn get_id(&self) -> ObjectPath<'_> { self.id.as_ref() } + /// Gets the `id` as a &[`str`] pub fn get_id_as_str(&self) -> &str { self.id.as_str() } @@ -83,7 +129,7 @@ impl From<(OwnedObjectPath, String, String)> for Playlist { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -/// Specifies the ordering of returned playlists +/// Specifies the ordering of returned playlists. pub enum PlaylistOrdering { /// Alphabetical ordering by name, ascending. Alphabetical, /* Alphabetical */ @@ -104,6 +150,9 @@ pub enum PlaylistOrdering { } impl PlaylistOrdering { + /// Returns the string value that's used on the D-Bus. + /// + /// See [`as_str()`][Self::as_str()] if you want the name of the enum variant. pub fn as_str_value(&self) -> &str { match self { PlaylistOrdering::Alphabetical => "Alphabetical", @@ -113,6 +162,10 @@ impl PlaylistOrdering { PlaylistOrdering::UserDefined => "User", } } + + /// Returns the name of the enum variant as a &[str] + /// + /// See [`as_str_value()`][Self::as_str_value] if you want the actual D-Bus value. pub fn as_str(&self) -> &str { match self { PlaylistOrdering::Alphabetical => "Alphabetical", From 9312e61829baa63e7c65936ff23222ebf7db252f Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Wed, 30 Oct 2024 23:14:11 +0100 Subject: [PATCH 41/55] Add case sensitivity argument to `find_by_name()` --- src/lib.rs | 15 ++++++++++++--- src/player.rs | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6ef874d..b64062a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -224,19 +224,28 @@ impl Mpris { Ok(first_paused.or(first_with_track).or(first_found)) } - /// Looks for a [`Player`] by it's MPRIS [`Identity`][identity] (case insensitive). + /// Looks for a [`Player`] by it's MPRIS [`Identity`][identity]. /// /// See also [`Player::identity()`]. /// /// [identity]: /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:Identity - pub async fn find_by_name(&self, name: &str) -> Result, MprisError> { + pub async fn find_by_name( + &self, + name: &str, + case_sensitive: bool, + ) -> Result, MprisError> { let mut players = self.into_stream().await?; if players.is_terminated() { return Ok(None); } while let Some(player) = players.try_next().await? { - if player.identity().await?.to_lowercase() == name.to_lowercase() { + let identity = player.identity().await?; + if case_sensitive { + if identity == name { + return Ok(Some(player)); + } + } else if identity.to_lowercase() == name.to_lowercase() { return Ok(Some(player)); } } diff --git a/src/player.rs b/src/player.rs index aec17f4..c5d5890 100644 --- a/src/player.rs +++ b/src/player.rs @@ -30,7 +30,7 @@ use crate::{ /// async fn main() { /// let mpris = Mpris::new().await.unwrap(); /// let vlc = mpris -/// .find_by_name("VLC media player") +/// .find_by_name("VLC media player", false) /// .await /// .unwrap() /// .unwrap(); From a3f25c052a406ed067ebb24c7cdefb615af187b2 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Wed, 30 Oct 2024 23:14:59 +0100 Subject: [PATCH 42/55] Make `TrackID` less restrictive Players seem to ignore the "/org/mpris" restriction so it's no longer required. It can be checked with the `is_valid()` method. This also means that converting from `ObjectPath` can now be `From` instead of `TryFrom`. --- src/metadata/track_id.rs | 86 +++++++++++++++++++++++----------------- src/player.rs | 2 +- 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index cdb0226..97a279e 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -12,13 +12,16 @@ use crate::errors::InvalidTrackID; /// > multiple times in the tracklist, this must be unique within the scope of the tracklist. /// /// This type is checked on creation. It must be a [valid D-Bus object path][object_path] and it -/// can't begin with `"/org/mpris"` besides the special [`NO_TRACK`][Self::NO_TRACK] value which you can get by -/// using [`no_track()`](Self::no_track). +/// _technically_ can't begin with `"/org/mpris"` besides the special [`NO_TRACK`][Self::NO_TRACK] +/// value but most players don't follow this rule so it is ignored. [`is_valid()`][Self::is_valid] +/// will still check for it. /// /// See [this link for the details][track_id]. /// -/// [track_id]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Simple-Type:Track_Id -/// [object_path]: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path +/// [track_id]: +/// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Simple-Type:Track_Id +/// [object_path]: +/// https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))] @@ -48,6 +51,25 @@ impl TrackID { self.as_str() == Self::NO_TRACK } + /// Checks if [`TrackID`] is valid. + /// + /// This also checks if the object path doesn't start with `/org/mpris` which gets ignored on + /// creation. + /// ``` + /// use mpris::TrackID; + /// + /// // Object path that follows the extra rule + /// let track = TrackID::try_from("/valid/path").expect("won't panic"); + /// assert!(track.is_valid()); + /// + /// // Will be created but is _technically_ not valid + /// let wrong_track = TrackID::try_from("/org/mpris/wrong").expect("won't panic"); + /// assert!(!wrong_track.is_valid()); + /// ``` + pub fn is_valid(&self) -> bool { + check_start(self.deref()).is_ok() + } + /// Gets the D-Bus object path value as a &[`str`]. pub fn as_str(&self) -> &str { self.0.as_str() @@ -71,7 +93,7 @@ where { if s.starts_with("/org/mpris") && s.deref() != TrackID::NO_TRACK { Err(InvalidTrackID::from( - "TrackID can't start with \"/org/mpris\"", + r#"TrackID can't start with "/org/mpris""#, )) } else { Ok(s) @@ -94,7 +116,7 @@ impl TryFrom<&str> for TrackID { type Error = InvalidTrackID; fn try_from(value: &str) -> Result { - match OwnedObjectPath::try_from(check_start(value)?) { + match OwnedObjectPath::try_from(value) { Ok(o) => Ok(Self(o)), Err(e) => { if let zbus::zvariant::Error::Message(s) = e { @@ -113,7 +135,7 @@ impl TryFrom for TrackID { type Error = InvalidTrackID; fn try_from(value: String) -> Result { - match OwnedObjectPath::try_from(check_start(value)?) { + match OwnedObjectPath::try_from(value) { Ok(o) => Ok(Self(o)), Err(e) => { if let zbus::zvariant::Error::Message(s) = e { @@ -156,28 +178,21 @@ impl TryFrom> for TrackID { fn try_from(value: Value) -> Result { match value { Value::Str(s) => Self::try_from(s.as_str()), - Value::ObjectPath(path) => Self::try_from(path), + Value::ObjectPath(path) => Ok(Self::from(path)), _ => Err(InvalidTrackID::from("not a String or ObjectPath")), } } } -impl TryFrom for TrackID { - type Error = InvalidTrackID; - - fn try_from(value: OwnedObjectPath) -> Result { - match check_start(value.as_str()) { - Ok(_) => Ok(Self(value)), - Err(e) => Err(e), - } +impl From for TrackID { + fn from(value: OwnedObjectPath) -> Self { + Self(value) } } -impl TryFrom> for TrackID { - type Error = InvalidTrackID; - - fn try_from(value: ObjectPath) -> Result { - Ok(Self(check_start(value)?.into())) +impl From> for TrackID { + fn from(value: ObjectPath) -> Self { + Self(value.into()) } } @@ -268,38 +283,28 @@ mod tests { assert!(TrackID::try_from("//some/path").is_err()); assert!(TrackID::try_from("/some.path").is_err()); assert!(TrackID::try_from("path").is_err()); - assert!(TrackID::try_from("/org/mpris").is_err()); assert!(TrackID::try_from("".to_string()).is_err()); assert!(TrackID::try_from("//some/path".to_string()).is_err()); assert!(TrackID::try_from("/some.path".to_string()).is_err()); assert!(TrackID::try_from("path".to_string()).is_err()); - assert!(TrackID::try_from("/org/mpris".to_string()).is_err()); } #[test] fn from_object_path() { assert_eq!( - TrackID::try_from(ObjectPath::from_str_unchecked("/valid/path")), - Ok(TrackID(OwnedObjectPath::from( - ObjectPath::from_str_unchecked("/valid/path") + TrackID::from(ObjectPath::from_str_unchecked("/valid/path")), + TrackID(OwnedObjectPath::from(ObjectPath::from_str_unchecked( + "/valid/path" ))) ); assert_eq!( - TrackID::try_from(OwnedObjectPath::from(ObjectPath::from_str_unchecked( + TrackID::from(OwnedObjectPath::from(ObjectPath::from_str_unchecked( "/valid/path" ))), - Ok(TrackID(OwnedObjectPath::from( - ObjectPath::from_str_unchecked("/valid/path") - ))) - ); - - assert!(TrackID::try_from(ObjectPath::from_str_unchecked("/org/mpris")).is_err()); - assert!( - TrackID::try_from(OwnedObjectPath::from(ObjectPath::from_str_unchecked( - "/org/mpris" + TrackID(OwnedObjectPath::from(ObjectPath::from_str_unchecked( + "/valid/path" ))) - .is_err() ); } @@ -328,6 +333,13 @@ mod tests { ); assert!(TrackID::try_from(MetadataValue::Unsupported).is_err()); } + + #[test] + fn is_valid() { + assert!(TrackID::try_from("/regular/path").unwrap().is_valid()); + assert!(!TrackID::try_from("/org/mpris").unwrap().is_valid()); + assert!(!TrackID::try_from("/org/mpris/invalid/path").unwrap().is_valid()); + } } #[cfg(all(test, feature = "serde"))] diff --git a/src/player.rs b/src/player.rs index c5d5890..04025ed 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1152,7 +1152,7 @@ impl Player { let result = self.check_track_list_support()?.tracks().await?; let mut track_ids = Vec::with_capacity(result.len()); for r in result { - track_ids.push(TrackID::try_from(r)?); + track_ids.push(TrackID::from(r)); } Ok(track_ids) } From 8f1b25b11a5b2b11ba238a8483c9b136d5b45d37 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sat, 2 Nov 2024 16:53:45 +0100 Subject: [PATCH 43/55] Add unique name for Player and other small changes `Player::unique_bus_name()` will return the unique bus name of the player if a player is connected. To make this work `Player::new()` is back to taking a `&Mpris` so that it can copy `DBusProxy` which is used to look up the unique name. Other changes: `Mpris::into_stream()` renamed to `stream_players()` because it doesn't consume Mpris. `PlayerStream` now also holds `DBusProxy` `MetadataIter` renamed to `MetadataIntoIter` Some minor formatting changes --- src/lib.rs | 39 +++++++++------ src/metadata.rs | 2 +- src/metadata/metadata.rs | 16 +++--- src/metadata/track_id.rs | 4 +- src/player.rs | 103 ++++++++++++++++++++++++++------------- src/proxies.rs | 3 ++ 6 files changed, 108 insertions(+), 59 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b64062a..34e135a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,6 @@ unused_qualifications )] -//! //! # mpris //! //! `mpris` is a library for dealing with [MPRIS2][spec]-compatible media players over D-Bus. @@ -125,7 +124,7 @@ type PlayerFuture = Pin> + Se /// All find methods first sort alphabetically by the players' [well known Bus Name][busname] and if /// any of the [`Player`]s fails to initialize they will immediately return with an [`Err`]. If you /// want to get all of the [`Player`]s even if one fails to initialize you should use -/// [`into_stream()`][Self::into_stream] and handle the errors. +/// [`stream_players()`][Self::stream_players] and handle the errors. /// /// # Find methods return types /// - [Ok]\([Some]\([Player]\)\): No error happened and a [`Player`] was found @@ -137,7 +136,7 @@ type PlayerFuture = Pin> + Se #[derive(Clone)] pub struct Mpris { connection: Connection, - dbus_proxy: DBusProxy<'static>, + pub(crate) dbus_proxy: DBusProxy<'static>, } impl Mpris { @@ -174,6 +173,11 @@ impl Mpris { self.connection.clone() } + /// Gets a reference to the [`Connection`] that is used. + pub fn get_connection_ref(&self) -> &Connection { + &self.connection + } + // Will be used later #[allow(dead_code)] /// Gets the internal executor for the [`Connection`]. Can be used to spawn tasks. @@ -184,7 +188,7 @@ impl Mpris { /// Returns the first found [`Player`] regardless of state. pub async fn find_first(&self) -> Result, MprisError> { match self.all_player_bus_names().await?.into_iter().next() { - Some(bus) => Ok(Some(Player::new(self.connection.clone(), bus).await?)), + Some(bus) => Ok(Some(Player::new(self, bus).await?)), None => Ok(None), } } @@ -196,7 +200,7 @@ impl Mpris { /// [`Paused`](PlaybackStatus::Paused), then one with any track metadata, after that it will /// just return the first it finds. pub async fn find_active(&self) -> Result, MprisError> { - let mut players = self.into_stream().await?; + let mut players = self.stream_players().await?; if players.is_terminated() { return Ok(None); } @@ -235,7 +239,7 @@ impl Mpris { name: &str, case_sensitive: bool, ) -> Result, MprisError> { - let mut players = self.into_stream().await?; + let mut players = self.stream_players().await?; if players.is_terminated() { return Ok(None); } @@ -259,7 +263,7 @@ impl Mpris { let bus_names = self.all_player_bus_names().await?; let mut players = Vec::with_capacity(bus_names.len()); for player_name in bus_names { - players.push(Player::new(self.connection.clone(), player_name).await?); + players.push(Player::new(self, player_name).await?); } Ok(players) } @@ -282,9 +286,9 @@ impl Mpris { /// Creates a [`PlayerStream`] which implements the [`Stream`] trait. /// /// For more details see [`PlayerStream`]'s documentation. - pub async fn into_stream(&self) -> Result { + pub async fn stream_players(&self) -> Result { let buses = self.all_player_bus_names().await?; - Ok(PlayerStream::new(self.connection.clone(), buses)) + Ok(PlayerStream::new(self, buses)) } } @@ -310,7 +314,7 @@ impl Debug for Mpris { /// #[async_std::main] /// async fn main() { /// let mpris = Mpris::new().await.unwrap(); -/// let mut stream = mpris.into_stream().await.unwrap(); +/// let mut stream = mpris.stream_players().await.unwrap(); /// /// while let Some(result) = stream.next().await { /// match result { @@ -324,6 +328,7 @@ impl Debug for Mpris { /// [lite]: https://docs.rs/futures-lite/latest/futures_lite/ pub struct PlayerStream { connection: Connection, + dbus_proxy: DBusProxy<'static>, buses: VecDeque>, cur_future: Option, } @@ -332,11 +337,12 @@ impl PlayerStream { /// Creates a new [`PlayerStream`]. /// /// There should be no need to use this directly and instead you should use - /// [`Mpris::into_stream`]. - pub fn new(connection: Connection, buses: Vec>) -> Self { + /// [`Mpris::stream_players`]. + pub fn new(mpris: &Mpris, buses: Vec>) -> Self { let buses = VecDeque::from(buses); Self { - connection, + connection: mpris.get_connection(), + dbus_proxy: mpris.dbus_proxy.clone(), buses, cur_future: None, } @@ -359,8 +365,11 @@ impl Stream for PlayerStream { }, None => match self.buses.front() { Some(bus) => { - self.cur_future = - Some(Box::pin(Player::new(self.connection.clone(), bus.clone()))) + self.cur_future = Some(Box::pin(Player::new_internal( + self.connection.clone(), + self.dbus_proxy.clone(), + bus.clone(), + ))) } None => return Poll::Ready(None), }, diff --git a/src/metadata.rs b/src/metadata.rs index 50f9174..fb0b2be 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -4,6 +4,6 @@ mod metadata; mod track_id; mod values; -pub use self::metadata::{Metadata, MetadataIter, RawMetadata}; +pub use self::metadata::{Metadata, MetadataIntoIter, RawMetadata}; pub use self::track_id::TrackID; pub use self::values::MetadataValue; diff --git a/src/metadata/metadata.rs b/src/metadata/metadata.rs index 14518a1..edeaca7 100644 --- a/src/metadata/metadata.rs +++ b/src/metadata/metadata.rs @@ -141,14 +141,14 @@ macro_rules! gen_metadata_struct { impl IntoIterator for $name { type Item = (String, Option); - type IntoIter = MetadataIter; + type IntoIter = MetadataIntoIter; fn into_iter(mut self) -> Self::IntoIter { // Turns the fields into Vec<&'static str, Option> with they key as the str let fields = vec![ $(($key, self.$field.take().map(MetadataValue::from))),* ]; - MetadataIter::new(fields, self.$others_name) + MetadataIntoIter::new(fields, self.$others_name) } } @@ -237,7 +237,7 @@ gen_metadata_struct!( /// # Miscellaneous features /// /// - Can be turned into [`RawMetadata`] using [Into]<[RawMetadata]> - /// - Implements [`IntoIterator`], see [`MetadataIter`] for details + /// - Implements [`IntoIterator`], see [`MetadataIntoIter`] for details /// - Can be lossily converted from [`RawMetadata`] by using /// [`from_raw_lossy()`][Self::from_raw_lossy] /// @@ -329,12 +329,12 @@ gen_metadata_struct!( /// /// [guidelines]: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ #[derive(Debug)] -pub struct MetadataIter { +pub struct MetadataIntoIter { values: std::vec::IntoIter<(&'static str, Option)>, map: std::collections::hash_map::IntoIter, } -impl MetadataIter { +impl MetadataIntoIter { fn new(fields: Vec<(&'static str, Option)>, map: RawMetadata) -> Self { Self { values: fields.into_iter(), @@ -343,7 +343,7 @@ impl MetadataIter { } } -impl Iterator for MetadataIter { +impl Iterator for MetadataIntoIter { type Item = (String, Option); fn next(&mut self) -> Option { @@ -359,8 +359,8 @@ impl Iterator for MetadataIter { } } -impl ExactSizeIterator for MetadataIter {} -impl FusedIterator for MetadataIter {} +impl ExactSizeIterator for MetadataIntoIter {} +impl FusedIterator for MetadataIntoIter {} #[cfg(test)] mod metadata_tests { diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index 97a279e..f55acb5 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -338,7 +338,9 @@ mod tests { fn is_valid() { assert!(TrackID::try_from("/regular/path").unwrap().is_valid()); assert!(!TrackID::try_from("/org/mpris").unwrap().is_valid()); - assert!(!TrackID::try_from("/org/mpris/invalid/path").unwrap().is_valid()); + assert!(!TrackID::try_from("/org/mpris/invalid/path") + .unwrap() + .is_valid()); } } diff --git a/src/player.rs b/src/player.rs index 04025ed..6aebe36 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,13 +1,13 @@ use std::collections::HashMap; -use futures_util::try_join; +use futures_util::{join, try_join}; use zbus::{names::BusName, Connection}; use crate::{ metadata::{MetadataValue, RawMetadata}, - proxies::{MediaPlayer2Proxy, PlayerProxy, PlaylistsProxy, TrackListProxy}, - LoopStatus, Metadata, MprisDuration, MprisError, PlaybackStatus, Playlist, PlaylistOrdering, - TrackID, MPRIS2_PREFIX, + proxies::{DBusProxy, MediaPlayer2Proxy, PlayerProxy, PlaylistsProxy, TrackListProxy}, + LoopStatus, Metadata, Mpris, MprisDuration, MprisError, PlaybackStatus, Playlist, + PlaylistOrdering, TrackID, MPRIS2_PREFIX, }; /// Struct that represents a player connected to the D-Bus server. Can be used to query and control @@ -49,11 +49,10 @@ use crate::{ /// use zbus::names::BusName; /// #[async_std::main] /// async fn main() { -/// // To create the connection /// let mpris = Mpris::new().await.unwrap(); /// // A "unique" Bus Name /// let unique_name = BusName::try_from(":1.123").unwrap(); -/// let vlc = Player::new(mpris.get_connection(), unique_name) +/// let vlc = Player::new(&mpris, unique_name) /// .await /// .unwrap(); /// @@ -214,10 +213,11 @@ use crate::{ #[derive(Clone)] pub struct Player { bus_name: BusName<'static>, + dbus_proxy: DBusProxy<'static>, mp2_proxy: MediaPlayer2Proxy<'static>, player_proxy: PlayerProxy<'static>, - playlist_proxy: Option>, track_list_proxy: Option>, + playlist_proxy: Option>, } impl Player { @@ -226,29 +226,42 @@ impl Player { /// In most cases there is no need to create [`Player`]s directly, instead you should create /// them through [`Mpris`][crate::Mpris]. Doing it this way however allows you to bind the /// [`Player`] to a unique Bus Name. See [this][Self#bus-name] for a simple explanation. - pub async fn new( - connection: Connection, + pub async fn new(mpris: &Mpris, bus_name: BusName<'static>) -> Result { + Self::new_internal(mpris.get_connection(), mpris.dbus_proxy.clone(), bus_name).await + } + + pub(crate) async fn new_internal( + conn: Connection, + dbus_proxy: DBusProxy<'static>, bus_name: BusName<'static>, - ) -> Result { + ) -> Result { let (mp2_proxy, player_proxy, playlist_proxy, track_list_proxy) = try_join!( - MediaPlayer2Proxy::new(&connection, bus_name.clone()), - PlayerProxy::new(&connection, bus_name.clone()), - PlaylistsProxy::new(&connection, bus_name.clone()), - TrackListProxy::new(&connection, bus_name.clone()), + MediaPlayer2Proxy::new(&conn, bus_name.clone()), + PlayerProxy::new(&conn, bus_name.clone()), + PlaylistsProxy::new(&conn, bus_name.clone()), + TrackListProxy::new(&conn, bus_name.clone()), )?; - let playlist = playlist_proxy.playlist_count().await.is_ok(); - let track_list = track_list_proxy.can_edit_tracks().await.is_ok(); + let (track_list, playlist) = join!( + playlist_proxy.playlist_count(), + track_list_proxy.can_edit_tracks() + ); + Ok(Player { bus_name, + dbus_proxy, mp2_proxy, player_proxy, - playlist_proxy: if playlist { Some(playlist_proxy) } else { None }, - track_list_proxy: if track_list { + track_list_proxy: if track_list.is_ok() { Some(track_list_proxy) } else { None }, + playlist_proxy: if playlist.is_ok() { + Some(playlist_proxy) + } else { + None + }, }) } @@ -301,35 +314,57 @@ impl Player { /// /// Simply pings the player and checks if it responds. /// - /// # Returns - /// - /// - [Some]\([true]\): player is connected - /// - [Some]\([false]\): player is not connected - /// - [`Err`]: some other issue occurred while trying to ping the player + /// This could return [`Err`] even if the player is connected but something with the connection + /// went wrong. pub async fn is_running(&self) -> Result { match self.mp2_proxy.ping().await { Ok(_) => Ok(true), - Err(e) => { - if let zbus::Error::MethodError(ref err_name, _, _) = e { - if err_name.as_str() == "org.freedesktop.DBus.Error.ServiceUnknown" { - Ok(false) - } else { - Err(e.into()) - } - } else { - Err(e.into()) + Err(e) => match e { + zbus::Error::MethodError(e_name, _, _) + if e_name == "org.freedesktop.DBus.Error.ServiceUnknown" => + { + Ok(false) } - } + _ => Err(e.into()), + }, } } /// Returns the Bus Name of the [`Player`]. /// - /// See also: [`bus_name_trimmed()`][Self::bus_name_trimmed]. + /// See also: [`bus_name_trimmed()`][Self::bus_name_trimmed] and + /// [`unique_bus_name()`][Self::unique_bus_name]. pub fn bus_name(&self) -> &str { self.bus_name.as_str() } + /// Returns the Unique Bus Name of the [`Player`]. + /// + /// If it returns [`None`] then no player is currently connected. + /// + /// If you just want to check if the player is connected you should use + /// [`is_running()`][Self::is_running] instead. + /// + /// See also: [`bus_name()`][Self::bus_name]. + pub async fn unique_bus_name(&self) -> Result, MprisError> { + // If Player is bound to a unique name there's no need to check. + if let BusName::Unique(unique_name) = &self.bus_name { + Ok(Some(unique_name.to_string())) + } else { + match self.dbus_proxy.get_name_owner(&self.bus_name).await { + Ok(name) => Ok(Some(name.to_string())), + Err(e) => match e { + zbus::Error::MethodError(e_name, _, _) + if e_name == "org.freedesktop.DBus.Error.NameHasNoOwner" => + { + Ok(None) + } + _ => Err(e.into()), + }, + } + } + } + /// Returns the player name part of the player's D-Bus bus name with the MPRIS2 prefix trimmed. /// /// Examples: diff --git a/src/proxies.rs b/src/proxies.rs index 422960d..5343d2f 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use zbus::names::{BusName, OwnedUniqueName}; use zbus::proxy; use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value}; @@ -11,6 +12,8 @@ use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value}; )] pub(crate) trait DBus { fn list_names(&self) -> zbus::Result>; + + fn get_name_owner(&self, bus_name: &BusName<'_>) -> zbus::Result; } #[proxy( From 11020f43a4bcc08a7431fb26dd0f2bd17b80540c Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sat, 2 Nov 2024 21:39:04 +0100 Subject: [PATCH 44/55] Warn about unwrap --- clippy.toml | 1 + src/lib.rs | 2 +- src/metadata/values.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/clippy.toml b/clippy.toml index 787620d..c115e91 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1,2 @@ allow-private-module-inception = true +allow-unwrap-in-tests = true diff --git a/src/lib.rs b/src/lib.rs index 34e135a..5349b76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![warn(clippy::print_stdout, missing_docs, clippy::todo)] +#![warn(clippy::print_stdout, missing_docs, clippy::todo, clippy::unwrap_used)] #![deny( missing_debug_implementations, missing_copy_implementations, diff --git a/src/metadata/values.rs b/src/metadata/values.rs index e40bec0..90a6417 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -250,7 +250,7 @@ impl TryFrom for String { MetadataValue::String(v) => Ok(v), MetadataValue::Strings(mut v) => { if v.len() == 1 { - Ok(v.pop().unwrap()) + Ok(v.pop().expect("length was checked to be 1")) } else { Err(InvalidMetadataValue::from( "MetadataValue::Strings contains more than 1 String", From 6b6679371b2d460e08576700d2af17d33ac54be7 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sun, 3 Nov 2024 21:16:19 +0100 Subject: [PATCH 45/55] Rework Playlists interface Created a `PlaylistsInterface` struct that holds the `PlaylistsProxy` and saves the changes received from the PlaylistChanged signal so that `Playlist` can get updated with the new values. --- src/player.rs | 87 ++++++++++++++---------- src/playlist.rs | 176 +++++++++++++++++++++++++++++++++++++++++++++--- src/proxies.rs | 2 +- 3 files changed, 218 insertions(+), 47 deletions(-) diff --git a/src/player.rs b/src/player.rs index 6aebe36..173513e 100644 --- a/src/player.rs +++ b/src/player.rs @@ -5,7 +5,8 @@ use zbus::{names::BusName, Connection}; use crate::{ metadata::{MetadataValue, RawMetadata}, - proxies::{DBusProxy, MediaPlayer2Proxy, PlayerProxy, PlaylistsProxy, TrackListProxy}, + playlist::PlaylistsInterface, + proxies::{DBusProxy, MediaPlayer2Proxy, PlayerProxy, TrackListProxy}, LoopStatus, Metadata, Mpris, MprisDuration, MprisError, PlaybackStatus, Playlist, PlaylistOrdering, TrackID, MPRIS2_PREFIX, }; @@ -217,7 +218,7 @@ pub struct Player { mp2_proxy: MediaPlayer2Proxy<'static>, player_proxy: PlayerProxy<'static>, track_list_proxy: Option>, - playlist_proxy: Option>, + playlist_interface: Option, } impl Player { @@ -235,16 +236,16 @@ impl Player { dbus_proxy: DBusProxy<'static>, bus_name: BusName<'static>, ) -> Result { - let (mp2_proxy, player_proxy, playlist_proxy, track_list_proxy) = try_join!( + let (mp2_proxy, player_proxy, track_list_proxy, playlists_interface) = try_join!( MediaPlayer2Proxy::new(&conn, bus_name.clone()), PlayerProxy::new(&conn, bus_name.clone()), - PlaylistsProxy::new(&conn, bus_name.clone()), TrackListProxy::new(&conn, bus_name.clone()), + PlaylistsInterface::new(&conn, bus_name.clone()), )?; - let (track_list, playlist) = join!( - playlist_proxy.playlist_count(), - track_list_proxy.can_edit_tracks() + let (track_list, playlists) = join!( + track_list_proxy.can_edit_tracks(), + playlists_interface.playlist_count(), ); Ok(Player { @@ -257,8 +258,8 @@ impl Player { } else { None }, - playlist_proxy: if playlist.is_ok() { - Some(playlist_proxy) + playlist_interface: if playlists.is_ok() { + Some(playlists_interface) } else { None }, @@ -289,7 +290,7 @@ impl Player { /// /// See the [interfaces section for more details][Self#interfaces] pub fn supports_playlists_interface(&self) -> bool { - self.playlist_proxy.is_some() + self.playlist_interface.is_some() } /// Queries the player for current metadata. @@ -1052,13 +1053,26 @@ impl Player { } /// Shortcut to check if `self.playlist_proxy` is Some - fn check_playlist_support(&self) -> Result<&PlaylistsProxy, MprisError> { - match &self.playlist_proxy { + fn check_playlist_support(&self) -> Result<&PlaylistsInterface, MprisError> { + match &self.playlist_interface { Some(proxy) => Ok(proxy), None => Err(MprisError::Unsupported), } } + /// Tries to update the given [`Playlist`]. + /// + /// Returns [`true`] if the given [`Playlist`] was found, [`false`] if [`Player`] isn't aware of + /// that playlist. Running [`get_playlists()`][Self::get_playlists] will refresh the list of + /// known playlists. + /// + /// Can only fail if the interface is not implemented. + pub fn update_playlist(&self, playlist: &mut Playlist) -> Result { + Ok(self + .check_playlist_support()? + .update_playlist_struct(playlist)) + } + /// Signals the player to activate a given [`Playlist`]. /// /// > Starts playing the given playlist. @@ -1074,10 +1088,9 @@ impl Player { /// [activate]: /// https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html#Method:ActivatePlaylist pub async fn activate_playlist(&self, playlist: &Playlist) -> Result<(), MprisError> { - Ok(self - .check_playlist_support()? - .activate_playlist(&playlist.get_id()) - .await?) + self.check_playlist_support()? + .activate_playlist(playlist) + .await } /// Gets the [`Playlist`]s of the player. @@ -1098,13 +1111,23 @@ impl Player { order: PlaylistOrdering, reverse_order: bool, ) -> Result, MprisError> { - Ok(self - .check_playlist_support()? - .get_playlists(start_index, max_count, order.as_str_value(), reverse_order) - .await? - .into_iter() - .map(Playlist::from) - .collect()) + self.check_playlist_support()? + .get_playlists(start_index, max_count, order, reverse_order) + .await + } + + /// Clears the stored [`Playlist`]s metadata. + /// + /// Players don't signal when a [`Playlist`] gets removed meaning that if you use a [`Player`] + /// instance for a long time and edit playlists often the internal playlist list will keep + /// getting bigger with pointless data. This method lets you clear it if it becomes an issue. + /// + /// It's recommended to run [`get_playlists()`][Self::get_playlists] after clearing. + /// + /// Can only fail if the interface is not implemented. + pub fn clear_playlists_data(&self) -> Result<(), MprisError> { + self.check_playlist_support()?.clear(); + Ok(()) } /// Gets the currently active [`Playlist`] if any. @@ -1118,12 +1141,7 @@ impl Player { /// [active]: /// https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html#Property:ActivePlaylist pub async fn active_playlist(&self) -> Result, MprisError> { - let result = self.check_playlist_support()?.active_playlist().await?; - if result.0 { - Ok(Some(Playlist::from(result.1))) - } else { - Ok(None) - } + self.check_playlist_support()?.active_playlist().await } /// Gets the [`PlaylistOrdering`]s the player supports. @@ -1135,12 +1153,7 @@ impl Player { /// [orderings]: /// https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html#Property:Orderings pub async fn orderings(&self) -> Result, MprisError> { - let result = self.check_playlist_support()?.orderings().await?; - let mut orderings = Vec::with_capacity(result.len()); - for s in result { - orderings.push(s.parse()?); - } - Ok(orderings) + self.check_playlist_support()?.orderings().await } /// Gets the number of available playlists. @@ -1150,7 +1163,7 @@ impl Player { /// [count]: /// https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html#Property:PlaylistCount pub async fn playlist_count(&self) -> Result { - Ok(self.check_playlist_support()?.playlist_count().await?) + self.check_playlist_support()?.playlist_count().await } /// Shortcut to check if `self.track_list_proxy` is Some @@ -1308,7 +1321,7 @@ impl std::fmt::Debug for Player { f.debug_struct("Player") .field("bus_name", &self.bus_name()) .field("track_list", &self.track_list_proxy.is_some()) - .field("playlist", &self.playlist_proxy.is_some()) + .field("playlist", &self.playlist_interface.is_some()) .finish_non_exhaustive() } } diff --git a/src/playlist.rs b/src/playlist.rs index a1c3f65..57e5ba9 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -1,8 +1,21 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex, MutexGuard}, +}; + +use futures_util::StreamExt; #[cfg(feature = "serde")] use serde::Serializer; -use zbus::zvariant::{ObjectPath, OwnedObjectPath}; +use zbus::{ + names::BusName, + zvariant::{ObjectPath, OwnedObjectPath}, + Connection, Task, +}; + +use crate::proxies::PlaylistsProxy; +use crate::{InvalidPlaylist, InvalidPlaylistOrdering, MprisError}; -use crate::{InvalidPlaylist, InvalidPlaylistOrdering}; +type InnerPlaylistData = HashMap)>; /// A data structure describing a playlist. /// @@ -14,15 +27,18 @@ use crate::{InvalidPlaylist, InvalidPlaylistOrdering}; /// - the name of the playlist /// - an optional icon url /// -/// It can be obtain from [`Player::active_playlist()`][crate::Player::active_playlist] and +/// It can be obtained from [`Player::active_playlist()`][crate::Player::active_playlist] and /// [`Player::get_playlists()`][crate::Player::get_playlists]. /// -/// **Note**: currently the name and icon url will not get updated if they get changed. If they -/// need to be up to date you should fetch the playlists again. +/// **Note**: the name and icon url will not get updated if they get changed by the player. If they +/// need to be up to date you should use +/// [`Player::update_playlist()`][crate::Player::update_playlist]. /// /// [interface]: https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html -/// [playlist]: https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html#Struct:Playlist -/// [object_path]: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path +/// [playlist]: +/// https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html#Struct:Playlist +/// [object_path]: +/// https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path #[derive(Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Playlist { @@ -68,7 +84,8 @@ impl Playlist { /// Gets the name of the playlist /// /// **Note**: as mentioned in the struct documentation this value might not be correct if the - /// player changed the name of this playlist. + /// player changed the name of this playlist. Use + /// [`Player::update_playlist()`][crate::Player::update_playlist] to update the values. pub fn get_name(&self) -> &str { &self.name } @@ -76,7 +93,8 @@ impl Playlist { /// Gets the icon url if present /// /// **Note**: as mentioned in the struct documentation this value might not be correct if the - /// player changed the icon of this playlist. + /// player changed the icon of this playlist. Use + /// [`Player::update_playlist()`][crate::Player::update_playlist] to update the values. pub fn get_icon(&self) -> Option<&str> { self.icon.as_deref() } @@ -92,6 +110,146 @@ impl Playlist { } } +/// Represents the [Playlists interface][playlists]. +/// +/// Listens to the PlaylistChanged signal and updates the data internally to allow [`Playlist`]s to +/// update. +/// +/// [playlists]: https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html +#[derive(Debug, Clone)] +pub(crate) struct PlaylistsInterface { + inner: PlaylistInner, + proxy: PlaylistsProxy<'static>, + // Just here to stop the task when it gets dropped + // Arc is needed to allow cloning + #[allow(dead_code)] + task: Arc>, +} + +impl PlaylistsInterface { + pub(crate) async fn new(conn: &Connection, bus_name: BusName<'static>) -> zbus::Result { + let inner = PlaylistInner::default(); + let i_clone = inner.clone(); + let proxy = PlaylistsProxy::new(conn, bus_name.clone()).await?; + let mut stream = proxy.receive_playlist_changed().await?; + + let task = proxy.inner().connection().executor().spawn( + async move { + while let Some(change) = stream.next().await { + // TODO: don't ignore errors somehow without panicking + if let Ok(args) = change.args() { + let playlist = Playlist::from(args.playlist); + i_clone + .get_lock() + .insert(playlist.id, (playlist.name, playlist.icon)); + } + } + }, + &format!("{} playlist watcher task", bus_name.as_str()), + ); + Ok(Self { + inner, + proxy, + task: Arc::new(task), + }) + } + + /// Clears the internal playlist storage. + pub(crate) fn clear(&self) { + self.inner.get_lock().clear(); + } + + /// Updates the given [`Playlist`]. + /// + /// Returns `true` if the given playlist was found else false. + pub(crate) fn update_playlist_struct(&self, playlist: &mut Playlist) -> bool { + let lock = self.inner.get_lock(); + match lock.get(&playlist.id) { + Some((name, icon)) => { + if playlist.name != *name { + playlist.name = name.clone(); + } + if playlist.icon != *icon { + playlist.icon = icon.clone(); + } + true + } + None => false, + } + } + + pub(crate) async fn activate_playlist(&self, playlist: &Playlist) -> Result<(), MprisError> { + Ok(self.proxy.activate_playlist(&playlist.get_id()).await?) + } + + /// Wraps the proxy method of the same name and updates the internal data. + pub(crate) async fn get_playlists( + &self, + start_index: u32, + max_count: u32, + order: PlaylistOrdering, + reverse_order: bool, + ) -> Result, MprisError> { + let playlists: Vec<_> = self + .proxy + .get_playlists(start_index, max_count, order.as_str(), reverse_order) + .await? + .into_iter() + .map(Playlist::from) + .collect(); + self.inner.update_playlists(&playlists); + Ok(playlists) + } + + /// Wraps the proxy method of the same name and updates the internal data. + pub(crate) async fn active_playlist(&self) -> Result, MprisError> { + Ok(match self.proxy.active_playlist().await? { + (true, data) => { + // Better to create a temporary Vec here than to lock the Mutex for each playlist + let mut playlist = vec![Playlist::from(data)]; + self.inner.update_playlists(&playlist); + Some(playlist.pop().expect("there should be at least 1 playlist")) + } + (false, _) => None, + }) + } + + pub(crate) async fn orderings(&self) -> Result, MprisError> { + let result = self.proxy.orderings().await?; + let mut orderings = Vec::with_capacity(result.len()); + for s in result { + orderings.push(s.parse()?); + } + Ok(orderings) + } + + pub(crate) async fn playlist_count(&self) -> Result { + Ok(self.proxy.playlist_count().await?) + } +} + +#[derive(Debug, Clone, Default)] +struct PlaylistInner { + data: Arc>, +} + +impl PlaylistInner { + fn get_lock(&self) -> MutexGuard { + self.data.lock().expect("poisoned lock") + } + + /// Updates the inner data with the given [`Playlist`]s. + fn update_playlists(&self, playlists: &[Playlist]) { + let mut lock = self.get_lock(); + for playlist in playlists { + lock.insert( + playlist.id.clone(), + (playlist.name.clone(), playlist.icon.clone()), + ); + } + } +} + impl std::fmt::Debug for Playlist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Playlist") diff --git a/src/proxies.rs b/src/proxies.rs index 5343d2f..2c3861e 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -211,7 +211,7 @@ pub(crate) trait Playlists { ) -> zbus::Result>; #[zbus(signal)] - fn playlist_changed(&self) -> zbus::Result>; + fn playlist_changed(&self, playlist: (OwnedObjectPath, String, String)) -> zbus::Result<()>; /// ActivePlaylist property #[zbus(property)] From 2e85acaee416dce94517b1317cb87ea2a283d319 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sun, 3 Nov 2024 23:08:39 +0100 Subject: [PATCH 46/55] Bump MSRV to 1.75 to match zbus --- Cargo.toml | 2 +- src/duration.rs | 4 +--- src/metadata/track_id.rs | 2 +- src/metadata/values.rs | 2 +- src/player.rs | 3 ++- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6cec0ab..6d37805 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ description = "Idiomatic MPRIS D-Bus interface library" version = "3.0.0-alpha1" license = "Apache-2.0" edition = "2021" -rust-version = "1.56" +rust-version = "1.75" authors = ["Magnus Bergmark "] repository = "https://github.com/Mange/mpris-rs" readme = "README.md" diff --git a/src/duration.rs b/src/duration.rs index bc5ce43..fd613ec 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -205,9 +205,7 @@ macro_rules! impl_math { } impl_math!(Mul, mul, saturating_mul); -// Using regular div because of the current MSRV -// Can you even underflow a u64 with div? -impl_math!(Div, div, div); +impl_math!(Div, div, saturating_div); impl_math!(Add, add, saturating_add); impl_math!(Sub, sub, saturating_sub); diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index f55acb5..ec36e4b 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -29,7 +29,7 @@ pub struct TrackID(OwnedObjectPath); impl TrackID { /// The special "NoTrack" value - pub const NO_TRACK: &str = "/org/mpris/MediaPlayer2/TrackList/NoTrack"; + pub const NO_TRACK: &'static str = "/org/mpris/MediaPlayer2/TrackList/NoTrack"; /// Tries to create a new [`TrackID`] /// diff --git a/src/metadata/values.rs b/src/metadata/values.rs index 90a6417..71d6b1e 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -54,7 +54,7 @@ impl MetadataValue { pub fn into_i64(self) -> Option { match self { MetadataValue::SignedInt(i) => Some(i), - MetadataValue::UnsignedInt(i) => Some(i.clamp(0, i64::MAX as u64) as i64), + MetadataValue::UnsignedInt(i) => Some(0_i64.saturating_add_unsigned(i)), _ => None, } } diff --git a/src/player.rs b/src/player.rs index 173513e..6b25f28 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1231,10 +1231,11 @@ impl Player { after_track: Option<&TrackID>, set_as_current: bool, ) -> Result<(), MprisError> { + let no_track = TrackID::no_track(); let after = if let Some(track_id) = after_track { track_id } else { - &TrackID::no_track() + &no_track }; Ok(self .check_track_list_support()? From 0238efee8a5dbbe93a782f1330fd503ac045a12f Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Mon, 4 Nov 2024 22:58:21 +0100 Subject: [PATCH 47/55] Make disabling the internal executor easier Added `Mpris::new_no_executor()` which disables the executor and added the `ExecutorLoop` future that ticks the executor in a loop. This way the user doesn't need to interact with the zbus library directly. To make this possible `Mpris` now holds the DBusProxy inside a `OnceCell` that gets initialized the first time some method needs it. This is needed because when you create the `Connection` without the internal executor trying to create the Proxy will hang. All of the Executor stuff (including some documentation) is hidden if you enable the tokio feature. TrackID got it's Debug changed. --- src/lib.rs | 188 ++++++++++++++++++++++++++++++--------- src/metadata/track_id.rs | 8 +- src/player.rs | 7 +- 3 files changed, 158 insertions(+), 45 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5349b76..b3597ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,10 @@ -#![warn(clippy::print_stdout, missing_docs, clippy::todo, clippy::unwrap_used)] +#![warn( + clippy::print_stdout, + missing_docs, + clippy::todo, + clippy::unwrap_used, + rustdoc::unescaped_backticks +)] #![deny( missing_debug_implementations, missing_copy_implementations, @@ -57,25 +63,13 @@ //! background tasks. If you want to prevent that you should "tick" the internal executor with your //! runtime like this: //! -//! ```no_run +//! ```ignore //! use mpris::Mpris; -//! use zbus::connection::Builder; //! //! #[async_std::main] //! async fn main() { -//! let conn = Builder::session() -//! .unwrap() -//! .internal_executor(false) // The important part -//! .build() -//! .await -//! .unwrap(); -//! let c = conn.clone(); -//! async_std::task::spawn(async move { -//! loop { -//! c.executor().tick().await; -//! } -//! }); -//! let mpris = Mpris::new_from_connection(conn).await.unwrap(); +//! let mpris = Mpris::new_no_executor().await.unwrap(); +//! async_std::task::spawn(mpris.get_executor_loop().expect("was started with no executor")); //! //! // The rest of your code here //! } @@ -85,6 +79,7 @@ //! [std]: https://docs.rs/async-std/latest/async_std/ //! [tokio]: https://docs.rs/tokio/latest/tokio/ +use std::cell::OnceCell; use std::collections::VecDeque; use std::fmt::{Debug, Display}; use std::future::Future; @@ -110,6 +105,8 @@ use crate::proxies::DBusProxy; pub use duration::MprisDuration; #[doc(inline)] pub use errors::MprisError; +#[cfg(not(feature = "tokio"))] +pub use internal_executor::ExecutorLoop; #[doc(inline)] pub use metadata::{Metadata, MetadataValue, TrackID}; pub use player::Player; @@ -136,7 +133,12 @@ type PlayerFuture = Pin> + Se #[derive(Clone)] pub struct Mpris { connection: Connection, - pub(crate) dbus_proxy: DBusProxy<'static>, + #[cfg(not(feature = "tokio"))] + internal_executor: bool, + // OnceCell is needed to allow disabling the internal executor since creating DBusProxy without + // ticking would just hang + // Option is not used because that would require making most of the functions &mut self + dbus_proxy: OnceCell>, } impl Mpris { @@ -144,30 +146,77 @@ impl Mpris { /// /// Use [`new_from_connection`](Self::new_from_connection) if you want to provide the D-Bus /// connection yourself. + #[cfg_attr( + not(feature = "tokio"), + doc = "\n\nSee also: [`new_no_executor()`][Self::new_no_executor]." + )] pub async fn new() -> Result { let connection = Connection::session().await?; - let dbus_proxy = DBusProxy::new(&connection).await?; + Ok(Self::new_from_connection(connection)) + } - Ok(Self { + /// Creates a new [`Mpris`] instance with the given connection. + /// + #[cfg_attr( + not(feature = "tokio"), + doc = "When creating `Mpris` through this it is assumed that the internal executor is set to `true`.\n\n" + )] + + /// Use [`new`](Self::new) if you don't have a need to provide the D-Bus connection yourself. + pub fn new_from_connection(connection: Connection) -> Self { + Self { connection, - dbus_proxy, - }) + #[cfg(not(feature = "tokio"))] + internal_executor: true, + dbus_proxy: OnceCell::new(), + } } - /// Creates a new [`Mpris`] struct with the given connection. + /// Creates a new [`Mpris`] instance with the internal executor disabled. /// - /// See [here](crate#runtime-compatibility) for why you would want to use a custom - /// [`Connection`]. + /// See [`Runtime compatibility`][crate#runtime-compatibility] for details. /// - /// Use [`new`](Self::new) if you don't have a need to provide the D-Bus connection yourself. - pub async fn new_from_connection(connection: Connection) -> Result { - let dbus_proxy = DBusProxy::new(&connection).await?; + /// See also: [`new()`][Self::new], [`get_executor_loop()`][Self::get_executor_loop] and + /// [`ExecutorLoop`]. + #[cfg(not(feature = "tokio"))] + pub async fn new_no_executor() -> Result { + let connection = zbus::conn::Builder::session()? + .internal_executor(false) + .build() + .await?; + Ok(Self { connection, - dbus_proxy, + internal_executor: false, + dbus_proxy: OnceCell::new(), }) } + /// Returns the [`ExecutorLoop`] if the internal executor is disabled. + /// + /// See [`Runtime compatibility`][crate#runtime-compatibility] for details. + #[cfg(not(feature = "tokio"))] + pub fn get_executor_loop(&self) -> Option { + if !self.internal_executor { + Some(ExecutorLoop::new(self.get_connection())) + } else { + None + } + } + + /// Returns the DBusProxy by setting it if not yet initialized + pub(crate) async fn get_dbus_proxy(&self) -> Result<&DBusProxy<'static>, MprisError> { + loop { + match self.dbus_proxy.get() { + Some(proxy) => return Ok(proxy), + None => { + let proxy = DBusProxy::new(&self.connection).await?; + let _ = self.dbus_proxy.set(proxy); + } + }; + } + } + /// Gets the [`Connection`] that is used. pub fn get_connection(&self) -> Connection { self.connection.clone() @@ -178,13 +227,6 @@ impl Mpris { &self.connection } - // Will be used later - #[allow(dead_code)] - /// Gets the internal executor for the [`Connection`]. Can be used to spawn tasks. - pub(crate) fn get_executor(&self) -> &'static zbus::Executor { - self.connection.executor() - } - /// Returns the first found [`Player`] regardless of state. pub async fn find_first(&self) -> Result, MprisError> { match self.all_player_bus_names().await?.into_iter().next() { @@ -271,7 +313,8 @@ impl Mpris { /// Gets all of the BusNames that start with the [`MPRIS2_PREFIX`] async fn all_player_bus_names(&self) -> Result>, MprisError> { let mut names: Vec = self - .dbus_proxy + .get_dbus_proxy() + .await? .list_names() .await? .into_iter() @@ -288,20 +331,75 @@ impl Mpris { /// For more details see [`PlayerStream`]'s documentation. pub async fn stream_players(&self) -> Result { let buses = self.all_player_bus_names().await?; - Ok(PlayerStream::new(self, buses)) + Ok(PlayerStream::new( + self.get_connection(), + self.get_dbus_proxy().await?.clone(), + buses, + )) } } impl Debug for Mpris { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Mpris") - .field("connection", &format_args!("Connection {{ .. }}")) - .finish_non_exhaustive() + let mut s = f.debug_struct("Mpris"); + s.field("connection", &format_args!("Connection {{ .. }}")); + #[cfg(not(feature = "tokio"))] + s.field("internal_executor", &self.internal_executor); + s.finish_non_exhaustive() + } +} + +#[cfg(not(feature = "tokio"))] +mod internal_executor { + use std::fmt::Debug; + use std::future::Future; + use std::pin::Pin; + use std::task::{Context, Poll}; + + use zbus::Connection; + + /// A future that ticks the executor in a endless loop. + /// + /// Created with [`Mpris::get_executor_loop()`]. + /// + ///
You have to spawn this as a new task with your runtime or everything will + /// hang.
+ /// + /// See [`Runtime compatibility`][crate#runtime-compatibility] for details. + pub struct ExecutorLoop { + fut: Pin + Send>>, + } + + impl ExecutorLoop { + pub(crate) fn new(connection: Connection) -> Self { + let fut = Box::pin(async move { + loop { + connection.executor().tick().await + } + }); + Self { fut } + } + } + + impl Future for ExecutorLoop { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.fut.as_mut().poll(cx) + } + } + + impl Debug for ExecutorLoop { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TickLoop").finish_non_exhaustive() + } } } /// Lazily returns the [`Player`]s on the connection. /// +/// Created with [`Mpris::stream_players()`][Mpris::stream_players]. +/// /// Implements the [`Stream`] trait which is the async version of [`Iterator`]. It is recommended to /// use the [`futures_util`] or [`futures_lite`][lite] crate which provide useful traits for streams. /// @@ -338,11 +436,15 @@ impl PlayerStream { /// /// There should be no need to use this directly and instead you should use /// [`Mpris::stream_players`]. - pub fn new(mpris: &Mpris, buses: Vec>) -> Self { + fn new( + connection: Connection, + dbus_proxy: DBusProxy<'static>, + buses: Vec>, + ) -> Self { let buses = VecDeque::from(buses); Self { - connection: mpris.get_connection(), - dbus_proxy: mpris.dbus_proxy.clone(), + connection, + dbus_proxy, buses, cur_future: None, } diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index ec36e4b..a15df62 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -22,7 +22,7 @@ use crate::errors::InvalidTrackID; /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Simple-Type:Track_Id /// [object_path]: /// https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))] pub struct TrackID(OwnedObjectPath); @@ -100,6 +100,12 @@ where } } +impl std::fmt::Debug for TrackID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + impl Ord for TrackID { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.as_str().cmp(other.as_str()) diff --git a/src/player.rs b/src/player.rs index 6b25f28..7bf241c 100644 --- a/src/player.rs +++ b/src/player.rs @@ -228,7 +228,12 @@ impl Player { /// them through [`Mpris`][crate::Mpris]. Doing it this way however allows you to bind the /// [`Player`] to a unique Bus Name. See [this][Self#bus-name] for a simple explanation. pub async fn new(mpris: &Mpris, bus_name: BusName<'static>) -> Result { - Self::new_internal(mpris.get_connection(), mpris.dbus_proxy.clone(), bus_name).await + Self::new_internal( + mpris.get_connection(), + mpris.get_dbus_proxy().await?.clone(), + bus_name, + ) + .await } pub(crate) async fn new_internal( From f3e8e4720a932e0ae28124e64cb9d09dc6ac375b Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sun, 10 Nov 2024 18:49:48 +0100 Subject: [PATCH 48/55] Improve serde `MetadataValue` now (de)serializes directly into the contained value. `Unsupported` turns into unit and it's the default values if no other matches. When serializing from a digit `i64` is preferred and it will only deserialize into a `u64` if the value is too big to fit `i64`. Tests included. `Playlist.icon` will now turn from/into a `String` the same way the `From` traits work. Also includes minor documentation and formatting changes. --- src/lib.rs | 2 +- src/metadata/metadata.rs | 8 +-- src/metadata/track_id.rs | 7 ++- src/metadata/values.rs | 111 ++++++++++++++++++++++++++++++++++++++- src/playlist.rs | 40 ++++++++++++-- 5 files changed, 155 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b3597ed..198a735 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -360,7 +360,7 @@ mod internal_executor { /// A future that ticks the executor in a endless loop. /// - /// Created with [`Mpris::get_executor_loop()`]. + /// Created with [`Mpris::get_executor_loop()`][crate::Mpris::get_executor_loop]. /// ///
You have to spawn this as a new task with your runtime or everything will /// hang.
diff --git a/src/metadata/metadata.rs b/src/metadata/metadata.rs index edeaca7..6a4f403 100644 --- a/src/metadata/metadata.rs +++ b/src/metadata/metadata.rs @@ -244,9 +244,11 @@ gen_metadata_struct!( /// [guide]: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ /// [object_path]: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path #[derive(Debug, Clone, Default, PartialEq)] - #[cfg_attr(feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(into = "RawMetadata", try_from = "RawMetadata"))] + #[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(into = "RawMetadata", try_from = "RawMetadata") + )] struct Metadata { /// The album artist(s). "xesam:albumArtist" => album_artists: Vec, diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index a15df62..46c43c8 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -23,8 +23,11 @@ use crate::errors::InvalidTrackID; /// [object_path]: /// https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path #[derive(Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(into = "String", try_from = "String"))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(into = "String", try_from = "String") +)] pub struct TrackID(OwnedObjectPath); impl TrackID { diff --git a/src/metadata/values.rs b/src/metadata/values.rs index 71d6b1e..b87399e 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "serde")] +use serde::{de::IgnoredAny, Deserialize, Deserializer, Serialize}; use zbus::zvariant::{OwnedValue, Value}; use crate::errors::InvalidMetadataValue; @@ -11,18 +13,27 @@ use crate::errors::InvalidMetadataValue; /// [dbus_types]: https://dbus.freedesktop.org/doc/dbus-specification.html#type-system /// [meta_spec]: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ #[derive(Debug, Clone, PartialEq, PartialOrd)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(untagged))] #[allow(missing_docs)] pub enum MetadataValue { Boolean(bool), - Float(f64), SignedInt(i64), UnsignedInt(u64), + Float(f64), String(String), Strings(Vec), + #[cfg_attr(feature = "serde", serde(deserialize_with = "deser_no_fail"))] Unsupported, } +#[cfg(feature = "serde")] +fn deser_no_fail<'de, D>(d: D) -> Result<(), D::Error> +where + D: Deserializer<'de>, +{ + IgnoredAny::deserialize(d).map(|_| ()) +} + impl MetadataValue { /// Tries to turn itself into a non-empty [`String`] /// @@ -333,3 +344,99 @@ mod metadata_value_integer_tests { ); } } + +#[cfg(all(test, feature = "serde"))] +mod metadata_value_serde { + use std::u64; + + use super::*; + use serde_test::{assert_de_tokens, assert_ser_tokens, Token}; + + #[test] + fn serialization() { + assert_ser_tokens(&MetadataValue::Boolean(true), &[Token::Bool(true)]); + assert_ser_tokens(&MetadataValue::UnsignedInt(1), &[Token::U64(1)]); + assert_ser_tokens(&MetadataValue::UnsignedInt(0), &[Token::U64(0)]); + assert_ser_tokens(&MetadataValue::SignedInt(-1), &[Token::I64(-1)]); + assert_ser_tokens(&MetadataValue::SignedInt(0), &[Token::I64(0)]); + assert_ser_tokens(&MetadataValue::Float(0.0), &[Token::F64(0.0)]); + assert_ser_tokens( + &MetadataValue::String(String::from("test")), + &[Token::String("test")], + ); + assert_ser_tokens( + &MetadataValue::Strings(vec![String::from("one"), String::from("two")]), + &[ + Token::Seq { len: Some(2) }, + Token::String("one"), + Token::String("two"), + Token::SeqEnd, + ], + ); + assert_ser_tokens(&MetadataValue::Unsupported, &[Token::Unit]); + } + + #[test] + fn deserialization() { + assert_de_tokens(&MetadataValue::Boolean(true), &[Token::Bool(true)]); + + assert_de_tokens( + &MetadataValue::UnsignedInt(u64::MAX), + &[Token::U64(u64::MAX)], + ); + + let signed = MetadataValue::SignedInt(0); + let neg = MetadataValue::SignedInt(-1); + assert_de_tokens(&signed, &[Token::I64(0)]); + assert_de_tokens(&signed, &[Token::U64(0)]); + assert_de_tokens(&signed, &[Token::U8(0)]); + assert_de_tokens(&signed, &[Token::I8(0)]); + assert_de_tokens(&neg, &[Token::I64(-1)]); + assert_de_tokens(&neg, &[Token::I8(-1)]); + + let float = MetadataValue::Float(0.0); + assert_de_tokens(&float, &[Token::F32(0.0)]); + assert_de_tokens(&float, &[Token::F64(0.0)]); + + let string = MetadataValue::String(String::from("test")); + assert_de_tokens(&string, &[Token::String("test")]); + assert_de_tokens(&string, &[Token::BorrowedStr("test")]); + + let strings = MetadataValue::Strings(vec![String::from("first"), String::from("second")]); + assert_de_tokens( + &strings, + &[ + Token::Seq { len: Some(2) }, + Token::String("first"), + Token::String("second"), + Token::SeqEnd, + ], + ); + assert_de_tokens( + &strings, + &[ + Token::Seq { len: Some(2) }, + Token::BorrowedStr("first"), + Token::BorrowedStr("second"), + Token::SeqEnd, + ], + ); + + let unsupported = MetadataValue::Unsupported; + assert_de_tokens(&unsupported, &[Token::Unit]); + assert_de_tokens(&unsupported, &[Token::Char('a')]); + assert_de_tokens(&unsupported, &[Token::None]); + assert_de_tokens(&unsupported, &[Token::Map { len: None }, Token::MapEnd]); + assert_de_tokens( + &unsupported, + &[ + Token::StructVariant { + name: "test", + variant: "test", + len: 0, + }, + Token::StructVariantEnd, + ], + ); + } +} diff --git a/src/playlist.rs b/src/playlist.rs index 57e5ba9..e616996 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -5,7 +5,7 @@ use std::{ use futures_util::StreamExt; #[cfg(feature = "serde")] -use serde::Serializer; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use zbus::{ names::BusName, zvariant::{ObjectPath, OwnedObjectPath}, @@ -40,7 +40,7 @@ type InnerPlaylistData = HashMap)>; /// [object_path]: /// https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path #[derive(Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Playlist { #[cfg_attr( feature = "serde", @@ -49,7 +49,14 @@ pub struct Playlist { /// Unique playlist identifier id: OwnedObjectPath, name: String, - #[cfg_attr(feature = "serde", serde(default))] + #[cfg_attr( + feature = "serde", + serde( + default, + deserialize_with = "deserialize_option_string", + serialize_with = "serialize_none_to_empty" + ) + )] icon: Option, } @@ -271,6 +278,30 @@ where ser.serialize_str(object.as_str()) } +#[cfg(feature = "serde")] +fn deserialize_option_string<'de, D>(deser: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deser)?; + if s.is_empty() { + Ok(None) + } else { + Ok(Some(s)) + } +} + +#[cfg(feature = "serde")] +fn serialize_none_to_empty(object: &Option, ser: S) -> Result +where + S: Serializer, +{ + ser.serialize_str(match object { + Some(s) => s, + None => "", + }) +} + impl From<(OwnedObjectPath, String, String)> for Playlist { fn from(value: (OwnedObjectPath, String, String)) -> Self { let icon = if value.2.is_empty() { @@ -460,7 +491,6 @@ mod playlist_serde_tests { Token::Str("name"), Token::String("TestName"), Token::Str("icon"), - Token::Some, Token::String("TestIcon"), Token::StructEnd, ], @@ -479,7 +509,7 @@ mod playlist_serde_tests { Token::Str("name"), Token::String("TestName"), Token::Str("icon"), - Token::None, + Token::Str(""), Token::StructEnd, ], ); From bf0e2a22aa487f1d93b6489921ef9fee239a5d67 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sun, 10 Nov 2024 19:21:28 +0100 Subject: [PATCH 49/55] Rework RawMetadata Instead of being just an alias it's now a struct that just wraps the HashMap. This is done so that it's possible to implement From traits making it a lot easier to covert from the HashMap received from DBus. It implements Deref and other traits to make it act like a HashMap. --- src/metadata/metadata.rs | 160 ++++++++++++++++++++++++++++++++++++--- src/player.rs | 17 +---- src/proxies.rs | 2 +- 3 files changed, 156 insertions(+), 23 deletions(-) diff --git a/src/metadata/metadata.rs b/src/metadata/metadata.rs index 6a4f403..20c518c 100644 --- a/src/metadata/metadata.rs +++ b/src/metadata/metadata.rs @@ -1,10 +1,105 @@ use std::{collections::HashMap, iter::FusedIterator}; +use zbus::zvariant::OwnedValue; + use super::{MetadataValue, TrackID}; use crate::{errors::InvalidMetadata, MprisDuration}; -/// The type of the metadata returned from [`Player::raw_metadata()`][crate::Player::raw_metadata]. -pub type RawMetadata = HashMap; +/// HashMap returned from DBus +type DBusMetadata = HashMap; +type InnerRawMetadata = HashMap; + +/// A struct that represents the raw version of [`Metadata`]. +/// +/// It's a simple wrapper around [HashMap]<[String], [MetadataValue]>. It should act +/// like a [`HashMap`] but it can be easily converted into and from one using the [`From`] traits or +/// [`into_inner()`][Self::into_inner]. +/// +/// Can be obtained from [`Player::raw_metadata()`][crate::Player::raw_metadata]. +#[derive(Clone, PartialEq, Default)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(transparent) +)] +pub struct RawMetadata(InnerRawMetadata); + +impl RawMetadata { + /// Creates a new empty [`RawMetadata`]. + pub fn new() -> Self { + Self(HashMap::new()) + } + + /// Turns [`RawMetadata`] into [HashMap]<[String], [MetadataValue]>. + pub fn into_inner(self) -> InnerRawMetadata { + self.0 + } +} + +impl std::ops::Deref for RawMetadata { + type Target = InnerRawMetadata; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for RawMetadata { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for RawMetadata { + fn from(value: DBusMetadata) -> Self { + Self( + value + .into_iter() + .map(|(k, v)| (k, MetadataValue::from(v))) + .collect(), + ) + } +} + +impl From for InnerRawMetadata { + fn from(value: RawMetadata) -> Self { + value.0 + } +} + +impl From for RawMetadata { + fn from(value: InnerRawMetadata) -> Self { + Self(value) + } +} + +impl FromIterator<(String, MetadataValue)> for RawMetadata { + fn from_iter>(iter: T) -> Self { + Self(HashMap::from_iter(iter)) + } +} + +impl IntoIterator for RawMetadata { + type Item = (String, MetadataValue); + + type IntoIter = std::collections::hash_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl PartialEq for RawMetadata { + fn eq(&self, other: &InnerRawMetadata) -> bool { + &self.0 == other + } +} + +impl std::fmt::Debug for RawMetadata { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map().entries(self.iter()).finish() + } +} /// Macro that auto implements useful things for Metadata without needing to repeat the fields every time /// while preserving documentation for the fields @@ -57,7 +152,7 @@ macro_rules! gen_metadata_struct { pub fn new() -> Self { Self { $($field: None),*, - $others_name: HashMap::new(), + $others_name: RawMetadata::new(), } } @@ -117,7 +212,7 @@ macro_rules! gen_metadata_struct { /// use std::collections::HashMap; /// use mpris::{Metadata, metadata::RawMetadata}; /// - /// let mut raw_meta: RawMetadata = HashMap::new(); + /// let mut raw_meta = RawMetadata::new(); /// // Wrong type for title /// raw_meta.insert(String::from("xesam:title"), 0_u64.into()); /// raw_meta.insert(String::from("xesam:comment"), String::from("Some comment").into()); @@ -212,7 +307,7 @@ gen_metadata_struct!( /// use std::collections::HashMap; /// use mpris::{Metadata, TrackID, metadata::RawMetadata}; /// - /// let mut raw_meta: RawMetadata = HashMap::new(); + /// let mut raw_meta = RawMetadata::new(); /// /// // Empty is valid /// assert!(Metadata::try_from(raw_meta.clone()).is_ok()); @@ -393,7 +488,7 @@ mod metadata_tests { url: None, use_count: None, user_rating: None, - others: HashMap::new(), + others: RawMetadata::new(), }; assert_eq!(empty, Metadata::default()); assert_eq!(empty, Metadata::new()) @@ -437,7 +532,7 @@ mod metadata_tests { #[test] fn try_from_raw() { - let raw_metadata: RawMetadata = HashMap::from_iter([ + let raw_metadata = RawMetadata::from_iter([ ("xesam:albumArtist".to_string(), vec![String::new()].into()), ("xesam:album".to_string(), String::new().into()), ("mpris:artUrl".to_string(), String::new().into()), @@ -486,7 +581,7 @@ mod metadata_tests { url: Some(String::new()), use_count: Some(0), user_rating: Some(0.0), - others: HashMap::from_iter([(String::from("other"), MetadataValue::Unsupported)]), + others: RawMetadata::from_iter([(String::from("other"), MetadataValue::Unsupported)]), }; assert_eq!(meta, Ok(manual_meta)); @@ -494,7 +589,7 @@ mod metadata_tests { #[test] fn try_from_raw_fail() { - let mut map = HashMap::new(); + let mut map = RawMetadata::new(); // Wrong type map.insert("xesam:autoRating".to_string(), true.into()); @@ -542,3 +637,50 @@ mod metadata_iterator_tests { } } } + +#[cfg(test)] +mod raw_metadata { + use super::*; + + #[test] + fn new_is_default() { + let manual = RawMetadata(HashMap::new()); + assert_eq!(manual, RawMetadata::new()); + assert_eq!(manual, RawMetadata::default()); + } + + #[test] + fn deref() { + let mut meta = RawMetadata::new(); + assert!(meta.is_empty()); + meta.insert(String::from("Key"), MetadataValue::Unsupported); + assert_eq!(meta["Key"], MetadataValue::Unsupported); + assert!(!meta.is_empty()); + meta.clear(); + assert!(meta.is_empty()) + } + + #[test] + fn from_hash_map() { + let map: HashMap = + HashMap::from_iter([(String::from("Some"), OwnedValue::from(0_u64))]); + let meta = RawMetadata::from(map); + assert_eq!(meta.get("Some"), Some(&MetadataValue::UnsignedInt(0))); + assert_eq!(meta.get("Other"), None); + } + + #[test] + fn iter_and_eq() { + let values = [ + (String::from("Bool"), MetadataValue::Boolean(false)), + (String::from("Number"), MetadataValue::UnsignedInt(0)), + ( + String::from("String"), + MetadataValue::String(String::from("Value")), + ), + ]; + let meta = RawMetadata::from_iter(values.clone()); + let map = HashMap::from_iter(values); + assert_eq!(meta, map); + } +} diff --git a/src/player.rs b/src/player.rs index 7bf241c..cd10597 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,10 +1,8 @@ -use std::collections::HashMap; - use futures_util::{join, try_join}; use zbus::{names::BusName, Connection}; use crate::{ - metadata::{MetadataValue, RawMetadata}, + metadata::RawMetadata, playlist::PlaylistsInterface, proxies::{DBusProxy, MediaPlayer2Proxy, PlayerProxy, TrackListProxy}, LoopStatus, Metadata, Mpris, MprisDuration, MprisError, PlaybackStatus, Playlist, @@ -310,10 +308,7 @@ impl Player { /// Similar to [`metadata()`][Self::metadata] but doesn't perform any checks or conversions. See /// [`Metadata::from_raw_lossy()`] for details. pub async fn raw_metadata(&self) -> Result { - let data = self.player_proxy.metadata().await?; - let raw: HashMap = - data.into_iter().map(|(k, v)| (k, v.into())).collect(); - Ok(raw) + Ok(self.player_proxy.metadata().await?.into()) } /// Checks if the player is still connected. @@ -1307,16 +1302,12 @@ impl Player { ) -> Result, MprisError> { let result = self .check_track_list_support()? - .get_tracks_metadata(&tracks.iter().map(|t| t.as_ref()).collect::>()) + .get_tracks_metadata(&tracks.iter().map(|x| x.as_ref()).collect::>()) .await?; let mut metadata = Vec::with_capacity(tracks.len()); for meta in result { - let raw: HashMap = meta - .into_iter() - .map(|(k, v)| (k, MetadataValue::from(v))) - .collect(); - metadata.push(Metadata::try_from(raw)?); + metadata.push(Metadata::try_from(RawMetadata::from(meta))?); } Ok(metadata) } diff --git a/src/proxies.rs b/src/proxies.rs index 2c3861e..f2c4bc7 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -156,7 +156,7 @@ pub(crate) trait Player { /// Metadata property #[zbus(property)] - fn metadata(&self) -> zbus::Result>; + fn metadata(&self) -> zbus::Result>; /// MinimumRate property #[zbus(property)] From a500dcca27a64d1c82ecd1585df0797ca4177f95 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sun, 10 Nov 2024 21:36:05 +0100 Subject: [PATCH 50/55] Switch to owned values The proxies now return owned values since they are getting cloned anyway. --- src/playlist.rs | 19 ++++++++++--------- src/proxies.rs | 18 +++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/playlist.rs b/src/playlist.rs index e616996..b4ae6b0 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -6,11 +6,7 @@ use std::{ use futures_util::StreamExt; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use zbus::{ - names::BusName, - zvariant::{ObjectPath, OwnedObjectPath}, - Connection, Task, -}; +use zbus::{names::BusName, zvariant::OwnedObjectPath, Connection, Task}; use crate::proxies::PlaylistsProxy; use crate::{InvalidPlaylist, InvalidPlaylistOrdering, MprisError}; @@ -107,8 +103,8 @@ impl Playlist { } /// Gets the `id` as a borrowed [`ObjectPath`] - pub fn get_id(&self) -> ObjectPath<'_> { - self.id.as_ref() + pub fn get_id(&self) -> &OwnedObjectPath { + &self.id } /// Gets the `id` as a &[`str`] @@ -186,7 +182,7 @@ impl PlaylistsInterface { } pub(crate) async fn activate_playlist(&self, playlist: &Playlist) -> Result<(), MprisError> { - Ok(self.proxy.activate_playlist(&playlist.get_id()).await?) + Ok(self.proxy.activate_playlist(playlist.get_id()).await?) } /// Wraps the proxy method of the same name and updates the internal data. @@ -434,6 +430,7 @@ mod playlist_ordering_tests { #[cfg(test)] mod playlist_tests { use super::*; + use zbus::zvariant::ObjectPath; #[test] fn new() { @@ -459,7 +456,10 @@ mod playlist_tests { ); assert_eq!(new.get_name(), "TestName"); assert_eq!(new.get_icon(), Some("TestIcon")); - assert_eq!(new.get_id(), ObjectPath::from_str_unchecked("/valid/path")); + assert_eq!( + new.get_id().as_ref(), + ObjectPath::from_str_unchecked("/valid/path") + ); assert_eq!(new.get_id_as_str(), "/valid/path"); new.icon = None; @@ -471,6 +471,7 @@ mod playlist_tests { mod playlist_serde_tests { use super::*; use serde_test::{assert_de_tokens, assert_de_tokens_error, assert_tokens, Token}; + use zbus::zvariant::ObjectPath; #[test] fn serialization() { diff --git a/src/proxies.rs b/src/proxies.rs index f2c4bc7..cc026ad 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use zbus::names::{BusName, OwnedUniqueName}; use zbus::proxy; -use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value}; +use zbus::zvariant::{OwnedObjectPath, OwnedValue}; #[proxy( default_service = "org.freedesktop.DBus", @@ -199,7 +199,7 @@ pub(crate) trait Player { )] pub(crate) trait Playlists { /// ActivatePlaylist method - fn activate_playlist(&self, playlist_id: &ObjectPath<'_>) -> zbus::Result<()>; + fn activate_playlist(&self, playlist_id: &OwnedObjectPath) -> zbus::Result<()>; /// GetPlaylists method fn get_playlists( @@ -256,29 +256,29 @@ pub trait TrackList { #[zbus(signal)] fn track_added( &self, - metadata: HashMap<&str, Value<'_>>, - after_track: ObjectPath<'_>, + metadata: HashMap, + after_track: OwnedObjectPath, ) -> zbus::Result<()>; /// TrackListReplaced signal #[zbus(signal)] fn track_list_replaced( &self, - track_ids: Vec>, - current_track: ObjectPath<'_>, + track_ids: Vec, + current_track: OwnedObjectPath, ) -> zbus::Result<()>; /// TrackMetadataChanged signal #[zbus(signal)] fn track_metadata_changed( &self, - track_id: ObjectPath<'_>, - metadata: HashMap<&str, Value<'_>>, + track_id: OwnedObjectPath, + metadata: HashMap, ) -> zbus::Result<()>; /// TrackRemoved signal #[zbus(signal)] - fn track_removed(&self, track_id: ObjectPath<'_>) -> zbus::Result<()>; + fn track_removed(&self, track_id: OwnedObjectPath) -> zbus::Result<()>; /// CanEditTracks property #[zbus(property)] From e45c2f4e345d7105f839d5087ee24d857b448eea Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Mon, 11 Nov 2024 18:32:08 +0100 Subject: [PATCH 51/55] Move serde related functions to a separate module --- src/lib.rs | 2 ++ src/metadata/values.rs | 18 ++++++--------- src/playlist.rs | 50 ++++-------------------------------------- src/serde_util.rs | 48 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 57 deletions(-) create mode 100644 src/serde_util.rs diff --git a/src/lib.rs b/src/lib.rs index 198a735..82b9297 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,8 @@ pub mod metadata; mod player; mod playlist; mod proxies; +#[cfg(feature = "serde")] +pub(crate) mod serde_util; use errors::*; diff --git a/src/metadata/values.rs b/src/metadata/values.rs index b87399e..cda5cc6 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -1,8 +1,8 @@ -#[cfg(feature = "serde")] -use serde::{de::IgnoredAny, Deserialize, Deserializer, Serialize}; use zbus::zvariant::{OwnedValue, Value}; use crate::errors::InvalidMetadataValue; +#[cfg(feature = "serde")] +use crate::serde_util::deser_no_fail; /// Subset of [DBus data types][dbus_types] that are commonly used in MPRIS metadata. /// @@ -13,7 +13,11 @@ use crate::errors::InvalidMetadataValue; /// [dbus_types]: https://dbus.freedesktop.org/doc/dbus-specification.html#type-system /// [meta_spec]: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ #[derive(Debug, Clone, PartialEq, PartialOrd)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(untagged))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(untagged) +)] #[allow(missing_docs)] pub enum MetadataValue { Boolean(bool), @@ -26,14 +30,6 @@ pub enum MetadataValue { Unsupported, } -#[cfg(feature = "serde")] -fn deser_no_fail<'de, D>(d: D) -> Result<(), D::Error> -where - D: Deserializer<'de>, -{ - IgnoredAny::deserialize(d).map(|_| ()) -} - impl MetadataValue { /// Tries to turn itself into a non-empty [`String`] /// diff --git a/src/playlist.rs b/src/playlist.rs index b4ae6b0..297c438 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -4,11 +4,11 @@ use std::{ }; use futures_util::StreamExt; -#[cfg(feature = "serde")] -use serde::{Deserialize, Deserializer, Serialize, Serializer}; use zbus::{names::BusName, zvariant::OwnedObjectPath, Connection, Task}; use crate::proxies::PlaylistsProxy; +#[cfg(feature = "serde")] +use crate::serde_util::{option_string, serialize_owned_object_path}; use crate::{InvalidPlaylist, InvalidPlaylistOrdering, MprisError}; type InnerPlaylistData = HashMap)>; @@ -36,7 +36,7 @@ type InnerPlaylistData = HashMap)>; /// [object_path]: /// https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path #[derive(Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Playlist { #[cfg_attr( feature = "serde", @@ -45,14 +45,7 @@ pub struct Playlist { /// Unique playlist identifier id: OwnedObjectPath, name: String, - #[cfg_attr( - feature = "serde", - serde( - default, - deserialize_with = "deserialize_option_string", - serialize_with = "serialize_none_to_empty" - ) - )] + #[cfg_attr(feature = "serde", serde(default, with = "option_string"))] icon: Option, } @@ -263,41 +256,6 @@ impl std::fmt::Debug for Playlist { } } -#[cfg(feature = "serde")] -pub(crate) fn serialize_owned_object_path( - object: &OwnedObjectPath, - ser: S, -) -> Result -where - S: Serializer, -{ - ser.serialize_str(object.as_str()) -} - -#[cfg(feature = "serde")] -fn deserialize_option_string<'de, D>(deser: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let s = String::deserialize(deser)?; - if s.is_empty() { - Ok(None) - } else { - Ok(Some(s)) - } -} - -#[cfg(feature = "serde")] -fn serialize_none_to_empty(object: &Option, ser: S) -> Result -where - S: Serializer, -{ - ser.serialize_str(match object { - Some(s) => s, - None => "", - }) -} - impl From<(OwnedObjectPath, String, String)> for Playlist { fn from(value: (OwnedObjectPath, String, String)) -> Self { let icon = if value.2.is_empty() { diff --git a/src/serde_util.rs b/src/serde_util.rs new file mode 100644 index 0000000..b09db58 --- /dev/null +++ b/src/serde_util.rs @@ -0,0 +1,48 @@ +use serde::{de::IgnoredAny, Deserialize, Deserializer, Serializer}; +use zbus::zvariant::OwnedObjectPath; + +/// Serializes OwnedObjectPath into simple string +pub(crate) fn serialize_owned_object_path( + object: &OwnedObjectPath, + ser: S, +) -> Result +where + S: Serializer, +{ + ser.serialize_str(object.as_str()) +} + +/// Takes anything and returns a unit +pub(crate) fn deser_no_fail<'de, D>(d: D) -> Result<(), D::Error> +where + D: Deserializer<'de>, +{ + IgnoredAny::deserialize(d).map(|_| ()) +} + +/// Deals with Option +pub(crate) mod option_string { + use super::*; + + pub(crate) fn serialize(object: &Option, ser: S) -> Result + where + S: Serializer, + { + ser.serialize_str(match object { + Some(s) => s, + None => "", + }) + } + + pub(crate) fn deserialize<'de, D>(deser: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deser)?; + if s.is_empty() { + Ok(None) + } else { + Ok(Some(s)) + } + } +} From bc671dc6cec2e9cea4b8172ff3c005eda6820e56 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Mon, 11 Nov 2024 18:34:00 +0100 Subject: [PATCH 52/55] Add MetadataValue::TrackID No point in going to and back from a `String` when we known that it's an `ObjectPath`. Since only the track id metadata key should have a path we can just use `TrackID`. Also added `TrackID::from_str_unchecked()` to make tests easier to write. --- src/metadata/track_id.rs | 56 +++++++++++++++++----------------------- src/metadata/values.rs | 18 ++++++++++--- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index 46c43c8..ede4116 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -82,6 +82,11 @@ impl TrackID { pub fn as_object_path(&self) -> ObjectPath<'_> { self.0.as_ref() } + + #[cfg(test)] + pub(crate) fn from_str_unchecked(s: &'static str) -> Self { + Self::from(ObjectPath::from_static_str_unchecked(s)) + } } impl AsRef for TrackID { @@ -168,7 +173,8 @@ impl TryFrom for TrackID { MetadataValue::Strings(mut s) if s.len() == 1 => { s.pop().expect("length should be 1").try_into() } - _ => Err(InvalidTrackID(String::from("not a string"))), + MetadataValue::TrackID(t) => Ok(t), + _ => Err(InvalidTrackID(String::from("not a string or track id"))), } } } @@ -238,9 +244,7 @@ mod tests { #[test] fn no_track() { let track = TrackID::no_track(); - let manual = TrackID(OwnedObjectPath::from( - ObjectPath::from_static_str_unchecked(TrackID::NO_TRACK), - )); + let manual = TrackID::from_str_unchecked(TrackID::NO_TRACK); assert!(track.is_no_track()); assert!(manual.is_no_track()); @@ -259,30 +263,19 @@ mod tests { #[test] fn valid_track_id() { - assert_eq!( - TrackID::try_from("/"), - Ok(TrackID(OwnedObjectPath::from( - ObjectPath::from_str_unchecked("/") - ))) - ); + assert_eq!(TrackID::try_from("/"), Ok(TrackID::from_str_unchecked("/"))); assert_eq!( TrackID::try_from("/some/path"), - Ok(TrackID(OwnedObjectPath::from( - ObjectPath::from_str_unchecked("/some/path") - ))) + Ok(TrackID::from_str_unchecked("/some/path")) ); assert_eq!( TrackID::try_from("/".to_string()), - Ok(TrackID(OwnedObjectPath::from( - ObjectPath::from_str_unchecked("/") - ))) + Ok(TrackID::from_str_unchecked("/")) ); assert_eq!( TrackID::try_from("/some/path".to_string()), - Ok(TrackID(OwnedObjectPath::from( - ObjectPath::from_str_unchecked("/some/path") - ))) + Ok(TrackID::from_str_unchecked("/some/path")) ); } @@ -303,43 +296,40 @@ mod tests { fn from_object_path() { assert_eq!( TrackID::from(ObjectPath::from_str_unchecked("/valid/path")), - TrackID(OwnedObjectPath::from(ObjectPath::from_str_unchecked( - "/valid/path" - ))) + TrackID::from_str_unchecked("/valid/path") ); assert_eq!( TrackID::from(OwnedObjectPath::from(ObjectPath::from_str_unchecked( "/valid/path" ))), - TrackID(OwnedObjectPath::from(ObjectPath::from_str_unchecked( - "/valid/path" - ))) + TrackID::from_str_unchecked("/valid/path") ); } #[test] fn from_metadata_value() { + let valid_track = Ok(TrackID::from_str_unchecked("/valid/path")); assert!(TrackID::try_from(MetadataValue::Boolean(true)).is_err()); assert!(TrackID::try_from(MetadataValue::Float(0.0)).is_err()); assert!(TrackID::try_from(MetadataValue::SignedInt(0)).is_err()); assert!(TrackID::try_from(MetadataValue::UnsignedInt(0)).is_err()); assert_eq!( TrackID::try_from(MetadataValue::String(String::from("/valid/path"))), - Ok(TrackID(OwnedObjectPath::from( - ObjectPath::from_str_unchecked("/valid/path") - ))) + valid_track, ); assert!(TrackID::try_from(MetadataValue::Strings(vec![])).is_err()); assert_eq!( TrackID::try_from(MetadataValue::Strings(vec![String::from("/valid/path")])), - Ok(TrackID(OwnedObjectPath::from( - ObjectPath::from_str_unchecked("/valid/path") - ))) + valid_track ); assert!( TrackID::try_from(MetadataValue::Strings(vec![String::from("/valid/path"); 2])) .is_err() ); + assert_eq!( + TrackID::try_from(MetadataValue::TrackID(valid_track.clone().unwrap())), + valid_track + ); assert!(TrackID::try_from(MetadataValue::Unsupported).is_err()); } @@ -360,13 +350,13 @@ mod serde_tests { #[test] fn test_serialization() { - let track_id = TrackID::try_from("/foo/bar").unwrap(); + let track_id = TrackID::from_str_unchecked("/foo/bar"); assert_ser_tokens(&track_id, &[Token::Str("/foo/bar")]); } #[test] fn test_deserialization() { - let track_id = TrackID::try_from("/foo/bar").unwrap(); + let track_id = TrackID::from_str_unchecked("/foo/bar"); assert_de_tokens(&track_id, &[Token::Str("/foo/bar")]); } } diff --git a/src/metadata/values.rs b/src/metadata/values.rs index cda5cc6..e2008c5 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -1,5 +1,6 @@ use zbus::zvariant::{OwnedValue, Value}; +use super::TrackID; use crate::errors::InvalidMetadataValue; #[cfg(feature = "serde")] use crate::serde_util::deser_no_fail; @@ -26,6 +27,7 @@ pub enum MetadataValue { Float(f64), String(String), Strings(Vec), + TrackID(TrackID), #[cfg_attr(feature = "serde", serde(deserialize_with = "deser_no_fail"))] Unsupported, } @@ -115,7 +117,7 @@ impl<'a> From> for MetadataValue { Value::Str(v) => MetadataValue::String(v.to_string()), Value::Signature(v) => MetadataValue::String(v.to_string()), - Value::ObjectPath(v) => MetadataValue::String(v.to_string()), + Value::ObjectPath(v) => MetadataValue::TrackID(TrackID::from(v)), Value::Array(a) if a.full_signature() == "as" => { let mut strings = Vec::with_capacity(a.len()); @@ -173,9 +175,9 @@ impl From> for MetadataValue { } } -impl From for MetadataValue { - fn from(value: super::TrackID) -> Self { - Self::String(value.into()) +impl From for MetadataValue { + fn from(value: TrackID) -> Self { + Self::TrackID(value) } } @@ -356,6 +358,10 @@ mod metadata_value_serde { assert_ser_tokens(&MetadataValue::SignedInt(-1), &[Token::I64(-1)]); assert_ser_tokens(&MetadataValue::SignedInt(0), &[Token::I64(0)]); assert_ser_tokens(&MetadataValue::Float(0.0), &[Token::F64(0.0)]); + assert_ser_tokens( + &MetadataValue::TrackID(TrackID::try_from("/valid/path").unwrap()), + &[Token::Str("/valid/path")], + ); assert_ser_tokens( &MetadataValue::String(String::from("test")), &[Token::String("test")], @@ -394,6 +400,10 @@ mod metadata_value_serde { assert_de_tokens(&float, &[Token::F32(0.0)]); assert_de_tokens(&float, &[Token::F64(0.0)]); + assert_de_tokens( + &MetadataValue::String(String::from("/valid/path")), + &[Token::Str("/valid/path")], + ); let string = MetadataValue::String(String::from("test")); assert_de_tokens(&string, &[Token::String("test")]); assert_de_tokens(&string, &[Token::BorrowedStr("test")]); From 9abb8f866471eb1ed8fb5f9dd5c5da0005291863 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sat, 16 Nov 2024 20:12:44 +0100 Subject: [PATCH 53/55] Add conversions from small ints for MprisDuration --- src/duration.rs | 100 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/src/duration.rs b/src/duration.rs index fd613ec..19d31f7 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -204,11 +204,61 @@ macro_rules! impl_math { }; } +macro_rules! impl_unsigned_small { + ($type:ty) => { + impl From<$type> for MprisDuration { + fn from(value: $type) -> Self { + Self(value as u64) + } + } + + impl TryFrom for $type { + type Error = std::num::TryFromIntError; + + fn try_from(value: MprisDuration) -> Result { + <$type>::try_from(value.0) + } + } + }; +} + +macro_rules! impl_signed_small { + ($type:ty) => { + impl TryFrom<$type> for MprisDuration { + type Error = InvalidMprisDuration; + + fn try_from(value: $type) -> Result { + if value < 0 { + Err(InvalidMprisDuration::new_negative()) + } else { + Ok(Self(value as u64)) + } + } + } + + impl TryFrom for $type { + type Error = std::num::TryFromIntError; + + fn try_from(value: MprisDuration) -> Result { + <$type>::try_from(value.0) + } + } + }; +} + impl_math!(Mul, mul, saturating_mul); impl_math!(Div, div, saturating_div); impl_math!(Add, add, saturating_add); impl_math!(Sub, sub, saturating_sub); +impl_unsigned_small!(u8); +impl_unsigned_small!(u16); +impl_unsigned_small!(u32); + +impl_signed_small!(i8); +impl_signed_small!(i16); +impl_signed_small!(i32); + #[cfg(test)] mod mrpis_duration_tests { use super::*; @@ -276,6 +326,56 @@ mod mrpis_duration_tests { assert!(MprisDuration::try_from(MAX + 1).is_err()); } + macro_rules! gen_small_unsigned_test { + ($type:ty) => { + assert_eq!(MprisDuration::from(<$type>::MIN), MprisDuration(0)); + assert_eq!( + MprisDuration::from(<$type>::MAX), + MprisDuration(<$type>::MAX as u64) + ); + + assert_eq!(<$type>::try_from(MprisDuration(0)), Ok(0)); + assert_eq!( + <$type>::try_from(MprisDuration(<$type>::MAX as u64)), + Ok(<$type>::MAX) + ); + assert!(<$type>::try_from(MprisDuration::new_max()).is_err()); + }; + } + + macro_rules! gen_small_signed_test { + ($type:ty) => { + assert!(MprisDuration::try_from(<$type>::MIN).is_err()); + let zero: $type = 0; + assert_eq!(MprisDuration::try_from(zero), Ok(MprisDuration(0))); + assert_eq!( + MprisDuration::try_from(<$type>::MAX), + Ok(MprisDuration(<$type>::MAX as u64)) + ); + + assert_eq!(<$type>::try_from(MprisDuration(0)), Ok(0)); + assert_eq!( + <$type>::try_from(MprisDuration(<$type>::MAX as u64)), + Ok(<$type>::MAX) + ); + assert!(<$type>::try_from(MprisDuration::new_max()).is_err()); + }; + } + + #[test] + fn small_unsigned_conversions() { + gen_small_unsigned_test!(u8); + gen_small_unsigned_test!(u16); + gen_small_unsigned_test!(u32); + } + + #[test] + fn small_signed_conversions() { + gen_small_signed_test!(i8); + gen_small_signed_test!(i16); + gen_small_signed_test!(i32); + } + #[test] fn try_from_metadata_value() { assert!(MprisDuration::try_from(MetadataValue::Boolean(false)).is_err()); From 68c89fa0bf0707c2f97396067e5405eec3594158 Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sat, 16 Nov 2024 20:21:45 +0100 Subject: [PATCH 54/55] Add helper function for errors --- src/duration.rs | 4 +++- src/errors.rs | 7 +++++++ src/metadata/track_id.rs | 6 +++--- src/metadata/values.rs | 24 ++++++++++-------------- src/player.rs | 2 +- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/duration.rs b/src/duration.rs index 19d31f7..67efc2d 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -147,7 +147,9 @@ impl TryFrom for MprisDuration { match value { MetadataValue::SignedInt(int) => int.try_into(), MetadataValue::UnsignedInt(int) => int.try_into(), - _ => Err(InvalidMprisDuration::from("unsupported MetadataValue type")), + _ => Err(InvalidMprisDuration::expected( + "MetadataValue::SignedInt or MetadataValue::UnsignedInt", + )), } } } diff --git a/src/errors.rs b/src/errors.rs index d30fa87..01e5925 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -19,6 +19,13 @@ macro_rules! generate_error { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct $error(pub(crate) String); + impl $error { + #[allow(dead_code)] + pub(crate) fn expected(expected: &str) -> Self { + Self(format!(r"invalid value, expected: {expected}")) + } + } + impl Display for $error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index ede4116..7d14bdc 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -174,7 +174,7 @@ impl TryFrom for TrackID { s.pop().expect("length should be 1").try_into() } MetadataValue::TrackID(t) => Ok(t), - _ => Err(InvalidTrackID(String::from("not a string or track id"))), + _ => Err(InvalidTrackID::expected("String or TrackID")), } } } @@ -194,7 +194,7 @@ impl TryFrom> for TrackID { match value { Value::Str(s) => Self::try_from(s.as_str()), Value::ObjectPath(path) => Ok(Self::from(path)), - _ => Err(InvalidTrackID::from("not a String or ObjectPath")), + _ => Err(InvalidTrackID::expected("Str or ObjectPath")), } } } @@ -207,7 +207,7 @@ impl From for TrackID { impl From> for TrackID { fn from(value: ObjectPath) -> Self { - Self(value.into()) + Self(OwnedObjectPath::from(value)) } } diff --git a/src/metadata/values.rs b/src/metadata/values.rs index e2008c5..b873d03 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -193,9 +193,7 @@ impl TryFrom for bool { fn try_from(value: MetadataValue) -> Result { match value { MetadataValue::Boolean(v) => Ok(v), - _ => Err(InvalidMetadataValue::from( - "expected MetadataValue::Boolean", - )), + _ => Err(InvalidMetadataValue::expected("MetadataValue::Boolean")), } } } @@ -206,7 +204,7 @@ impl TryFrom for f64 { fn try_from(value: MetadataValue) -> Result { match value { MetadataValue::Float(v) => Ok(v), - _ => Err(InvalidMetadataValue::from("expected MetadataValue::Float")), + _ => Err(InvalidMetadataValue::expected("MetadataValue::Float")), } } } @@ -224,8 +222,8 @@ impl TryFrom for i64 { Err(InvalidMetadataValue::from("value too big for i64")) } } - _ => Err(InvalidMetadataValue::from( - "expected MetadataValue::SignedInt or MetadataValue::UnsignedInt", + _ => Err(InvalidMetadataValue::expected( + "MetadataValue::SignedInt or MetadataValue::UnsignedInt", )), } } @@ -244,8 +242,8 @@ impl TryFrom for u64 { Err(InvalidMetadataValue::from("value is negative")) } } - _ => Err(InvalidMetadataValue::from( - "expected MetadataValue::SignedInt or MetadataValue::UnsignedInt", + _ => Err(InvalidMetadataValue::expected( + "MetadataValue::SignedInt or MetadataValue::UnsignedInt", )), } } @@ -266,8 +264,8 @@ impl TryFrom for String { )) } } - _ => Err(InvalidMetadataValue::from( - "expected MetadataValue::Strings or MetadataValue::String", + _ => Err(InvalidMetadataValue::expected( + "MetadataValue::Strings or MetadataValue::String", )), } } @@ -280,8 +278,8 @@ impl TryFrom for Vec { match value { MetadataValue::String(v) => Ok(vec![v]), MetadataValue::Strings(v) => Ok(v), - _ => Err(InvalidMetadataValue::from( - "expected MetadataValue::Strings or MetadataValue::String", + _ => Err(InvalidMetadataValue::expected( + "MetadataValue::Strings or MetadataValue::String", )), } } @@ -345,8 +343,6 @@ mod metadata_value_integer_tests { #[cfg(all(test, feature = "serde"))] mod metadata_value_serde { - use std::u64; - use super::*; use serde_test::{assert_de_tokens, assert_ser_tokens, Token}; diff --git a/src/player.rs b/src/player.rs index cd10597..c58aa9a 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1318,7 +1318,7 @@ impl std::fmt::Debug for Player { f.debug_struct("Player") .field("bus_name", &self.bus_name()) .field("track_list", &self.track_list_proxy.is_some()) - .field("playlist", &self.playlist_interface.is_some()) + .field("playlists", &self.playlist_interface.is_some()) .finish_non_exhaustive() } } From e858f1ccb12768841b896f790acab6ba6694f77e Mon Sep 17 00:00:00 2001 From: Kanjirito Date: Sat, 16 Nov 2024 21:03:00 +0100 Subject: [PATCH 55/55] Use custom types with zbus zbus allows to directly use custom types in the proxy macro but do that some of the types need to implement serde because of this serde is no longer an optional dependency. This commit adds all of the needed changes needed to integrate the types into zbus. This includes serde implementations, adding `From` and `Into` for the zbus Error types. In the cases where it was impossible to integrate the type (`MetadataValue` and `Playlist`) in a nice way the proxy methods are just wrapped with new methods that avoid the issue. --- Cargo.toml | 4 +- src/duration.rs | 49 ++++++++-- src/errors.rs | 24 +++++ src/lib.rs | 50 +++++++++- src/metadata/metadata.rs | 45 ++++++--- src/metadata/track_id.rs | 19 ++-- src/metadata/values.rs | 13 +-- src/player.rs | 130 +++++++++++-------------- src/playlist.rs | 204 ++++++++++++++++++++++++++++++++------- src/proxies.rs | 174 +++++++++++++++++++-------------- 10 files changed, 490 insertions(+), 222 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6d37805..b1eb002 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,12 +18,10 @@ maintenance = { status = "actively-developed" } # Enable zbus' tight tokio integration; in case you want to use this crate in a # Tokio-based app. tokio = ["zbus/tokio"] -# Enable serde support for metadata -serde = ["dep:serde"] [dependencies] zbus = "4.4.0" -serde = { version = "1.0.164", optional = true } +serde ="1.0.164" futures-util = "0.3.31" # Example dependencies diff --git a/src/duration.rs b/src/duration.rs index 67efc2d..b29e404 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -3,6 +3,8 @@ use std::{ time::Duration, }; +use zbus::zvariant::{OwnedValue, Value}; + use crate::{errors::InvalidMprisDuration, metadata::MetadataValue}; const MAX: u64 = i64::MAX as u64; @@ -39,12 +41,22 @@ const MAX: u64 = i64::MAX as u64; /// ## Ops /// [`MprisDuration`] implements [`Add`], [`Sub`], [`Mul`] and [`Div`] for itself, [`Duration`] and /// [`u64`]. The values will stay valid. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord, Default)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(try_from = "i64", into = "i64") +#[derive( + Debug, + PartialEq, + Eq, + Clone, + Copy, + Hash, + PartialOrd, + Ord, + Default, + serde::Serialize, + serde::Deserialize, + zbus::zvariant::Type, )] +#[serde(try_from = "i64", into = "i64")] +#[zvariant(signature = "x")] pub struct MprisDuration(u64); impl MprisDuration { @@ -154,6 +166,31 @@ impl TryFrom for MprisDuration { } } +impl TryFrom> for MprisDuration { + type Error = InvalidMprisDuration; + + fn try_from(value: Value<'_>) -> Result { + match value { + Value::U8(v) => Ok(Self::from(v)), + Value::U16(v) => Ok(Self::from(v)), + Value::U32(v) => Ok(Self::from(v)), + Value::U64(v) => Self::try_from(v), + Value::I16(v) => Self::try_from(v), + Value::I32(v) => Self::try_from(v), + Value::I64(v) => Self::try_from(v), + _ => Err(InvalidMprisDuration::expected("integer value")), + } + } +} + +impl TryFrom for MprisDuration { + type Error = InvalidMprisDuration; + + fn try_from(value: OwnedValue) -> Result { + Self::try_from(Value::from(value)) + } +} + macro_rules! impl_math { ($trait:ident, $method:ident, $sat:ident) => { impl $trait for MprisDuration { @@ -465,7 +502,7 @@ mod ops_tests { } } -#[cfg(all(test, feature = "serde"))] +#[cfg(test)] mod mpris_duration_serde_tests { use super::*; use serde_test::{assert_de_tokens_error, assert_tokens, Token}; diff --git a/src/errors.rs b/src/errors.rs index 01e5925..b7e9317 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -43,6 +43,18 @@ macro_rules! generate_error { Self(value.to_string()) } } + + impl From<$error> for Error { + fn from(_: $error) -> Self { + zbus::zvariant::Error::IncorrectType.into() + } + } + + impl From<$error> for zbus::zvariant::Error { + fn from(_: $error) -> Self { + Self::IncorrectType + } + } }; } @@ -107,6 +119,18 @@ impl From for MprisError { } } +impl From for MprisError { + fn from(value: zbus::zvariant::Error) -> Self { + MprisError::DbusError(value.into()) + } +} + +impl From for MprisError { + fn from(value: zbus::fdo::Error) -> Self { + Self::DbusError(value.into()) + } +} + impl From for MprisError { fn from(value: InvalidPlaybackStatus) -> Self { Self::ParseError(value.0) diff --git a/src/lib.rs b/src/lib.rs index 82b9297..3525844 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,6 +87,7 @@ use std::pin::Pin; use std::task::{Context, Poll}; use futures_util::stream::{FusedStream, Stream, TryStreamExt}; +use zbus::zvariant::{OwnedValue, Value}; use zbus::{ names::{BusName, WellKnownName}, Connection, @@ -98,7 +99,6 @@ pub mod metadata; mod player; mod playlist; mod proxies; -#[cfg(feature = "serde")] pub(crate) mod serde_util; use errors::*; @@ -537,7 +537,7 @@ impl ::std::str::FromStr for PlaybackStatus { impl PlaybackStatus { /// Returns it's value as a &[str] - pub fn as_str(&self) -> &str { + pub fn as_str(&self) -> &'static str { match self { PlaybackStatus::Playing => "Playing", PlaybackStatus::Paused => "Paused", @@ -546,6 +546,25 @@ impl PlaybackStatus { } } +impl TryFrom> for PlaybackStatus { + type Error = InvalidPlaybackStatus; + + fn try_from(value: Value<'_>) -> Result { + match value { + Value::Str(s) => s.parse(), + _ => Err(InvalidPlaybackStatus::expected("Value::Str")), + } + } +} + +impl TryFrom for PlaybackStatus { + type Error = InvalidPlaybackStatus; + + fn try_from(value: OwnedValue) -> Result { + Self::try_from(Value::from(value)) + } +} + impl Display for PlaybackStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) @@ -585,7 +604,7 @@ impl ::std::str::FromStr for LoopStatus { impl LoopStatus { /// Returns it's value as a &[str] - pub fn as_str(&self) -> &str { + pub fn as_str(&self) -> &'static str { match self { LoopStatus::None => "None", LoopStatus::Track => "Track", @@ -594,6 +613,31 @@ impl LoopStatus { } } +impl From for Value<'static> { + fn from(value: LoopStatus) -> Value<'static> { + Value::Str(value.as_str().into()) + } +} + +impl TryFrom> for LoopStatus { + type Error = InvalidLoopStatus; + + fn try_from(value: Value<'_>) -> Result { + match value { + Value::Str(s) => s.parse(), + _ => Err(InvalidLoopStatus::expected("Value::Str")), + } + } +} + +impl TryFrom for LoopStatus { + type Error = InvalidLoopStatus; + + fn try_from(value: OwnedValue) -> Result { + Self::try_from(Value::from(value)) + } +} + impl Display for LoopStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) diff --git a/src/metadata/metadata.rs b/src/metadata/metadata.rs index 20c518c..bdaa92a 100644 --- a/src/metadata/metadata.rs +++ b/src/metadata/metadata.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, iter::FusedIterator}; -use zbus::zvariant::OwnedValue; +use zbus::zvariant::{Error as ZError, OwnedValue, Type, Value}; use super::{MetadataValue, TrackID}; use crate::{errors::InvalidMetadata, MprisDuration}; @@ -16,12 +16,9 @@ type InnerRawMetadata = HashMap; /// [`into_inner()`][Self::into_inner]. /// /// Can be obtained from [`Player::raw_metadata()`][crate::Player::raw_metadata]. -#[derive(Clone, PartialEq, Default)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(transparent) -)] +#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, Type)] +#[serde(transparent)] +#[zvariant(signature = "a{sv}")] pub struct RawMetadata(InnerRawMetadata); impl RawMetadata { @@ -79,6 +76,22 @@ impl FromIterator<(String, MetadataValue)> for RawMetadata { } } +impl TryFrom> for RawMetadata { + type Error = ZError; + + fn try_from(value: Value<'_>) -> Result { + InnerRawMetadata::try_from(value).map(Self) + } +} + +impl TryFrom for RawMetadata { + type Error = ZError; + + fn try_from(value: OwnedValue) -> Result { + InnerRawMetadata::try_from(value).map(Self) + } +} + impl IntoIterator for RawMetadata { type Item = (String, MetadataValue); @@ -264,6 +277,7 @@ macro_rules! gen_metadata_struct { // Fails if MetadataValue is of the wrong type for the field or if mpris:trackid" is missing impl TryFrom for $name { type Error = InvalidMetadata; + fn try_from(mut raw: RawMetadata) -> Result { if raw.is_empty() { return Ok(Self::new()); @@ -284,6 +298,15 @@ macro_rules! gen_metadata_struct { }) } } + + impl TryFrom for $name { + type Error = InvalidMetadata; + + fn try_from(value: DBusMetadata) -> Result { + let raw = RawMetadata::from(value); + Self::try_from(raw) + } + } }} gen_metadata_struct!( @@ -338,12 +361,8 @@ gen_metadata_struct!( /// /// [guide]: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ /// [object_path]: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path - #[derive(Debug, Clone, Default, PartialEq)] - #[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(into = "RawMetadata", try_from = "RawMetadata") - )] + #[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] + #[serde(into = "RawMetadata", try_from = "RawMetadata")] struct Metadata { /// The album artist(s). "xesam:albumArtist" => album_artists: Vec, diff --git a/src/metadata/track_id.rs b/src/metadata/track_id.rs index 7d14bdc..71f5a47 100644 --- a/src/metadata/track_id.rs +++ b/src/metadata/track_id.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value}; +use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Type, Value}; use super::MetadataValue; use crate::errors::InvalidTrackID; @@ -22,12 +22,9 @@ use crate::errors::InvalidTrackID; /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Simple-Type:Track_Id /// [object_path]: /// https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path -#[derive(Clone, PartialEq, Eq, Hash)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(into = "String", try_from = "String") -)] +#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Type)] +#[serde(into = "String", try_from = "String")] +#[zvariant(signature = "o")] pub struct TrackID(OwnedObjectPath); impl TrackID { @@ -199,6 +196,12 @@ impl TryFrom> for TrackID { } } +impl From for Value<'static> { + fn from(value: TrackID) -> Self { + Self::ObjectPath(value.0.into()) + } +} + impl From for TrackID { fn from(value: OwnedObjectPath) -> Self { Self(value) @@ -343,7 +346,7 @@ mod tests { } } -#[cfg(all(test, feature = "serde"))] +#[cfg(test)] mod serde_tests { use super::*; use serde_test::{assert_de_tokens, assert_ser_tokens, Token}; diff --git a/src/metadata/values.rs b/src/metadata/values.rs index b873d03..a1ec33b 100644 --- a/src/metadata/values.rs +++ b/src/metadata/values.rs @@ -2,7 +2,6 @@ use zbus::zvariant::{OwnedValue, Value}; use super::TrackID; use crate::errors::InvalidMetadataValue; -#[cfg(feature = "serde")] use crate::serde_util::deser_no_fail; /// Subset of [DBus data types][dbus_types] that are commonly used in MPRIS metadata. @@ -13,12 +12,8 @@ use crate::serde_util::deser_no_fail; /// /// [dbus_types]: https://dbus.freedesktop.org/doc/dbus-specification.html#type-system /// [meta_spec]: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ -#[derive(Debug, Clone, PartialEq, PartialOrd)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(untagged) -)] +#[derive(Debug, Clone, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] #[allow(missing_docs)] pub enum MetadataValue { Boolean(bool), @@ -28,7 +23,7 @@ pub enum MetadataValue { String(String), Strings(Vec), TrackID(TrackID), - #[cfg_attr(feature = "serde", serde(deserialize_with = "deser_no_fail"))] + #[serde(deserialize_with = "deser_no_fail")] Unsupported, } @@ -341,7 +336,7 @@ mod metadata_value_integer_tests { } } -#[cfg(all(test, feature = "serde"))] +#[cfg(test)] mod metadata_value_serde { use super::*; use serde_test::{assert_de_tokens, assert_ser_tokens, Token}; diff --git a/src/player.rs b/src/player.rs index c58aa9a..37aaf60 100644 --- a/src/player.rs +++ b/src/player.rs @@ -277,7 +277,7 @@ impl Player { /// [track_list]: /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:HasTrackList pub async fn has_track_list(&self) -> Result { - Ok(self.mp2_proxy.has_track_list().await?) + self.mp2_proxy.has_track_list().await } /// Checks if the [`Player`] has support for the @@ -308,7 +308,7 @@ impl Player { /// Similar to [`metadata()`][Self::metadata] but doesn't perform any checks or conversions. See /// [`Metadata::from_raw_lossy()`] for details. pub async fn raw_metadata(&self) -> Result { - Ok(self.player_proxy.metadata().await?.into()) + self.player_proxy.metadata().await } /// Checks if the player is still connected. @@ -321,12 +321,12 @@ impl Player { match self.mp2_proxy.ping().await { Ok(_) => Ok(true), Err(e) => match e { - zbus::Error::MethodError(e_name, _, _) + MprisError::DbusError(zbus::Error::MethodError(e_name, _, _)) if e_name == "org.freedesktop.DBus.Error.ServiceUnknown" => { Ok(false) } - _ => Err(e.into()), + _ => Err(e), }, } } @@ -355,12 +355,12 @@ impl Player { match self.dbus_proxy.get_name_owner(&self.bus_name).await { Ok(name) => Ok(Some(name.to_string())), Err(e) => match e { - zbus::Error::MethodError(e_name, _, _) + MprisError::DbusError(zbus::Error::MethodError(e_name, _, _)) if e_name == "org.freedesktop.DBus.Error.NameHasNoOwner" => { Ok(None) } - _ => Err(e.into()), + _ => Err(e), }, } } @@ -391,7 +391,7 @@ impl Player { /// [quit]: /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Method:Quit pub async fn quit(&self) -> Result<(), MprisError> { - Ok(self.mp2_proxy.quit().await?) + self.mp2_proxy.quit().await } /// Queries the player to see if it can be asked to quit. @@ -405,7 +405,7 @@ impl Player { /// [can_quit]: /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:CanQuit pub async fn can_quit(&self) -> Result { - Ok(self.mp2_proxy.can_quit().await?) + self.mp2_proxy.can_quit().await } /// Send a `Raise` signal to the player. @@ -422,7 +422,7 @@ impl Player { /// [raise]: /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Method:Raise pub async fn raise(&self) -> Result<(), MprisError> { - Ok(self.mp2_proxy.raise().await?) + self.mp2_proxy.raise().await } /// Queries the player to see if it can be raised or not. @@ -437,7 +437,7 @@ impl Player { /// [can_raise]: /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:CanRaise pub async fn can_raise(&self) -> Result { - Ok(self.mp2_proxy.can_raise().await?) + self.mp2_proxy.can_raise().await } /// Returns the player's [`DesktopEntry`][entry] property, if supported. /// @@ -450,7 +450,7 @@ impl Player { /// [entry]: /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:DesktopEntry pub async fn desktop_entry(&self) -> Result { - Ok(self.mp2_proxy.desktop_entry().await?) + self.mp2_proxy.desktop_entry().await } /// Returns the player's MPRIS [`Identity`][identity]. @@ -464,7 +464,7 @@ impl Player { /// [identity]: /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:Identity pub async fn identity(&self) -> Result { - Ok(self.mp2_proxy.identity().await?) + self.mp2_proxy.identity().await } /// Returns the player's [`SupportedMimeTypes`][mime] property. @@ -476,7 +476,7 @@ impl Player { /// [mime]: /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:SupportedMimeTypes pub async fn supported_mime_types(&self) -> Result, MprisError> { - Ok(self.mp2_proxy.supported_mime_types().await?) + self.mp2_proxy.supported_mime_types().await } /// Returns the player's [`SupportedUriSchemes`][uri] property. @@ -492,7 +492,7 @@ impl Player { /// [uri]: /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:SupportedUriSchemes pub async fn supported_uri_schemes(&self) -> Result, MprisError> { - Ok(self.mp2_proxy.supported_uri_schemes().await?) + self.mp2_proxy.supported_uri_schemes().await } /// Returns the player's [`Fullscreen`][full] property. @@ -513,7 +513,7 @@ impl Player { /// [full]: /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:Fullscreen pub async fn get_fullscreen(&self) -> Result { - Ok(self.mp2_proxy.fullscreen().await?) + self.mp2_proxy.fullscreen().await } /// Asks the player to set the [`Fullscreen`][full] property. @@ -527,7 +527,7 @@ impl Player { /// [full]: /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:Fullscreen pub async fn set_fullscreen(&self, value: bool) -> Result<(), MprisError> { - Ok(self.mp2_proxy.set_fullscreen(value).await?) + self.mp2_proxy.set_fullscreen(value).await } /// Queries the player to see if it can be asked to enter fullscreen. @@ -550,7 +550,7 @@ impl Player { /// [can_full]: /// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:CanSetFullscreen pub async fn can_set_fullscreen(&self) -> Result { - Ok(self.mp2_proxy.can_set_fullscreen().await?) + self.mp2_proxy.can_set_fullscreen().await } /// Queries the player to see if it can be controlled or not. @@ -570,7 +570,7 @@ impl Player { /// [control]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanControl pub async fn can_control(&self) -> Result { - Ok(self.player_proxy.can_control().await?) + self.player_proxy.can_control().await } /// Sends a [`Next`][next] signal to the player. @@ -589,7 +589,7 @@ impl Player { /// [next]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Next pub async fn next(&self) -> Result<(), MprisError> { - Ok(self.player_proxy.next().await?) + self.player_proxy.next().await } /// Queries the player to see if it can go to next. @@ -607,7 +607,7 @@ impl Player { /// [can_next]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanGoNext pub async fn can_go_next(&self) -> Result { - Ok(self.player_proxy.can_go_next().await?) + self.player_proxy.can_go_next().await } /// Sends a [`Previous`][prev] signal to the player. @@ -626,7 +626,7 @@ impl Player { /// [prev]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Previous pub async fn previous(&self) -> Result<(), MprisError> { - Ok(self.player_proxy.previous().await?) + self.player_proxy.previous().await } /// Queries the player to see if it can go to previous or not. @@ -644,7 +644,7 @@ impl Player { /// [can_prev]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanGoPrevious pub async fn can_go_previous(&self) -> Result { - Ok(self.player_proxy.can_go_previous().await?) + self.player_proxy.can_go_previous().await } /// Sends a [`Play`][play] signal to the player. @@ -664,7 +664,7 @@ impl Player { /// [play]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Play pub async fn play(&self) -> Result<(), MprisError> { - Ok(self.player_proxy.play().await?) + self.player_proxy.play().await } /// Queries the player to see if it can play. @@ -680,7 +680,7 @@ impl Player { /// [can_play]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanPlay pub async fn can_play(&self) -> Result { - Ok(self.player_proxy.can_play().await?) + self.player_proxy.can_play().await } /// Sends a [`Pause`][pause] signal to the player. @@ -698,7 +698,7 @@ impl Player { /// [pause]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Pause pub async fn pause(&self) -> Result<(), MprisError> { - Ok(self.player_proxy.pause().await?) + self.player_proxy.pause().await } /// Queries the player to see if it can pause. @@ -716,7 +716,7 @@ impl Player { /// [can_pause]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanPause pub async fn can_pause(&self) -> Result { - Ok(self.player_proxy.can_pause().await?) + self.player_proxy.can_pause().await } /// Sends a [`PlayPause`][play_pause] signal to the player. @@ -735,7 +735,7 @@ impl Player { /// [play_pause]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:PlayPause pub async fn play_pause(&self) -> Result<(), MprisError> { - Ok(self.player_proxy.play_pause().await?) + self.player_proxy.play_pause().await } /// Sends a [`Stop`][stop] signal to the player. @@ -753,7 +753,7 @@ impl Player { /// [stop]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Stop pub async fn stop(&self) -> Result<(), MprisError> { - Ok(self.player_proxy.stop().await?) + self.player_proxy.stop().await } /// Sends a [`Seek`][seek] signal to the player. @@ -774,7 +774,7 @@ impl Player { /// [seek]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Seek pub async fn seek(&self, offset_in_microseconds: i64) -> Result<(), MprisError> { - Ok(self.player_proxy.seek(offset_in_microseconds).await?) + self.player_proxy.seek(offset_in_microseconds).await } /// Tells the player to seek forwards. @@ -784,7 +784,7 @@ impl Player { /// /// See also: [`seek_backwards()`][Self::seek_backwards] pub async fn seek_forwards(&self, offset: MprisDuration) -> Result<(), MprisError> { - Ok(self.player_proxy.seek(offset.into()).await?) + self.player_proxy.seek(offset.into()).await } /// Tells the player to seek backwards. @@ -794,7 +794,7 @@ impl Player { /// /// See also: [`seek_forwards()`][Self::seek_forwards] pub async fn seek_backwards(&self, offset: MprisDuration) -> Result<(), MprisError> { - Ok(self.player_proxy.seek(-i64::from(offset)).await?) + self.player_proxy.seek(-i64::from(offset)).await } /// Queries the player to see if it can seek within the media. @@ -810,7 +810,7 @@ impl Player { /// [can_seek]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanSeek pub async fn can_seek(&self) -> Result { - Ok(self.player_proxy.can_seek().await?) + self.player_proxy.can_seek().await } /// Gets the player's MPRIS [`Position`][position] as a [`MprisDuration`] since the start of the @@ -822,7 +822,7 @@ impl Player { /// [position]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Position pub async fn get_position(&self) -> Result { - Ok(self.player_proxy.position().await?.try_into()?) + self.player_proxy.position().await } /// Sets the position of the current track to the given position (as a [`MprisDuration`]). @@ -862,10 +862,7 @@ impl Player { if track_id.is_no_track() { return Err(MprisError::track_id_is_no_track()); } - Ok(self - .player_proxy - .set_position(track_id.as_ref(), position.into()) - .await?) + self.player_proxy.set_position(track_id, position).await } /// Gets the player's current loop status. @@ -875,7 +872,7 @@ impl Player { /// [loop_status]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:LoopStatus pub async fn get_loop_status(&self) -> Result { - Ok(self.player_proxy.loop_status().await?.parse()?) + self.player_proxy.loop_status().await } /// Sets the loop status of the player. @@ -888,10 +885,7 @@ impl Player { /// [loop_status]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:LoopStatus pub async fn set_loop_status(&self, loop_status: LoopStatus) -> Result<(), MprisError> { - Ok(self - .player_proxy - .set_loop_status(loop_status.as_str()) - .await?) + self.player_proxy.set_loop_status(loop_status).await } /// Gets the player's current playback status. @@ -902,7 +896,7 @@ impl Player { /// [playback]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:PlaybackStatus pub async fn playback_status(&self) -> Result { - Ok(self.player_proxy.playback_status().await?.parse()?) + self.player_proxy.playback_status().await } /// Signals the player to open the given `uri`. @@ -930,7 +924,7 @@ impl Player { /// [uri]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:OpenUri pub async fn open_uri(&self, uri: &str) -> Result<(), MprisError> { - Ok(self.player_proxy.open_uri(uri).await?) + self.player_proxy.open_uri(uri).await } /// Gets the minimum allowed value for playback rate. @@ -949,7 +943,7 @@ impl Player { /// [min_rate]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:MinimumRate pub async fn maximum_rate(&self) -> Result { - Ok(self.player_proxy.maximum_rate().await?) + self.player_proxy.maximum_rate().await } /// Gets the maximum allowed value for playback rate. @@ -965,7 +959,7 @@ impl Player { /// [max_rate]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:MaximumRate pub async fn minimum_rate(&self) -> Result { - Ok(self.player_proxy.minimum_rate().await?) + self.player_proxy.minimum_rate().await } /// Returns the player's MPRIS (playback) [`rate`][rate] as a factor. @@ -977,7 +971,7 @@ impl Player { /// [rate]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Rate pub async fn get_playback_rate(&self) -> Result { - Ok(self.player_proxy.rate().await?) + self.player_proxy.rate().await } /// Sets the player's MPRIS (playback) [`rate`][rate] as a factor. @@ -1000,7 +994,7 @@ impl Player { if rate == 0.0 { return Err(MprisError::InvalidArgument("rate can't be 0.0".to_string())); } - Ok(self.player_proxy.set_rate(rate).await?) + self.player_proxy.set_rate(rate).await } /// Gets the player's [`Shuffle`][shuffle] property. @@ -1013,7 +1007,7 @@ impl Player { /// [shuffle]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Shuffle pub async fn get_shuffle(&self) -> Result { - Ok(self.player_proxy.shuffle().await?) + self.player_proxy.shuffle().await } /// Sets the [`Shuffle`][shuffle] property of the player. @@ -1026,7 +1020,7 @@ impl Player { /// [shuffle]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Shuffle pub async fn set_shuffle(&self, shuffle: bool) -> Result<(), MprisError> { - Ok(self.player_proxy.set_shuffle(shuffle).await?) + self.player_proxy.set_shuffle(shuffle).await } /// Gets the [`Volume`][vol] of the player. @@ -1036,7 +1030,7 @@ impl Player { /// [vol]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Volume pub async fn get_volume(&self) -> Result { - Ok(self.player_proxy.volume().await?) + self.player_proxy.volume().await } /// Sets the [`Volume`][vol] of the player. @@ -1049,7 +1043,7 @@ impl Player { /// [vol]: /// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Volume pub async fn set_volume(&self, volume: f64) -> Result<(), MprisError> { - Ok(self.player_proxy.set_volume(volume).await?) + self.player_proxy.set_volume(volume).await } /// Shortcut to check if `self.playlist_proxy` is Some @@ -1185,7 +1179,7 @@ impl Player { /// [can_edit]: /// https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Property:CanEditTracks pub async fn can_edit_tracks(&self) -> Result { - Ok(self.check_track_list_support()?.can_edit_tracks().await?) + self.check_track_list_support()?.can_edit_tracks().await } /// Gets the tracks in the current `TrackList` @@ -1197,12 +1191,7 @@ impl Player { /// [tracks]: /// https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Property:Tracks pub async fn tracks(&self) -> Result, MprisError> { - let result = self.check_track_list_support()?.tracks().await?; - let mut track_ids = Vec::with_capacity(result.len()); - for r in result { - track_ids.push(TrackID::from(r)); - } - Ok(track_ids) + self.check_track_list_support()?.tracks().await } /// Adds a `uri` to the `TrackList` and optionally set it as current. @@ -1237,10 +1226,9 @@ impl Player { } else { &no_track }; - Ok(self - .check_track_list_support()? - .add_track(uri, after.as_ref(), set_as_current) - .await?) + self.check_track_list_support()? + .add_track(uri, after, set_as_current) + .await } /// Removes an item from the TrackList. @@ -1263,10 +1251,9 @@ impl Player { if track_id.is_no_track() { return Err(MprisError::track_id_is_no_track()); } - Ok(self - .check_track_list_support()? - .remove_track(track_id.as_ref()) - .await?) + self.check_track_list_support()? + .remove_track(track_id) + .await } /// Go to a specific track on the [`Player`]'s `TrackList`. @@ -1282,10 +1269,7 @@ impl Player { if track_id.is_no_track() { return Err(MprisError::track_id_is_no_track()); } - Ok(self - .check_track_list_support()? - .go_to(track_id.as_ref()) - .await?) + self.check_track_list_support()?.go_to(track_id).await } /// Gets the [`Metadata`] for the given [`TrackID`]s. @@ -1302,12 +1286,12 @@ impl Player { ) -> Result, MprisError> { let result = self .check_track_list_support()? - .get_tracks_metadata(&tracks.iter().map(|x| x.as_ref()).collect::>()) + .get_tracks_metadata(tracks) .await?; let mut metadata = Vec::with_capacity(tracks.len()); for meta in result { - metadata.push(Metadata::try_from(RawMetadata::from(meta))?); + metadata.push(Metadata::try_from(meta)?); } Ok(metadata) } diff --git a/src/playlist.rs b/src/playlist.rs index 297c438..a58345c 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -4,10 +4,13 @@ use std::{ }; use futures_util::StreamExt; -use zbus::{names::BusName, zvariant::OwnedObjectPath, Connection, Task}; +use zbus::{ + names::BusName, + zvariant::{OwnedObjectPath, OwnedValue, Structure, Type, Value}, + Connection, Task, +}; use crate::proxies::PlaylistsProxy; -#[cfg(feature = "serde")] use crate::serde_util::{option_string, serialize_owned_object_path}; use crate::{InvalidPlaylist, InvalidPlaylistOrdering, MprisError}; @@ -35,17 +38,14 @@ type InnerPlaylistData = HashMap)>; /// https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html#Struct:Playlist /// [object_path]: /// https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path -#[derive(Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Type)] +#[zvariant(signature = "(oss)")] pub struct Playlist { - #[cfg_attr( - feature = "serde", - serde(serialize_with = "serialize_owned_object_path") - )] + #[serde(serialize_with = "serialize_owned_object_path")] /// Unique playlist identifier id: OwnedObjectPath, name: String, - #[cfg_attr(feature = "serde", serde(default, with = "option_string"))] + #[serde(default, with = "option_string")] icon: Option, } @@ -96,12 +96,12 @@ impl Playlist { } /// Gets the `id` as a borrowed [`ObjectPath`] - pub fn get_id(&self) -> &OwnedObjectPath { + pub(crate) fn get_path(&self) -> &OwnedObjectPath { &self.id } /// Gets the `id` as a &[`str`] - pub fn get_id_as_str(&self) -> &str { + pub fn get_id(&self) -> &str { self.id.as_str() } } @@ -134,7 +134,7 @@ impl PlaylistsInterface { while let Some(change) = stream.next().await { // TODO: don't ignore errors somehow without panicking if let Ok(args) = change.args() { - let playlist = Playlist::from(args.playlist); + let playlist = args.playlist; i_clone .get_lock() .insert(playlist.id, (playlist.name, playlist.icon)); @@ -175,7 +175,7 @@ impl PlaylistsInterface { } pub(crate) async fn activate_playlist(&self, playlist: &Playlist) -> Result<(), MprisError> { - Ok(self.proxy.activate_playlist(playlist.get_id()).await?) + self.proxy.activate_playlist(playlist.get_path()).await } /// Wraps the proxy method of the same name and updates the internal data. @@ -188,11 +188,8 @@ impl PlaylistsInterface { ) -> Result, MprisError> { let playlists: Vec<_> = self .proxy - .get_playlists(start_index, max_count, order.as_str(), reverse_order) - .await? - .into_iter() - .map(Playlist::from) - .collect(); + .get_playlists(start_index, max_count, order.as_str_value(), reverse_order) + .await?; self.inner.update_playlists(&playlists); Ok(playlists) } @@ -200,27 +197,22 @@ impl PlaylistsInterface { /// Wraps the proxy method of the same name and updates the internal data. pub(crate) async fn active_playlist(&self) -> Result, MprisError> { Ok(match self.proxy.active_playlist().await? { - (true, data) => { + Some(playlist) => { // Better to create a temporary Vec here than to lock the Mutex for each playlist - let mut playlist = vec![Playlist::from(data)]; + let mut playlist = vec![playlist]; self.inner.update_playlists(&playlist); Some(playlist.pop().expect("there should be at least 1 playlist")) } - (false, _) => None, + None => None, }) } pub(crate) async fn orderings(&self) -> Result, MprisError> { - let result = self.proxy.orderings().await?; - let mut orderings = Vec::with_capacity(result.len()); - for s in result { - orderings.push(s.parse()?); - } - Ok(orderings) + self.proxy.orderings().await } pub(crate) async fn playlist_count(&self) -> Result { - Ok(self.proxy.playlist_count().await?) + self.proxy.playlist_count().await } } @@ -249,7 +241,7 @@ impl PlaylistInner { impl std::fmt::Debug for Playlist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Playlist") - .field("id", &self.get_id_as_str()) + .field("id", &self.get_id()) .field("name", &self.name) .field("icon", &self.icon) .finish() @@ -271,6 +263,38 @@ impl From<(OwnedObjectPath, String, String)> for Playlist { } } +impl TryFrom> for Playlist { + type Error = InvalidPlaylist; + + fn try_from(value: Value<'_>) -> Result { + match value { + Value::Structure(structure) => Self::try_from(structure), + _ => Err(InvalidPlaylist::expected("Value::Structure")), + } + } +} + +impl TryFrom for Playlist { + type Error = InvalidPlaylist; + + fn try_from(value: OwnedValue) -> Result { + Self::try_from(Value::from(value)) + } +} + +impl TryFrom> for Playlist { + type Error = InvalidPlaylist; + + fn try_from(value: Structure<'_>) -> Result { + if value.full_signature() == "(oss)" { + if let Ok((id, name, icon)) = <(OwnedObjectPath, String, String)>::try_from(value) { + return Ok(Playlist::from((id, name, icon))); + } + } + Err(InvalidPlaylist::from("incorrect signature")) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] /// Specifies the ordering of returned playlists. pub enum PlaylistOrdering { @@ -296,7 +320,7 @@ impl PlaylistOrdering { /// Returns the string value that's used on the D-Bus. /// /// See [`as_str()`][Self::as_str()] if you want the name of the enum variant. - pub fn as_str_value(&self) -> &str { + pub fn as_str_value(&self) -> &'static str { match self { PlaylistOrdering::Alphabetical => "Alphabetical", PlaylistOrdering::CreationDate => "Created", @@ -343,8 +367,29 @@ impl std::str::FromStr for PlaylistOrdering { } } +impl TryFrom> for PlaylistOrdering { + type Error = InvalidPlaylistOrdering; + + fn try_from(value: Value<'_>) -> Result { + match value { + Value::Str(s) => s.parse(), + _ => Err(InvalidPlaylistOrdering::expected("Value::Str")), + } + } +} + +impl TryFrom for PlaylistOrdering { + type Error = InvalidPlaylistOrdering; + + fn try_from(value: OwnedValue) -> Result { + Self::try_from(Value::from(value)) + } +} + #[cfg(test)] mod playlist_ordering_tests { + use zbus::zvariant::{Dict, ObjectPath, Signature, Structure}; + use super::*; #[test] @@ -383,12 +428,56 @@ mod playlist_ordering_tests { assert_eq!(&PlaylistOrdering::LastPlayDate.to_string(), "LastPlayDate"); assert_eq!(&PlaylistOrdering::UserDefined.to_string(), "UserDefined"); } + + #[test] + fn from_value() { + let s_signature = Signature::try_from("s").unwrap(); + + assert!(PlaylistOrdering::try_from(Value::U8(0)).is_err()); + assert!(PlaylistOrdering::try_from(Value::Bool(false)).is_err()); + assert!(PlaylistOrdering::try_from(Value::I16(0)).is_err()); + assert!(PlaylistOrdering::try_from(Value::U16(0)).is_err()); + assert!(PlaylistOrdering::try_from(Value::I32(0)).is_err()); + assert!(PlaylistOrdering::try_from(Value::U32(0)).is_err()); + assert!(PlaylistOrdering::try_from(Value::I64(0)).is_err()); + assert!(PlaylistOrdering::try_from(Value::U64(0)).is_err()); + assert!(PlaylistOrdering::try_from(Value::F64(0.0)).is_err()); + + assert_eq!( + PlaylistOrdering::try_from(Value::Str("Alphabetical".into())), + Ok(PlaylistOrdering::Alphabetical) + ); + assert_eq!( + PlaylistOrdering::try_from(Value::Str("Created".into())), + Ok(PlaylistOrdering::CreationDate) + ); + assert_eq!( + PlaylistOrdering::try_from(Value::Str("Played".into())), + Ok(PlaylistOrdering::LastPlayDate) + ); + assert_eq!( + PlaylistOrdering::try_from(Value::Str("User".into())), + Ok(PlaylistOrdering::UserDefined) + ); + + assert!(PlaylistOrdering::try_from(Value::Str("Wrong".into())).is_err()); + assert!(PlaylistOrdering::try_from(Value::Signature(s_signature.clone())).is_err()); + assert!(PlaylistOrdering::try_from(Value::ObjectPath(ObjectPath::default())).is_err()); + assert!(PlaylistOrdering::try_from(Value::Value(Box::new(Value::Bool(false)))).is_err()); + assert!(PlaylistOrdering::try_from(Value::Array(vec![0].try_into().unwrap())).is_err()); + assert!(PlaylistOrdering::try_from(Value::Dict(Dict::new( + s_signature.clone(), + s_signature.clone() + ))) + .is_err()); + assert!(PlaylistOrdering::try_from(Value::Structure(Structure::default())).is_err()); + } } #[cfg(test)] mod playlist_tests { use super::*; - use zbus::zvariant::ObjectPath; + use zbus::zvariant::{Dict, ObjectPath, Signature}; #[test] fn new() { @@ -415,17 +504,64 @@ mod playlist_tests { assert_eq!(new.get_name(), "TestName"); assert_eq!(new.get_icon(), Some("TestIcon")); assert_eq!( - new.get_id().as_ref(), + new.get_path().as_ref(), ObjectPath::from_str_unchecked("/valid/path") ); - assert_eq!(new.get_id_as_str(), "/valid/path"); + assert_eq!(new.get_id(), "/valid/path"); new.icon = None; assert_eq!(new.get_icon(), None); } + + #[test] + fn from_value_fail() { + let s_signature = Signature::try_from("s").unwrap(); + + assert!(Playlist::try_from(Value::U8(0)).is_err()); + assert!(Playlist::try_from(Value::Bool(false)).is_err()); + assert!(Playlist::try_from(Value::I16(0)).is_err()); + assert!(Playlist::try_from(Value::U16(0)).is_err()); + assert!(Playlist::try_from(Value::I32(0)).is_err()); + assert!(Playlist::try_from(Value::U32(0)).is_err()); + assert!(Playlist::try_from(Value::I64(0)).is_err()); + assert!(Playlist::try_from(Value::U64(0)).is_err()); + assert!(Playlist::try_from(Value::F64(0.0)).is_err()); + assert!(Playlist::try_from(Value::Str("".into())).is_err()); + assert!(Playlist::try_from(Value::Signature(s_signature.clone())).is_err()); + assert!(Playlist::try_from(Value::ObjectPath(ObjectPath::default())).is_err()); + assert!(Playlist::try_from(Value::Value(Box::new(Value::Bool(false)))).is_err()); + assert!(Playlist::try_from(Value::Array(vec![0].try_into().unwrap())).is_err()); + assert!(Playlist::try_from(Value::Dict(Dict::new( + s_signature.clone(), + s_signature.clone() + ))) + .is_err()); + } + + #[test] + fn from_value_structure() { + assert!(Playlist::try_from(Value::Structure(Structure::default())).is_err()); + + let valid_structure = + Structure::from((ObjectPath::from_str_unchecked("/valid"), "Name", "")); + assert_eq!( + Playlist::try_from(valid_structure), + Ok(Playlist { + id: ObjectPath::from_str_unchecked("/valid").into(), + name: String::from("Name"), + icon: None + }) + ); + + let wrong_signature = Structure::from((ObjectPath::from_str_unchecked("/valid"), 0, "")); + assert!(Playlist::try_from(wrong_signature).is_err()); + + let too_long = Structure::from((ObjectPath::from_str_unchecked("/valid"), "", "", "")); + assert!(Playlist::try_from(too_long).is_err()); + } } -#[cfg(all(test, feature = "serde"))] +#[cfg(test)] mod playlist_serde_tests { use super::*; use serde_test::{assert_de_tokens, assert_de_tokens_error, assert_tokens, Token}; diff --git a/src/proxies.rs b/src/proxies.rs index cc026ad..91c7a3e 100644 --- a/src/proxies.rs +++ b/src/proxies.rs @@ -4,6 +4,9 @@ use zbus::names::{BusName, OwnedUniqueName}; use zbus::proxy; use zbus::zvariant::{OwnedObjectPath, OwnedValue}; +use crate::{metadata::RawMetadata, MprisDuration, MprisError, TrackID}; +use crate::{LoopStatus, PlaybackStatus, Playlist, PlaylistOrdering}; + #[proxy( default_service = "org.freedesktop.DBus", interface = "org.freedesktop.DBus", @@ -11,9 +14,9 @@ use zbus::zvariant::{OwnedObjectPath, OwnedValue}; gen_blocking = false )] pub(crate) trait DBus { - fn list_names(&self) -> zbus::Result>; + fn list_names(&self) -> Result, MprisError>; - fn get_name_owner(&self, bus_name: &BusName<'_>) -> zbus::Result; + fn get_name_owner(&self, bus_name: &BusName<'_>) -> Result; } #[proxy( @@ -23,51 +26,51 @@ pub(crate) trait DBus { )] pub(crate) trait MediaPlayer2 { /// Quit method - fn quit(&self) -> zbus::Result<()>; + fn quit(&self) -> Result<(), MprisError>; /// Raise method - fn raise(&self) -> zbus::Result<()>; + fn raise(&self) -> Result<(), MprisError>; /// CanQuit property #[zbus(property)] - fn can_quit(&self) -> zbus::Result; + fn can_quit(&self) -> Result; /// CanRaise property #[zbus(property)] - fn can_raise(&self) -> zbus::Result; + fn can_raise(&self) -> Result; /// DesktopEntry property #[zbus(property)] - fn desktop_entry(&self) -> zbus::Result; + fn desktop_entry(&self) -> Result; /// HasTrackList property #[zbus(property)] - fn has_track_list(&self) -> zbus::Result; + fn has_track_list(&self) -> Result; /// Identity property #[zbus(property)] - fn identity(&self) -> zbus::Result; + fn identity(&self) -> Result; /// SupportedMimeTypes property #[zbus(property)] - fn supported_mime_types(&self) -> zbus::Result>; + fn supported_mime_types(&self) -> Result, MprisError>; /// SupportedUriSchemes property #[zbus(property)] - fn supported_uri_schemes(&self) -> zbus::Result>; + fn supported_uri_schemes(&self) -> Result, MprisError>; #[zbus(property)] - fn fullscreen(&self) -> zbus::Result; + fn fullscreen(&self) -> Result; #[zbus(property)] - fn set_fullscreen(&self, value: bool) -> zbus::Result<()>; + fn set_fullscreen(&self, value: bool) -> Result<(), MprisError>; #[zbus(property)] - fn can_set_fullscreen(&self) -> zbus::Result; + fn can_set_fullscreen(&self) -> Result; } impl MediaPlayer2Proxy<'_> { - pub(crate) async fn ping(&self) -> zbus::Result<()> { + pub(crate) async fn ping(&self) -> Result<(), MprisError> { self.inner() .connection() .call_method( @@ -78,7 +81,8 @@ impl MediaPlayer2Proxy<'_> { &(), ) .await - .and(Ok(())) + .map(|_| ()) + .map_err(MprisError::from) } } @@ -89,107 +93,107 @@ impl MediaPlayer2Proxy<'_> { )] pub(crate) trait Player { /// Next method - fn next(&self) -> zbus::Result<()>; + fn next(&self) -> Result<(), MprisError>; /// OpenUri method - fn open_uri(&self, uri: &str) -> zbus::Result<()>; + fn open_uri(&self, uri: &str) -> Result<(), MprisError>; /// Pause method - fn pause(&self) -> zbus::Result<()>; + fn pause(&self) -> Result<(), MprisError>; /// Play method - fn play(&self) -> zbus::Result<()>; + fn play(&self) -> Result<(), MprisError>; /// PlayPause method - fn play_pause(&self) -> zbus::Result<()>; + fn play_pause(&self) -> Result<(), MprisError>; /// Previous method - fn previous(&self) -> zbus::Result<()>; + fn previous(&self) -> Result<(), MprisError>; /// Seek method - fn seek(&self, offset: i64) -> zbus::Result<()>; + fn seek(&self, offset: i64) -> Result<(), MprisError>; /// SetPosition method - fn set_position(&self, track_id: &OwnedObjectPath, position: i64) -> zbus::Result<()>; + fn set_position(&self, track_id: &TrackID, position: MprisDuration) -> Result<(), MprisError>; /// Stop method - fn stop(&self) -> zbus::Result<()>; + fn stop(&self) -> Result<(), MprisError>; /// Seeked signal #[zbus(signal)] - fn seeked(&self, position: i64) -> zbus::Result<()>; + fn seeked(&self, position: MprisDuration) -> Result<(), MprisError>; /// CanControl property #[zbus(property(emits_changed_signal = "const"))] - fn can_control(&self) -> zbus::Result; + fn can_control(&self) -> Result; /// CanGoNext property #[zbus(property)] - fn can_go_next(&self) -> zbus::Result; + fn can_go_next(&self) -> Result; /// CanGoPrevious property #[zbus(property)] - fn can_go_previous(&self) -> zbus::Result; + fn can_go_previous(&self) -> Result; /// CanPause property #[zbus(property)] - fn can_pause(&self) -> zbus::Result; + fn can_pause(&self) -> Result; /// CanPlay property #[zbus(property)] - fn can_play(&self) -> zbus::Result; + fn can_play(&self) -> Result; /// CanSeek property #[zbus(property)] - fn can_seek(&self) -> zbus::Result; + fn can_seek(&self) -> Result; /// LoopStatus property #[zbus(property)] - fn loop_status(&self) -> zbus::Result; + fn loop_status(&self) -> Result; #[zbus(property)] - fn set_loop_status(&self, value: &str) -> zbus::Result<()>; + fn set_loop_status(&self, value: LoopStatus) -> Result<(), MprisError>; /// MaximumRate property #[zbus(property)] - fn maximum_rate(&self) -> zbus::Result; + fn maximum_rate(&self) -> Result; /// Metadata property #[zbus(property)] - fn metadata(&self) -> zbus::Result>; + fn metadata(&self) -> Result; /// MinimumRate property #[zbus(property)] - fn minimum_rate(&self) -> zbus::Result; + fn minimum_rate(&self) -> Result; /// PlaybackStatus property #[zbus(property)] - fn playback_status(&self) -> zbus::Result; + fn playback_status(&self) -> Result; /// Position property #[zbus(property(emits_changed_signal = "const"))] - fn position(&self) -> zbus::Result; + fn position(&self) -> Result; /// Rate property #[zbus(property)] - fn rate(&self) -> zbus::Result; + fn rate(&self) -> Result; #[zbus(property)] - fn set_rate(&self, value: f64) -> zbus::Result<()>; + fn set_rate(&self, value: f64) -> Result<(), MprisError>; /// Shuffle property #[zbus(property)] - fn shuffle(&self) -> zbus::Result; + fn shuffle(&self) -> Result; #[zbus(property)] - fn set_shuffle(&self, value: bool) -> zbus::Result<()>; + fn set_shuffle(&self, value: bool) -> Result<(), MprisError>; /// Volume property #[zbus(property)] - fn volume(&self) -> zbus::Result; + fn volume(&self) -> Result; #[zbus(property)] - fn set_volume(&self, value: f64) -> zbus::Result<()>; + fn set_volume(&self, value: f64) -> Result<(), MprisError>; } #[proxy( @@ -199,7 +203,7 @@ pub(crate) trait Player { )] pub(crate) trait Playlists { /// ActivatePlaylist method - fn activate_playlist(&self, playlist_id: &OwnedObjectPath) -> zbus::Result<()>; + fn activate_playlist(&self, playlist_id: &OwnedObjectPath) -> Result<(), MprisError>; /// GetPlaylists method fn get_playlists( @@ -208,22 +212,33 @@ pub(crate) trait Playlists { max_count: u32, order: &str, reverse_order: bool, - ) -> zbus::Result>; + ) -> Result, MprisError>; #[zbus(signal)] - fn playlist_changed(&self, playlist: (OwnedObjectPath, String, String)) -> zbus::Result<()>; + fn playlist_changed(&self, playlist: Playlist) -> Result<(), MprisError>; /// ActivePlaylist property - #[zbus(property)] - fn active_playlist(&self) -> zbus::Result<(bool, (OwnedObjectPath, String, String))>; + #[zbus(property, name = "ActivePlaylist")] + fn _active_playlist_inner( + &self, + ) -> Result<(bool, (OwnedObjectPath, String, String)), MprisError>; /// Orderings property #[zbus(property)] - fn orderings(&self) -> zbus::Result>; + fn orderings(&self) -> Result, MprisError>; /// PlaylistCount property #[zbus(property)] - fn playlist_count(&self) -> zbus::Result; + fn playlist_count(&self) -> Result; +} + +impl PlaylistsProxy<'_> { + pub(crate) async fn active_playlist(&self) -> Result, MprisError> { + Ok(match self._active_playlist_inner().await? { + (true, data) => Some(Playlist::from(data)), + _ => None, + }) + } } #[proxy( @@ -236,55 +251,68 @@ pub trait TrackList { fn add_track( &self, uri: &str, - after_track: &OwnedObjectPath, + after_track: &TrackID, set_as_current: bool, - ) -> zbus::Result<()>; + ) -> Result<(), MprisError>; + // There is no way to implement zvariant::Type for MetadataValue while keeping the current serde + // implementation. If you try to do it then returning RawMetadata here will cause a infinite + // recursion that ends with a stack overflow. To avoid that the function just gets wrapped /// GetTracksMetadata method - fn get_tracks_metadata( + fn _get_tracks_metadata( &self, - track_ids: &[&OwnedObjectPath], - ) -> zbus::Result>>; + track_ids: &[TrackID], + ) -> Result>, MprisError>; /// GoTo method - fn go_to(&self, track_id: &OwnedObjectPath) -> zbus::Result<()>; + fn go_to(&self, track_id: &TrackID) -> Result<(), MprisError>; /// RemoveTrack method - fn remove_track(&self, track_id: &OwnedObjectPath) -> zbus::Result<()>; + fn remove_track(&self, track_id: &TrackID) -> Result<(), MprisError>; /// TrackAdded signal #[zbus(signal)] - fn track_added( - &self, - metadata: HashMap, - after_track: OwnedObjectPath, - ) -> zbus::Result<()>; + fn track_added(&self, metadata: RawMetadata, after_track: TrackID) -> Result<(), MprisError>; /// TrackListReplaced signal #[zbus(signal)] fn track_list_replaced( &self, - track_ids: Vec, - current_track: OwnedObjectPath, - ) -> zbus::Result<()>; + track_ids: Vec, + current_track: TrackID, + ) -> Result<(), MprisError>; /// TrackMetadataChanged signal #[zbus(signal)] fn track_metadata_changed( &self, - track_id: OwnedObjectPath, - metadata: HashMap, - ) -> zbus::Result<()>; + track_id: TrackID, + metadata: RawMetadata, + ) -> Result<(), MprisError>; /// TrackRemoved signal #[zbus(signal)] - fn track_removed(&self, track_id: OwnedObjectPath) -> zbus::Result<()>; + fn track_removed(&self, track_id: TrackID) -> Result<(), MprisError>; /// CanEditTracks property #[zbus(property)] - fn can_edit_tracks(&self) -> zbus::Result; + fn can_edit_tracks(&self) -> Result; /// Tracks property #[zbus(property(emits_changed_signal = "invalidates"))] - fn tracks(&self) -> zbus::Result>; + fn tracks(&self) -> Result, MprisError>; +} + +impl TrackListProxy<'_> { + pub(crate) async fn get_tracks_metadata( + &self, + tracks: &[TrackID], + ) -> Result, MprisError> { + Ok(self + ._get_tracks_metadata(tracks) + .await? + .into_iter() + .map(RawMetadata::from) + .collect()) + } }