From c7fe6017a718a8475f0c6983483ff8c353bdc404 Mon Sep 17 00:00:00 2001 From: Henrik Friedrichsen Date: Thu, 30 Mar 2023 22:25:52 +0200 Subject: [PATCH] Rewrite MPRIS implementation using zbus The initial DBus implementation was getting harder to maintain and `zbus` offers some nice convenience features that should make our MPRIS implementation cleaner. For now this only implements the `org.mpris.MediaPlayer2` interface which does not do much. Should help with #1103 --- Cargo.lock | 263 ++++++++++++++++- Cargo.toml | 5 +- src/main.rs | 10 +- src/mpris.rs | 784 +++------------------------------------------------ 4 files changed, 294 insertions(+), 768 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e37e793f..4668faa4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,6 +152,27 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener", + "futures-core", +] + +[[package]] +name = "async-recursion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.12", +] + [[package]] name = "async-trait" version = "0.1.68" @@ -766,12 +787,14 @@ dependencies = [ ] [[package]] -name = "dbus-tree" -version = "0.9.2" +name = "derivative" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f456e698ae8e54575e19ddb1f9b7bce2298568524f215496b248eb9498b4f508" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "dbus", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -804,6 +827,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "1.0.2" @@ -824,6 +856,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -882,6 +925,27 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "enumflags2" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0044ebdf7fbb2a772e0c0233a9d3173c5cd8af8ae7078d4c5188af44ffffaa4b" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2c772ccdbdfd1967b4f5d79d17c98ebf92009fdcc838db7aa434462f600c26" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.12", +] + [[package]] name = "enumset" version = "1.0.12" @@ -924,6 +988,12 @@ dependencies = [ "libc", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fastrand" version = "1.9.0" @@ -1186,6 +1256,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.11.0" @@ -1734,6 +1810,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1795,8 +1880,6 @@ dependencies = [ "crossbeam-channel", "cursive", "cursive_buffered_backend", - "dbus", - "dbus-tree", "fern", "futures", "ioctl-rs", @@ -1827,6 +1910,7 @@ dependencies = [ "unicode-width", "url", "wl-clipboard-rs", + "zbus", ] [[package]] @@ -1906,7 +1990,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -1918,7 +2002,21 @@ dependencies = [ "bitflags", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", + "static_assertions", ] [[package]] @@ -2249,6 +2347,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_pipe" version = "1.1.3" @@ -2902,6 +3010,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.12", +] + [[package]] name = "serde_spanned" version = "0.6.1" @@ -3035,6 +3154,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stdweb" version = "0.1.3" @@ -3274,6 +3399,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio-macros", + "tracing", "windows-sys 0.45.0", ] @@ -3372,9 +3498,21 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "tracing-core" version = "0.1.30" @@ -3410,6 +3548,16 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -3953,6 +4101,67 @@ dependencies = [ "ncspot", ] +[[package]] +name = "zbus" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dc29e76f558b2cb94190e8605ecfe77dd40f5df8c072951714b4b71a97f5848" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "byteorder", + "derivative", + "dirs", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "lazy_static", + "nix 0.26.2", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "winapi", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62a80fd82c011cd08459eaaf1fd83d3090c1b61e6d5284360074a7475af3a85d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.6.1" @@ -3973,3 +4182,41 @@ dependencies = [ "quote", "syn 1.0.109", ] + +[[package]] +name = "zvariant" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe4914a985446d6fd287019b5fceccce38303d71407d9e6e711d44954a05d8" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34c20260af4b28b3275d6676c7e2a6be0d4332e8e0aba4616d34007fd84e462a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b22993dbc4d128a17a3b6c92f1c63872dd67198537ee728d8b5d7c40640a8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/Cargo.toml b/Cargo.toml index def3cb8d..f3fac550 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,7 @@ chrono = "0.4" clap = "4.1.7" clipboard = {version = "0.5", optional = true} crossbeam-channel = "0.5" -dbus = {version = "0.9.6", optional = true} -dbus-tree = {version = "0.9.2", optional = true} +zbus = {version = "3.11.1", default-features = false, features = ["tokio"], optional = true} fern = "0.6" futures = "0.3" ioctl-rs = {version = "0.2", optional = true} @@ -80,7 +79,7 @@ optional = true alsa_backend = ["librespot-playback/alsa-backend"] cover = ["ioctl-rs"] # Support displaying the album cover default = ["share_clipboard", "pulseaudio_backend", "mpris", "notify", "termion_backend"] -mpris = ["dbus", "dbus-tree"] # Allow ncspot to be controlled via MPRIS API +mpris = ["zbus"] # Allow ncspot to be controlled via MPRIS API notify = ["notify-rust"] # Show what's playing via a notification pancurses_backend = ["cursive/pancurses-backend", "pancurses/win32"] portaudio_backend = ["librespot-playback/portaudio-backend"] diff --git a/src/main.rs b/src/main.rs index 5e932c22..1a6751ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -203,12 +203,7 @@ fn main() -> Result<(), String> { )); #[cfg(feature = "mpris")] - let mpris_manager = Arc::new(mpris::MprisManager::new( - event_manager.clone(), - spotify.clone(), - queue.clone(), - library.clone(), - )); + ASYNC_RUNTIME.spawn(mpris::serve()); let mut cmd_manager = CommandManager::new( spotify.clone(), @@ -364,9 +359,6 @@ fn main() -> Result<(), String> { trace!("event received: {:?}", state); spotify.update_status(state.clone()); - #[cfg(feature = "mpris")] - mpris_manager.update(); - #[cfg(unix)] ipc.publish(&state, queue.get_current()); diff --git a/src/mpris.rs b/src/mpris.rs index 1f23ea6f..04175e20 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -1,763 +1,51 @@ -use std::collections::HashMap; -use std::rc::Rc; -use std::sync::{mpsc, Arc}; -use std::time::Duration; +use std::{error::Error, future::pending}; +use zbus::{dbus_interface, ConnectionBuilder}; -use dbus::arg::{RefArg, Variant}; -use dbus::ffidisp::stdintf::org_freedesktop_dbus::PropertiesPropertiesChanged; -use dbus::message::SignalArgs; -use dbus::strings::Path; -use dbus_tree::{Access, Factory}; -use log::{debug, warn}; +struct MprisRoot {} -use crate::events::EventManager; -use crate::library::Library; -use crate::model::album::Album; -use crate::model::episode::Episode; -use crate::model::playable::Playable; -use crate::model::playlist::Playlist; -use crate::model::show::Show; -use crate::model::track::Track; -use crate::queue::{Queue, RepeatSetting}; -use crate::spotify::{PlayerEvent, Spotify, UriType, VOLUME_PERCENT}; -use crate::traits::ListItem; -use regex::Regex; - -type Metadata = HashMap>>; - -struct MprisState(String, Option); - -fn get_playbackstatus(spotify: Spotify) -> String { - match spotify.get_current_status() { - PlayerEvent::Playing(_) | PlayerEvent::FinishedTrack => "Playing", - PlayerEvent::Paused(_) => "Paused", - _ => "Stopped", +#[dbus_interface(name = "org.mpris.MediaPlayer2")] +impl MprisRoot { + #[dbus_interface(property)] + fn can_quit(&self) -> bool { + true } - .to_string() -} - -fn get_metadata(playable: Option, spotify: Spotify, library: Arc) -> Metadata { - let mut hm: Metadata = HashMap::new(); - - // Fetch full track details in case this playable is based on a SimplifiedTrack - // This is necessary because SimplifiedTrack objects don't contain a cover_url - let playable_full = playable.and_then(|p| match p { - Playable::Track(track) => { - if track.cover_url.is_some() { - // We already have `cover_url`, no need to fetch the full track - Some(Playable::Track(track)) - } else { - spotify - .api - .track(&track.id.unwrap_or_default()) - .as_ref() - .map(|t| Playable::Track(t.into())) - } - } - Playable::Episode(episode) => Some(Playable::Episode(episode)), - }); - let playable = playable_full.as_ref(); - - hm.insert( - "mpris:trackid".to_string(), - Variant(Box::new(Path::from(format!( - "/org/ncspot/{}", - playable - .filter(|t| t.id().is_some()) - .map(|t| t.uri().replace(':', "/")) - .unwrap_or_else(|| String::from("0")) - )))), - ); - hm.insert( - "mpris:length".to_string(), - Variant(Box::new( - playable.map(|t| t.duration() as i64 * 1_000).unwrap_or(0), - )), - ); - hm.insert( - "mpris:artUrl".to_string(), - Variant(Box::new( - playable - .map(|t| t.cover_url().unwrap_or_default()) - .unwrap_or_default(), - )), - ); - - hm.insert( - "xesam:album".to_string(), - Variant(Box::new( - playable - .and_then(|p| p.track()) - .map(|t| t.album.unwrap_or_default()) - .unwrap_or_default(), - )), - ); - hm.insert( - "xesam:albumArtist".to_string(), - Variant(Box::new( - playable - .and_then(|p| p.track()) - .map(|t| t.album_artists) - .unwrap_or_default(), - )), - ); - hm.insert( - "xesam:artist".to_string(), - Variant(Box::new( - playable - .and_then(|p| p.track()) - .map(|t| t.artists) - .unwrap_or_default(), - )), - ); - hm.insert( - "xesam:discNumber".to_string(), - Variant(Box::new( - playable - .and_then(|p| p.track()) - .map(|t| t.disc_number) - .unwrap_or(0), - )), - ); - hm.insert( - "xesam:title".to_string(), - Variant(Box::new( - playable - .map(|t| match t { - Playable::Track(t) => t.title.clone(), - Playable::Episode(ep) => ep.name.clone(), - }) - .unwrap_or_default(), - )), - ); - hm.insert( - "xesam:trackNumber".to_string(), - Variant(Box::new( - playable - .and_then(|p| p.track()) - .map(|t| t.track_number) - .unwrap_or(0) as i32, - )), - ); - hm.insert( - "xesam:url".to_string(), - Variant(Box::new( - playable - .map(|t| t.share_url().unwrap_or_default()) - .unwrap_or_default(), - )), - ); - hm.insert( - "xesam:userRating".to_string(), - Variant(Box::new( - playable - .and_then(|p| p.track()) - .map(|t| match library.is_saved_track(&Playable::Track(t)) { - true => 1.0, - false => 0.0, - }) - .unwrap_or(0.0), - )), - ); - - hm -} - -fn run_dbus_server( - ev: EventManager, - spotify: Spotify, - queue: Arc, - library: Arc, - rx: mpsc::Receiver, -) { - let conn = Rc::new( - dbus::ffidisp::Connection::get_private(dbus::ffidisp::BusType::Session) - .expect("Failed to connect to dbus"), - ); - conn.register_name( - "org.mpris.MediaPlayer2.ncspot", - dbus::ffidisp::NameFlag::ReplaceExisting as u32, - ) - .expect("Failed to register dbus player name"); - - let f = Factory::new_fn::<()>(); - - let property_canquit = f - .property::("CanQuit", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(false); // TODO - Ok(()) - }); - - let property_canraise = f - .property::("CanRaise", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(false); - Ok(()) - }); - - let property_cansetfullscreen = f - .property::("CanSetFullscreen", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(false); - Ok(()) - }); - - let property_hastracklist = f - .property::("HasTrackList", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(false); // TODO - Ok(()) - }); - - let property_identity = f - .property::("Identity", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append("ncspot".to_string()); - Ok(()) - }); - - let property_urischemes = f - .property::, _>("SupportedUriSchemes", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(vec!["spotify".to_string()]); - Ok(()) - }); - - let property_mimetypes = f - .property::, _>("SupportedMimeTypes", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(Vec::new() as Vec); - Ok(()) - }); - - // https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html - let interface = f - .interface("org.mpris.MediaPlayer2", ()) - .add_p(property_canquit) - .add_p(property_canraise) - .add_p(property_cansetfullscreen) - .add_p(property_hastracklist) - .add_p(property_identity) - .add_p(property_urischemes) - .add_p(property_mimetypes); - - let property_playbackstatus = { - let spotify = spotify.clone(); - f.property::("PlaybackStatus", ()) - .access(Access::Read) - .on_get(move |iter, _| { - let status = get_playbackstatus(spotify.clone()); - iter.append(status); - Ok(()) - }) - }; - - let property_loopstatus = { - let queue1 = queue.clone(); - let queue2 = queue.clone(); - f.property::("LoopStatus", ()) - .access(Access::ReadWrite) - .on_get(move |iter, _| { - iter.append( - match queue1.get_repeat() { - RepeatSetting::None => "None", - RepeatSetting::RepeatTrack => "Track", - RepeatSetting::RepeatPlaylist => "Playlist", - } - .to_string(), - ); - Ok(()) - }) - .on_set(move |iter, _| { - let setting = match iter.get::<&str>().unwrap_or_default() { - "Track" => RepeatSetting::RepeatTrack, - "Playlist" => RepeatSetting::RepeatPlaylist, - _ => RepeatSetting::None, - }; - queue2.set_repeat(setting); - - Ok(()) - }) - }; - - let property_metadata = { - let spotify = spotify.clone(); - let queue = queue.clone(); - let library = library.clone(); - f.property::>>, _>("Metadata", ()) - .access(Access::Read) - .on_get(move |iter, _| { - let hm = get_metadata( - queue.clone().get_current(), - spotify.clone(), - library.clone(), - ); - - iter.append(hm); - Ok(()) - }) - }; - - let property_position = { - let spotify = spotify.clone(); - f.property::("Position", ()) - .access(Access::Read) - .on_get(move |iter, _| { - let progress = spotify.get_current_progress(); - iter.append(progress.as_micros() as i64); - Ok(()) - }) - }; - - let property_volume = { - let spotify1 = spotify.clone(); - let spotify2 = spotify.clone(); - let event = ev.clone(); - f.property::("Volume", ()) - .access(Access::ReadWrite) - .on_get(move |i, _| { - i.append(spotify1.volume() as f64 / 65535_f64); - Ok(()) - }) - .on_set(move |i, _| { - let cur = spotify2.volume() as f64 / 65535_f64; - let req = i.get::().unwrap_or(cur); - if (0.0..=1.0).contains(&req) { - let vol = (VOLUME_PERCENT as f64) * req * 100.0; - spotify2.set_volume(vol as u16); - } - event.trigger(); - Ok(()) - }) - }; - - let property_rate = f - .property::("Rate", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(1.0); - Ok(()) - }); - - let property_minrate = f - .property::("MinimumRate", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(1.0); - Ok(()) - }); - - let property_maxrate = f - .property::("MaximumRate", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(1.0); - Ok(()) - }); - - let property_canplay = f - .property::("CanPlay", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(true); - Ok(()) - }); - - let property_canpause = f - .property::("CanPause", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(true); - Ok(()) - }); - let property_canseek = f - .property::("CanSeek", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(true); - Ok(()) - }); - - let property_cancontrol = f - .property::("CanControl", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(true); - Ok(()) - }); - - let property_cangonext = f - .property::("CanGoNext", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(true); - Ok(()) - }); - - let property_cangoprevious = f - .property::("CanGoPrevious", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(true); - Ok(()) - }); - - let property_shuffle = { - let queue_get = queue.clone(); - let queue_set = queue.clone(); - f.property::("Shuffle", ()) - .access(Access::ReadWrite) - .on_get(move |iter, _| { - let current_state = queue_get.get_shuffle(); - iter.append(current_state); - Ok(()) - }) - .on_set(move |iter, _| { - if let Some(shuffle_state) = iter.get() { - queue_set.set_shuffle(shuffle_state); - } - ev.trigger(); - Ok(()) - }) - }; - - let property_cangoforward = f - .property::("CanGoForward", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(true); - Ok(()) - }); - - let property_canrewind = f - .property::("CanRewind", ()) - .access(Access::Read) - .on_get(|iter, _| { - iter.append(true); - Ok(()) - }); - - let method_playpause = { - let queue = queue.clone(); - f.method("PlayPause", (), move |m| { - queue.toggleplayback(); - Ok(vec![m.msg.method_return()]) - }) - }; - - let method_play = { - let spotify = spotify.clone(); - f.method("Play", (), move |m| { - spotify.play(); - Ok(vec![m.msg.method_return()]) - }) - }; - - let method_pause = { - let spotify = spotify.clone(); - f.method("Pause", (), move |m| { - spotify.pause(); - Ok(vec![m.msg.method_return()]) - }) - }; - - let method_stop = { - let spotify = spotify.clone(); - f.method("Stop", (), move |m| { - spotify.stop(); - Ok(vec![m.msg.method_return()]) - }) - }; - - let method_next = { - let queue = queue.clone(); - f.method("Next", (), move |m| { - queue.next(true); - Ok(vec![m.msg.method_return()]) - }) - }; - - let method_previous = { - let spotify = spotify.clone(); - let queue = queue.clone(); - f.method("Previous", (), move |m| { - if spotify.get_current_progress() < Duration::from_secs(5) { - queue.previous(); - } else { - spotify.seek(0); - } - Ok(vec![m.msg.method_return()]) - }) - }; - - let method_forward = { - let spotify = spotify.clone(); - f.method("Forward", (), move |m| { - spotify.seek_relative(5000); - Ok(vec![m.msg.method_return()]) - }) - }; - - let method_rewind = { - let spotify = spotify.clone(); - f.method("Rewind", (), move |m| { - spotify.seek_relative(-5000); - Ok(vec![m.msg.method_return()]) - }) - }; - - let method_seek = { - let queue = queue.clone(); - let spotify = spotify.clone(); - f.method("Seek", (), move |m| { - if let Some(current_track) = queue.get_current() { - let offset = m.msg.get1::().unwrap_or(0); // micros - let progress = spotify.get_current_progress(); - let new_position = (progress.as_secs() * 1000) as i32 - + progress.subsec_millis() as i32 - + (offset / 1000) as i32; - let new_position = new_position.max(0) as u32; - let duration = current_track.duration(); - - if new_position < duration { - spotify.seek(new_position); - } else { - queue.next(true); - } - } - Ok(vec![m.msg.method_return()]) - }) - }; - - let method_set_position = { - let queue = queue.clone(); - let spotify = spotify.clone(); - f.method("SetPosition", (), move |m| { - if let Some(current_track) = queue.get_current() { - let (_, position) = m.msg.get2::(); // micros - let position = (position.unwrap_or(0) / 1000) as u32; - let duration = current_track.duration(); - - if position < duration { - spotify.seek(position); - } - } - Ok(vec![m.msg.method_return()]) - }) - }; - - let method_openuri = { - let spotify = spotify.clone(); - f.method("OpenUri", (), move |m| { - let uri_data: Option<&str> = m.msg.get1(); - let uri = match uri_data { - Some(s) => { - let spotify_uri = if s.contains("open.spotify.com") { - let regex = Regex::new(r"https?://open\.spotify\.com(/user/\S+)?/(album|track|playlist|show|episode)/(.+)(\?si=\S+)?").unwrap(); - let captures = regex.captures(s).unwrap(); - let uri_type = &captures[2]; - let id = &captures[3]; - format!("spotify:{uri_type}:{id}") - }else { - s.to_string() - }; - spotify_uri - } - None => "".to_string(), - }; - let id = &uri[uri.rfind(':').unwrap_or(0) + 1..uri.len()]; - let uri_type = UriType::from_uri(&uri); - match uri_type { - Some(UriType::Album) => { - if let Some(a) = spotify.api.album(id) { - if let Some(t) = &Album::from(&a).tracks { - let should_shuffle = queue.get_shuffle(); - queue.clear(); - let index = queue.append_next( - &t.iter() - .map(|track| Playable::Track(track.clone())) - .collect(), - ); - queue.play(index, should_shuffle, should_shuffle) - } - } - } - Some(UriType::Track) => { - if let Some(t) = spotify.api.track(id) { - queue.clear(); - queue.append(Playable::Track(Track::from(&t))); - queue.play(0, false, false) - } - } - Some(UriType::Playlist) => { - if let Some(p) = spotify.api.playlist(id) { - let mut playlist = Playlist::from(&p); - let spotify = spotify.clone(); - playlist.load_tracks(spotify); - if let Some(tracks) = &playlist.tracks { - let should_shuffle = queue.get_shuffle(); - queue.clear(); - let index = queue.append_next(tracks); - queue.play(index, should_shuffle, should_shuffle) - } - } - } - Some(UriType::Show) => { - if let Some(s) = spotify.api.get_show(id) { - let mut show: Show = (&s).into(); - let spotify = spotify.clone(); - show.load_all_episodes(spotify); - if let Some(e) = &show.episodes { - let should_shuffle = queue.get_shuffle(); - queue.clear(); - let mut ep = e.clone(); - ep.reverse(); - let index = queue.append_next( - &ep.iter() - .map(|episode| Playable::Episode(episode.clone())) - .collect(), - ); - queue.play(index, should_shuffle, should_shuffle) - } - } - } - Some(UriType::Episode) => { - if let Some(e) = spotify.api.episode(id) { - queue.clear(); - queue.append(Playable::Episode(Episode::from(&e))); - queue.play(0, false, false) - } - } - Some(UriType::Artist) => { - if let Some(a) = spotify.api.artist_top_tracks(id) { - let should_shuffle = queue.get_shuffle(); - queue.clear(); - let index = queue.append_next(&a.iter().map(|track| Playable::Track(track.clone())).collect()); - queue.play(index, should_shuffle, should_shuffle) - } - } - None => {} - } - Ok(vec![m.msg.method_return()]) - }) - }; - - // https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html - let interface_player = f - .interface("org.mpris.MediaPlayer2.Player", ()) - .add_p(property_playbackstatus) - .add_p(property_loopstatus) - .add_p(property_metadata) - .add_p(property_position) - .add_p(property_volume) - .add_p(property_rate) - .add_p(property_minrate) - .add_p(property_maxrate) - .add_p(property_canplay) - .add_p(property_canpause) - .add_p(property_canseek) - .add_p(property_cancontrol) - .add_p(property_cangonext) - .add_p(property_cangoprevious) - .add_p(property_shuffle) - .add_p(property_cangoforward) - .add_p(property_canrewind) - .add_m(method_playpause) - .add_m(method_play) - .add_m(method_pause) - .add_m(method_stop) - .add_m(method_next) - .add_m(method_previous) - .add_m(method_forward) - .add_m(method_rewind) - .add_m(method_seek) - .add_m(method_set_position) - .add_m(method_openuri); - - let tree = f.tree(()).add( - f.object_path("/org/mpris/MediaPlayer2", ()) - .introspectable() - .add(interface) - .add(interface_player), - ); - - tree.set_registered(&conn, true) - .expect("failed to register tree"); - - conn.add_handler(tree); - loop { - if let Some(m) = conn.incoming(200).next() { - warn!("Unhandled dbus message: {:?}", m); - } - - if let Ok(state) = rx.try_recv() { - let mut changed: PropertiesPropertiesChanged = Default::default(); - debug!( - "mpris PropertiesChanged: status {}, track: {:?}", - state.0, state.1 - ); + #[dbus_interface(property)] + fn can_raise(&self) -> bool { + false + } - changed.interface_name = "org.mpris.MediaPlayer2.Player".to_string(); - changed.changed_properties.insert( - "Metadata".to_string(), - Variant(Box::new(get_metadata( - state.1, - spotify.clone(), - library.clone(), - ))), - ); + #[dbus_interface(property)] + fn has_tracklist(&self) -> bool { + true + } - changed - .changed_properties - .insert("PlaybackStatus".to_string(), Variant(Box::new(state.0))); + #[dbus_interface(property)] + fn identity(&self) -> &str { + "ncspot" + } - conn.send( - changed.to_emit_message(&Path::new("/org/mpris/MediaPlayer2".to_string()).unwrap()), - ) - .unwrap(); - } + #[dbus_interface(property)] + fn supported_uri_schemes(&self) -> Vec { + vec!["spotify".to_string()] } -} -#[derive(Clone)] -pub struct MprisManager { - tx: mpsc::Sender, - queue: Arc, - spotify: Spotify, + #[dbus_interface(property)] + fn supported_mime_types(&self) -> Vec { + Vec::new() + } } -impl MprisManager { - pub fn new( - ev: EventManager, - spotify: Spotify, - queue: Arc, - library: Arc, - ) -> Self { - let (tx, rx) = mpsc::channel::(); +pub async fn serve() -> Result<(), Box> { + let root = MprisRoot {}; - { - let spotify = spotify.clone(); - let queue = queue.clone(); - std::thread::spawn(move || { - run_dbus_server(ev, spotify.clone(), queue.clone(), library.clone(), rx); - }); - } + let _conn = ConnectionBuilder::session()? + .name("org.mpris.MediaPlayer2.ncspot")? + .serve_at("/org/mpris/MediaPlayer2", root)? + .build() + .await?; - MprisManager { tx, queue, spotify } - } + pending::<()>().await; - pub fn update(&self) { - let status = get_playbackstatus(self.spotify.clone()); - let track = self.queue.get_current(); - self.tx.send(MprisState(status, track)).unwrap(); - } + Ok(()) }