diff --git a/Cargo.toml b/Cargo.toml index 4b62e05..4b5e0fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,9 +22,12 @@ rspotify = "0.2.5" serde = "1.0" serde_derive = "1.0" toml = "0.4" +tokio = "0.1.7" tokio-core = "0.1" tokio-timer = "0.2" unicode-width = "0.1.5" +dbus = { version = "0.6.4", optional = true } +dbus-tokio = { version = "0.3.0", optional = true } [dependencies.librespot] git = "https://github.com/librespot-org/librespot.git" @@ -39,4 +42,5 @@ features = ["pancurses-backend"] [features] pulseaudio_backend = ["librespot/pulseaudio-backend"] portaudio_backend = ["librespot/portaudio-backend"] -default = ["pulseaudio_backend"] +mpris = ["dbus", "dbus-tokio"] +default = ["pulseaudio_backend", "mpris"] diff --git a/src/main.rs b/src/main.rs index 9077710..51328df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,10 +4,16 @@ extern crate failure; extern crate futures; extern crate librespot; extern crate rspotify; +extern crate tokio; extern crate tokio_core; extern crate tokio_timer; extern crate unicode_width; +#[cfg(feature = "mpris")] +extern crate dbus; +#[cfg(feature = "mpris")] +extern crate dbus_tokio; + #[macro_use] extern crate serde_derive; extern crate serde; @@ -40,10 +46,16 @@ mod theme; mod track; mod ui; +#[cfg(feature = "mpris")] +mod mpris; + use commands::CommandManager; use events::{Event, EventManager}; use spotify::PlayerEvent; +#[cfg(feature = "mpris")] +use mpris::run_dbus_server; + fn init_logger(content: TextContent, write_to_file: bool) { let mut builder = env_logger::Builder::from_default_env(); { @@ -134,6 +146,15 @@ fn main() { spotify.clone(), ))); + #[cfg(feature = "mpris")] + { + let spotify = spotify.clone(); + let queue = queue.clone(); + std::thread::spawn(move || { + run_dbus_server(spotify, queue); + }); + } + let search = ui::search::SearchView::new(spotify.clone(), queue.clone()); let mut playlists = diff --git a/src/mpris.rs b/src/mpris.rs new file mode 100644 index 0000000..59d31c1 --- /dev/null +++ b/src/mpris.rs @@ -0,0 +1,324 @@ +use std::collections::HashMap; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; + +use dbus::arg::{Variant, RefArg}; +use dbus::tree::{Access}; +use dbus_tokio::AConnection; +use dbus_tokio::tree::{AFactory, ATree, ATreeServer}; + +use tokio::reactor::Handle; +use tokio::runtime::current_thread::Runtime; +use futures::Stream; + +use queue::Queue; +use spotify::{PlayerEvent, Spotify}; + +pub fn run_dbus_server(spotify: Arc, queue: Arc>) { + let conn = Rc::new(dbus::Connection::get_private(dbus::BusType::Session) + .expect("Failed to connect to dbus")); + conn.register_name("org.mpris.MediaPlayer2.ncspot", dbus::NameFlag::ReplaceExisting as u32) + .expect("Failed to register dbus player name"); + + let f = AFactory::new_afn::<()>(); + + 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.MediaPlayer", ()) + .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 = match spotify.get_current_status() { + PlayerEvent::Playing => "Playing", + PlayerEvent::Paused => "Paused", + _ => "Stopped" + }.to_string(); + iter.append(status); + Ok(()) + }) + }; + + let property_loopstatus = f.property::("LoopStatus", ()) + .access(Access::Read) + .on_get(|iter, _| { + iter.append("None".to_string()); // TODO + Ok(()) + }); + + let property_metadata = { + let queue = queue.clone(); + f.property::>>, _>("Metadata", ()) + .access(Access::Read) + .on_get(move |iter, _| { + let mut hm: HashMap>> = HashMap::new(); + + let queue = queue.lock().expect("could not lock queue"); + let track = queue.get_current(); + + hm.insert("mpris:trackid".to_string(), Variant(Box::new( + track.map(|t| format!("spotify:track:{}", t.id.to_base62())).unwrap_or("".to_string()) + ))); + hm.insert("mpris:length".to_string(), Variant(Box::new( + track.map(|t| t.duration * 1_000_000).unwrap_or(0) + ))); + hm.insert("mpris:artUrl".to_string(), Variant(Box::new( + track.map(|t| t.cover_url.clone()).unwrap_or("".to_string()) + ))); + + hm.insert("xesam:album".to_string(), Variant(Box::new( + track.map(|t| t.album.clone()).unwrap_or("".to_string()) + ))); + hm.insert("xesam:albumArtist".to_string(), Variant(Box::new( + track.map(|t| t.album_artists.join(", ")).unwrap_or("".to_string()) + ))); + hm.insert("xesam:artist".to_string(), Variant(Box::new( + track.map(|t| t.artists.join(", ")).unwrap_or("".to_string()) + ))); + hm.insert("xesam:discNumber".to_string(), Variant(Box::new( + track.map(|t| t.disc_number).unwrap_or(0) + ))); + hm.insert("xesam:title".to_string(), Variant(Box::new( + track.map(|t| t.title.clone()).unwrap_or("".to_string()) + ))); + hm.insert("xesam:trackNumber".to_string(), Variant(Box::new( + track.map(|t| t.track_number).unwrap_or(0) + ))); + hm.insert("xesam:url".to_string(), Variant(Box::new( + track.map(|t| t.url.clone()).unwrap_or("".to_string()) + ))); + + iter.append(hm); + Ok(()) + }) + }; + + let property_position = { + let spotify = spotify.clone(); + f.property::("Position", ()) + .access(Access::Read) + .on_get(move |iter, _| { + iter.append(spotify.get_current_progress().as_micros() as i64); + Ok(()) + }) + }; + + let property_volume = f.property::("Volume", ()) + .access(Access::Read) + .on_get(|iter, _| { + iter.append(1.0); + 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(false); // TODO + 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 method_playpause = { + let spotify = spotify.clone(); + f.amethod("PlayPause", (), move |m| { + spotify.toggleplayback(); + Ok(vec![m.msg.method_return()]) + }) + }; + + let method_play = { + let spotify = spotify.clone(); + f.amethod("Play", (), move |m| { + spotify.play(); + Ok(vec![m.msg.method_return()]) + }) + }; + + let method_pause = { + let spotify = spotify.clone(); + f.amethod("Pause", (), move |m| { + spotify.pause(); + Ok(vec![m.msg.method_return()]) + }) + }; + + let method_stop = { + let spotify = spotify.clone(); + f.amethod("Stop", (), move |m| { + spotify.stop(); + Ok(vec![m.msg.method_return()]) + }) + }; + + let method_next = { + let queue = queue.clone(); + f.amethod("Next", (), move |m| { + queue.lock().expect("failed to lock queue").next(); + Ok(vec![m.msg.method_return()]) + }) + }; + + let method_previous = { + let queue = queue.clone(); + f.amethod("Previous", (), move |m| { + queue.lock().expect("failed to lock queue").previous(); + Ok(vec![m.msg.method_return()]) + }) + }; + + // TODO: Seek, SetPosition, Shuffle, OpenUri (?) + + // 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_m(method_playpause) + .add_m(method_play) + .add_m(method_pause) + .add_m(method_stop) + .add_m(method_next) + .add_m(method_previous); + + let tree = f.tree(ATree::new()) + .add(f.object_path("/org/mpris/MediaPlayer2", ()).introspectable() + .add(interface) + .add(interface_player) + ); + + tree.set_registered(&conn, true).expect("failed to register tree"); + + let mut rt = Runtime::new().unwrap(); + let aconn = AConnection::new(conn.clone(), Handle::default(), &mut rt).unwrap(); + let server = ATreeServer::new(conn.clone(), &tree, aconn.messages().unwrap()); + + let server = server.for_each(|m| { + warn!("Unhandled dbus message: {:?}", m); + Ok(()) + }); + rt.block_on(server).unwrap(); + rt.run().unwrap(); +} diff --git a/src/queue.rs b/src/queue.rs index 8da5ad3..b177a23 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -37,6 +37,19 @@ impl Queue { } } + pub fn previous_index(&self) -> Option { + match self.current_track { + Some(index) => { + if index > 0 { + Some(index - 1) + } else { + None + } + } + None => None, + } + } + pub fn get_current(&self) -> Option<&Track> { match self.current_track { Some(index) => Some(&self.queue[index]), @@ -106,8 +119,16 @@ impl Queue { } pub fn next(&mut self) { - if let Some(next_index) = self.next_index() { - self.play(next_index); + if let Some(index) = self.next_index() { + self.play(index); + } else { + self.spotify.stop(); + } + } + + pub fn previous(&mut self) { + if let Some(index) = self.previous_index() { + self.play(index); } else { self.spotify.stop(); } diff --git a/src/track.rs b/src/track.rs index af553d6..e8ac489 100644 --- a/src/track.rs +++ b/src/track.rs @@ -6,25 +6,46 @@ use rspotify::spotify::model::track::FullTrack; #[derive(Clone)] pub struct Track { pub id: SpotifyId, - pub duration: u32, - pub artists: String, pub title: String, + pub track_number: u32, + pub disc_number: i32, + pub duration: u32, + pub artists: Vec, + pub album: String, + pub album_artists: Vec, + pub cover_url: String, + pub url: String, } impl Track { pub fn new(track: &FullTrack) -> Track { - let artists_joined = track + let artists = track .artists .iter() .map(|ref artist| artist.name.clone()) - .collect::>() - .join(", "); + .collect::>(); + let album_artists = track + .album.artists + .iter() + .map(|ref artist| artist.name.clone()) + .collect::>(); + + let cover_url = match track.album.images.get(0) { + Some(image) => image.url.clone(), + None => "".to_owned(), + }; Track { id: SpotifyId::from_base62(&track.id).expect("could not load track"), - duration: track.duration_ms / 1000, - artists: artists_joined, title: track.name.clone(), + track_number: track.track_number, + disc_number: track.disc_number, + duration: track.duration_ms / 1000, + artists: artists, + album: track.album.name.clone(), + album_artists: album_artists, + cover_url: cover_url, + url: track.uri.clone(), } } @@ -37,7 +58,7 @@ impl Track { impl fmt::Display for Track { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} - {}", self.artists, self.title) + write!(f, "{} - {}", self.artists.join(", "), self.title) } } @@ -46,7 +67,7 @@ impl fmt::Debug for Track { write!( f, "({} - {} ({}))", - self.artists, + self.artists.join(", "), self.title, self.id.to_base62() )