use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::{Context, Result};
use zbus::interface;
use zbus::zvariant::{ObjectPath, OwnedValue, Value};
const PATH: &str = "/org/mpris/MediaPlayer2";
#[derive(Debug, Clone, Copy)]
pub enum Remote {
PlayPause,
Play,
Pause,
Stop,
Next,
Prev,
Seek(i64),
Volume(f64),
Quit,
}
#[derive(Clone, Default, PartialEq)]
pub struct Status {
pub has_track: bool,
pub paused: bool,
pub title: String,
pub artist: String,
pub album: String,
pub path: PathBuf,
pub length_us: u64,
pub pos_us: u64,
pub volume: f64,
pub can_next: bool,
pub can_prev: bool,
}
pub struct Mpris {
pub rx: Receiver<Remote>,
status: Arc<Mutex<Status>>,
}
impl Mpris {
pub fn publish(&self, status: &Status) {
if let Ok(mut slot) = self.status.lock()
&& *slot != *status
{
slot.clone_from(status);
}
}
}
pub fn start() -> Result<Mpris> {
let (tx, rx) = channel();
let status = Arc::new(Mutex::new(Status::default()));
let conn = match serve("org.mpris.MediaPlayer2.vibox", &tx, &status) {
Ok(conn) => conn,
Err(_) => serve(
&format!("org.mpris.MediaPlayer2.vibox.instance{}", std::process::id()),
&tx,
&status,
)
.context("no d-bus session bus, so no media key support")?,
};
let watched = Arc::clone(&status);
std::thread::Builder::new()
.name("vibox-mpris".into())
.spawn(move || {
let Ok(iface) = conn
.object_server()
.interface::<_, Player>(PATH)
else {
return;
};
let mut last = Status::default();
loop {
std::thread::sleep(Duration::from_millis(400));
let now = match watched.lock() {
Ok(s) => s.clone(),
Err(_) => return,
};
let emitter = iface.signal_emitter();
let guard = iface.get();
if now.has_track != last.has_track || now.paused != last.paused {
let _ = zbus::block_on(guard.playback_status_changed(emitter));
}
if now.path != last.path {
let _ = zbus::block_on(guard.metadata_changed(emitter));
}
if (now.volume - last.volume).abs() > f64::EPSILON {
let _ = zbus::block_on(guard.volume_changed(emitter));
}
if now.can_next != last.can_next || now.can_prev != last.can_prev {
let _ = zbus::block_on(guard.can_go_next_changed(emitter));
let _ = zbus::block_on(guard.can_go_previous_changed(emitter));
}
drop(guard);
last = now;
}
})
.context("cannot start the mpris thread")?;
Ok(Mpris { rx, status })
}
fn serve(
name: &str,
tx: &Sender<Remote>,
status: &Arc<Mutex<Status>>,
) -> zbus::Result<zbus::blocking::Connection> {
zbus::blocking::connection::Builder::session()?
.name(name)?
.serve_at(PATH, Root { tx: tx.clone() })?
.serve_at(
PATH,
Player {
tx: tx.clone(),
status: Arc::clone(status),
},
)?
.build()
}
struct Root {
tx: Sender<Remote>,
}
#[interface(name = "org.mpris.MediaPlayer2")]
impl Root {
fn raise(&self) {}
fn quit(&self) {
let _ = self.tx.send(Remote::Quit);
}
#[zbus(property)]
fn can_quit(&self) -> bool {
true
}
#[zbus(property)]
fn can_raise(&self) -> bool {
false
}
#[zbus(property)]
fn has_track_list(&self) -> bool {
false
}
#[zbus(property)]
fn identity(&self) -> String {
"vibox".into()
}
#[zbus(property)]
fn supported_uri_schemes(&self) -> Vec<String> {
vec!["file".into()]
}
#[zbus(property)]
fn supported_mime_types(&self) -> Vec<String> {
["audio/mpeg", "audio/flac", "audio/ogg", "audio/mp4", "audio/wav"]
.iter()
.map(|s| (*s).to_string())
.collect()
}
}
struct Player {
tx: Sender<Remote>,
status: Arc<Mutex<Status>>,
}
impl Player {
fn status(&self) -> Status {
self.status.lock().map(|s| s.clone()).unwrap_or_default()
}
fn send(&self, remote: Remote) {
let _ = self.tx.send(remote);
}
}
#[interface(name = "org.mpris.MediaPlayer2.Player")]
impl Player {
fn next(&self) {
self.send(Remote::Next);
}
fn previous(&self) {
self.send(Remote::Prev);
}
fn pause(&self) {
self.send(Remote::Pause);
}
fn play_pause(&self) {
self.send(Remote::PlayPause);
}
fn stop(&self) {
self.send(Remote::Stop);
}
fn play(&self) {
self.send(Remote::Play);
}
fn seek(&self, offset_us: i64) {
self.send(Remote::Seek(offset_us));
}
fn set_position(&self, _track: ObjectPath<'_>, position_us: i64) {
let now = self.status().pos_us as i64;
self.send(Remote::Seek(position_us - now));
}
fn open_uri(&self, _uri: String) {}
#[zbus(property)]
fn playback_status(&self) -> String {
let s = self.status();
match (s.has_track, s.paused) {
(false, _) => "Stopped",
(true, true) => "Paused",
(true, false) => "Playing",
}
.into()
}
#[zbus(property)]
fn metadata(&self) -> HashMap<String, OwnedValue> {
let s = self.status();
let mut meta = HashMap::new();
let id = s.path.to_string_lossy().bytes().fold(0u64, |acc, b| {
acc.wrapping_mul(31).wrapping_add(u64::from(b))
});
if let Ok(path) = ObjectPath::try_from(format!("/org/mpris/vibox/track/{id}"))
&& let Ok(v) = OwnedValue::try_from(Value::from(path))
{
meta.insert("mpris:trackid".into(), v);
}
if let Ok(v) = OwnedValue::try_from(Value::from(s.length_us as i64)) {
meta.insert("mpris:length".into(), v);
}
if let Ok(v) = OwnedValue::try_from(Value::from(s.title.clone())) {
meta.insert("xesam:title".into(), v);
}
if let Ok(v) = OwnedValue::try_from(Value::from(vec![s.artist.clone()])) {
meta.insert("xesam:artist".into(), v);
}
if let Ok(v) = OwnedValue::try_from(Value::from(s.album.clone())) {
meta.insert("xesam:album".into(), v);
}
if let Ok(v) = OwnedValue::try_from(Value::from(file_uri(&s.path))) {
meta.insert("xesam:url".into(), v);
}
meta
}
#[zbus(property)]
fn volume(&self) -> f64 {
self.status().volume
}
#[zbus(property)]
fn set_volume(&self, volume: f64) {
self.send(Remote::Volume(volume.clamp(0.0, 1.0)));
}
#[zbus(property)]
fn position(&self) -> i64 {
self.status().pos_us as i64
}
#[zbus(property)]
fn rate(&self) -> f64 {
1.0
}
#[zbus(property)]
fn minimum_rate(&self) -> f64 {
1.0
}
#[zbus(property)]
fn maximum_rate(&self) -> f64 {
1.0
}
#[zbus(property)]
fn can_go_next(&self) -> bool {
self.status().can_next
}
#[zbus(property)]
fn can_go_previous(&self) -> bool {
self.status().can_prev
}
#[zbus(property)]
fn can_play(&self) -> bool {
true
}
#[zbus(property)]
fn can_pause(&self) -> bool {
true
}
#[zbus(property)]
fn can_seek(&self) -> bool {
true
}
#[zbus(property)]
fn can_control(&self) -> bool {
true
}
}
fn file_uri(path: &std::path::Path) -> String {
let mut out = String::from("file://");
for byte in path.to_string_lossy().bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'/' | b'-' | b'_' | b'.' | b'~' => {
out.push(byte as char);
}
_ => out.push_str(&format!("%{byte:02X}")),
}
}
out
}