Implement MPRIS D-Bus spec
This commit is contained in:
@@ -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"]
|
||||
|
||||
21
src/main.rs
21
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 =
|
||||
|
||||
324
src/mpris.rs
Normal file
324
src/mpris.rs
Normal file
@@ -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<Spotify>, queue: Arc<Mutex<Queue>>) {
|
||||
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::<bool, _>("CanQuit", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(false); // TODO
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_canraise = f.property::<bool, _>("CanRaise", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(false);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_cansetfullscreen = f.property::<bool, _>("CanSetFullscreen", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(false);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_hastracklist = f.property::<bool, _>("HasTrackList", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(false); // TODO
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_identity = f.property::<String, _>("Identity", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append("ncspot".to_string());
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_urischemes = f.property::<Vec<String>, _>("SupportedUriSchemes", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(vec!["spotify".to_string()]);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_mimetypes = f.property::<Vec<String>, _>("SupportedMimeTypes", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(Vec::new() as Vec<String>);
|
||||
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::<String, _>("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::<String, _>("LoopStatus", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append("None".to_string()); // TODO
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_metadata = {
|
||||
let queue = queue.clone();
|
||||
f.property::<HashMap<String, Variant<Box<RefArg>>>, _>("Metadata", ())
|
||||
.access(Access::Read)
|
||||
.on_get(move |iter, _| {
|
||||
let mut hm: HashMap<String, Variant<Box<RefArg>>> = 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::<i64, _>("Position", ())
|
||||
.access(Access::Read)
|
||||
.on_get(move |iter, _| {
|
||||
iter.append(spotify.get_current_progress().as_micros() as i64);
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
|
||||
let property_volume = f.property::<f64, _>("Volume", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(1.0);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_rate = f.property::<f64, _>("Rate", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(1.0);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_minrate = f.property::<f64, _>("MinimumRate", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(1.0);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_maxrate = f.property::<f64, _>("MaximumRate", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(1.0);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_canplay = f.property::<bool, _>("CanPlay", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(true);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_canpause = f.property::<bool, _>("CanPause", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(true);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_canseek = f.property::<bool, _>("CanSeek", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(false); // TODO
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_cancontrol = f.property::<bool, _>("CanControl", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(true);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_cangonext = f.property::<bool, _>("CanGoNext", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(true);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_cangoprevious = f.property::<bool, _>("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();
|
||||
}
|
||||
25
src/queue.rs
25
src/queue.rs
@@ -37,6 +37,19 @@ impl Queue {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_index(&self) -> Option<usize> {
|
||||
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();
|
||||
}
|
||||
|
||||
34
src/track.rs
34
src/track.rs
@@ -6,25 +6,41 @@ 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<String>,
|
||||
pub album: String,
|
||||
pub album_artists: Vec<String>,
|
||||
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::<Vec<String>>()
|
||||
.join(", ");
|
||||
.collect::<Vec<String>>();
|
||||
let album_artists = track
|
||||
.album.artists
|
||||
.iter()
|
||||
.map(|ref artist| artist.name.clone())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
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: track.album.images[0].url.clone(),
|
||||
url: track.uri.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +53,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 +62,7 @@ impl fmt::Debug for Track {
|
||||
write!(
|
||||
f,
|
||||
"({} - {} ({}))",
|
||||
self.artists,
|
||||
self.artists.join(", "),
|
||||
self.title,
|
||||
self.id.to_base62()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user