diff --git a/Cargo.lock b/Cargo.lock index d6c0b27..9eedaaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "async-broadcast" version = "0.7.1" @@ -204,6 +219,7 @@ version = "0.1.0" dependencies = [ "async-channel", "bindgen", + "chrono", "gettext-rs", "glib-build-tools", "gtk4", @@ -397,6 +413,21 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "cipher" version = "0.4.4" @@ -1180,6 +1211,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -2577,6 +2631,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-registry" version = "0.2.0" @@ -2889,6 +2952,7 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c690a1da8858fd4377b8cc3134a753b0bea1d8ebd78ad6e5897fab821c5e184e" dependencies = [ + "chrono", "endi", "enumflags2", "serde", diff --git a/Cargo.toml b/Cargo.toml index 881b26b..219a1a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] adw = { version = "0.7.0", package = "libadwaita", features = ["v1_6"] } async-channel = "2.3.1" +chrono = { version = "0.4.38", features = ["serde"] } gettext-rs = { version = "0.7.2", features = ["gettext-system"] } gtk = { version = "0.9.2", package = "gtk4", features = ["v4_16"] } oo7 = "0.3.3" @@ -14,7 +15,7 @@ reqwest = { version = "0.12.9", features = ["json"] } serde = { version = "1.0.214", features = ["derive"] } tokio = { version = "1", features = ["rt-multi-thread"] } url = "2.5.2" -zbus = "5.0.1" +zbus = { version = "5.0.1", features = ["chrono"] } [build-dependencies] bindgen = "0.70.1" diff --git a/src/mpris/player.rs b/src/mpris/player.rs index 1a18b87..50dfb02 100644 --- a/src/mpris/player.rs +++ b/src/mpris/player.rs @@ -8,30 +8,88 @@ const MICROSECONDS: f64 = 1e6; // in a second #[derive(Default)] struct MetadataMap { - pub track_id: Option, - // rest: TODO + // mpris + track_id: Option, + length: Option, + //art_url: Option, + // xesam + album: Option, + //album_artist: Option>, + artist: Option>, + //as_text: Option, + //audio_bpm: Option, + //auto_rating: Option, + //comment: Option>, + //composer: Option>, + content_created: Option, + //disc_number: Option, + //first_used: Option, + genre: Option>, + //last_used: Option, + //lyricist: Option>, + title: Option, + track_number: Option, + //url: Option, + //use_count: Option, + user_rating: Option, } impl MetadataMap { fn from_playbin_song(song: Option<&crate::playbin::Song>) -> Self { song.map(|song| MetadataMap { - // FIXME: see if there's a better way to do this lol + // use a unique growing counter to identify tracks track_id: Some({ - let song_object_intptr = - glib::translate::ToGlibPtr::<*const _>::to_glib_none(song).0 as usize; - format!("/eu/callcc/audrey/TrackId/{song_object_intptr:x}") + format!("/eu/callcc/audrey/Track/{}", song.counter()) .try_into() .unwrap() }), + //length: Some((song.duration() * MICROSECONDS) as i64), + album: Some(song.album().into()), + artist: Some(vec![song.artist().into()]), + //content_created: song.year().map(|year| chrono::NaiveDate::from_yo_opt(year, 1).unwrap()), // FIXME: replace this unwrap with Some(Err) -> None + //genre: Some(song.genre.iter().collect()), + title: Some(song.title().into()), + //track_number: song.track().map(|u| u as i32), + //user_rating: Some(if song.starred().is_none() { 0.0 } else { 1.0 }), + ..Default::default() }) .unwrap_or_else(Default::default) } fn as_hash_map(&self) -> HashMap<&'static str, Value> { let mut map = HashMap::new(); + if let Some(track_id) = &self.track_id { map.insert("mpris:trackid", Value::new(track_id.as_ref())); } + if let Some(length) = &self.length { + map.insert("mpris:length", Value::new(length)); + } + if let Some(album) = &self.album { + map.insert("xesam:album", Value::new(album)); + } + if let Some(artist) = &self.artist { + map.insert("xesam:artist", Value::new(artist)); + } + if let Some(content_created) = &self.content_created { + map.insert( + "xesam:contentCreated", + Value::new(content_created.format("%+").to_string()), + ); + } + if let Some(genre) = &self.genre { + map.insert("xesam:genre", Value::new(genre)); + } + if let Some(track_number) = self.track_number { + map.insert("xesam:trackNumber", Value::new(track_number)); + } + if let Some(title) = &self.title { + map.insert("xesam:title", Value::new(title)); + } + if let Some(user_rating) = self.user_rating { + map.insert("xesam:userRating", Value::new(user_rating)); + } + map } } @@ -83,6 +141,33 @@ impl Player { ), ); + playbin.connect_notify_local( + Some("play-queue-length"), + glib::clone!( + #[strong] + player_ref, + move |_playbin: &crate::Playbin, _| { + let player_ref = player_ref.clone(); + glib::spawn_future_local(async move { + let player = player_ref.get_mut().await; + // properties that depend on the play queue length + player + .can_go_next_changed(player_ref.signal_emitter()) + .await + .unwrap(); + player + .can_go_previous_changed(player_ref.signal_emitter()) + .await + .unwrap(); + player + .can_play_changed(player_ref.signal_emitter()) + .await + .unwrap(); + }); + } + ), + ); + playbin.connect_notify_local( Some("state"), glib::clone!( @@ -91,7 +176,9 @@ impl Player { move |_playbin: &crate::Playbin, _| { let player_ref = player_ref.clone(); glib::spawn_future_local(async move { - player_ref.get_mut().await + let player = player_ref.get_mut().await; + // properties that depend on the playbin state + player .playback_status_changed(player_ref.signal_emitter()) .await .unwrap(); @@ -269,13 +356,18 @@ impl Player { } #[zbus(property)] - fn playback_rate(&self) -> f64 { + fn rate(&self) -> f64 { 1.0 } #[zbus(property)] - fn set_playback_rate(&self, _playback_rate: f64) -> zbus::Result<()> { - Err(zbus::Error::Unsupported) + fn set_rate(&self, rate: f64) { + // A value of 0.0 should not be set by the client. If it is, the media player should act as though Pause was called. + if rate == 0.0 { + self.pause(); + } + + // just ignore anything else } #[zbus(property)] @@ -299,7 +391,11 @@ impl Player { } #[zbus(property)] - fn set_volume(&mut self, volume: f64) { + fn set_volume(&mut self, mut volume: f64) { + // When setting, if a negative value is passed, the volume should be set to 0.0. + if volume < 0.0 { + volume = 0.0; + } let playbin = self.playbin.upgrade().unwrap(); // FIXME: check if this is set by the notify callback: self.volume = volume; playbin.set_volume((volume * 100.0) as i32); @@ -322,26 +418,31 @@ impl Player { #[zbus(property)] fn can_go_next(&self) -> bool { - true + // same as can_play + self.playbin.upgrade().unwrap().play_queue_length() > 0 } #[zbus(property)] fn can_go_previous(&self) -> bool { - true + // same as can_play + self.playbin.upgrade().unwrap().play_queue_length() > 0 } #[zbus(property)] fn can_play(&self) -> bool { - true + // it only makes sense to disallow "play" when the play queue is empty + self.playbin.upgrade().unwrap().play_queue_length() > 0 } #[zbus(property)] fn can_pause(&self) -> bool { + // we don't play anything that can't be paused true } #[zbus(property)] fn can_seek(&self) -> bool { + // we don't play anything that can't be seeked true } diff --git a/src/playbin.vala b/src/playbin.vala index d15fd67..74d1eee 100644 --- a/src/playbin.vala +++ b/src/playbin.vala @@ -10,6 +10,9 @@ private struct Audrey.CommandCallback { } public class Audrey.PlaybinSong : Object { + private static int64 next_counter = 0; + public int64 counter { get; private set; } + private Subsonic.Song inner; public string id { get { return inner.id; } } public string title { get { return inner.title; } } @@ -27,6 +30,9 @@ public class Audrey.PlaybinSong : Object { public PlaybinSong (Subsonic.Client api, Subsonic.Song song) { this.api = api; this.inner = song; + + this.counter = next_counter; + next_counter += 1; } private Subsonic.Client api; diff --git a/src/playbin/song.rs b/src/playbin/song.rs index 09c8ffd..e325a18 100644 --- a/src/playbin/song.rs +++ b/src/playbin/song.rs @@ -13,6 +13,9 @@ pub mod ffi { extern "C" { pub fn audrey_playbin_song_get_type() -> glib::ffi::GType; + pub fn audrey_playbin_song_get_counter(self_: *mut AudreyPlaybinSong) -> i64; + pub fn audrey_playbin_song_get_id(self_: *mut AudreyPlaybinSong) + -> *const std::ffi::c_char; pub fn audrey_playbin_song_get_title( self_: *mut AudreyPlaybinSong, ) -> *const std::ffi::c_char; @@ -38,6 +41,14 @@ glib::wrapper! { } impl Song { + pub fn counter(&self) -> i64 { + unsafe { ffi::audrey_playbin_song_get_counter(self.to_glib_none().0) } + } + + pub fn id(&self) -> GString { + unsafe { from_glib_none(ffi::audrey_playbin_song_get_id(self.to_glib_none().0)) } + } + pub fn title(&self) -> GString { unsafe { from_glib_none(ffi::audrey_playbin_song_get_title(self.to_glib_none().0)) } } diff --git a/src/subsonic/schema.rs b/src/subsonic/schema.rs index 1710033..22b90dd 100644 --- a/src/subsonic/schema.rs +++ b/src/subsonic/schema.rs @@ -44,7 +44,8 @@ pub struct Child { pub artist: String, pub track: Option, pub year: Option, - pub starred: Option<()>, + pub starred: Option>, // TODO: check which is best + // applicable pub duration: u64, pub play_count: Option, pub genre: Option,