use anyhow::Result;
use mpris_server::{Metadata, PlaybackStatus, Player, Time};
use std::thread;
use tokio::sync::mpsc;
#[derive(Debug, Clone)]
pub enum MprisEvent {
PlayPause,
Play,
Pause,
Next,
Previous,
Stop,
Seek(i64), }
#[derive(Debug, Clone)]
#[allow(dead_code)] pub enum MprisCommand {
Metadata {
title: String,
artists: Vec<String>,
album: String,
duration_ms: u32,
art_url: Option<String>,
},
PlaybackStatus(bool), Position(u64), Volume(u8), Stopped,
}
pub struct MprisManager {
event_rx: std::sync::Mutex<Option<mpsc::UnboundedReceiver<MprisEvent>>>,
command_tx: mpsc::UnboundedSender<MprisCommand>,
}
impl MprisManager {
pub fn new() -> Result<Self> {
let (event_tx, event_rx) = mpsc::unbounded_channel();
let (command_tx, mut command_rx) = mpsc::unbounded_channel::<MprisCommand>();
thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create MPRIS runtime");
let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move {
let player = match Player::builder("spotatui")
.identity("spotatui")
.desktop_entry("spotatui")
.can_play(true)
.can_pause(true)
.can_go_next(true)
.can_go_previous(true)
.can_seek(true)
.can_control(true)
.can_quit(false)
.can_raise(false)
.can_set_fullscreen(false)
.build()
.await
{
Ok(p) => p,
Err(e) => {
eprintln!("Failed to build MPRIS player: {}", e);
return;
}
};
let tx = event_tx.clone();
player.connect_play_pause(move |_player| {
let _ = tx.send(MprisEvent::PlayPause);
});
let tx = event_tx.clone();
player.connect_play(move |_player| {
let _ = tx.send(MprisEvent::Play);
});
let tx = event_tx.clone();
player.connect_pause(move |_player| {
let _ = tx.send(MprisEvent::Pause);
});
let tx = event_tx.clone();
player.connect_next(move |_player| {
let _ = tx.send(MprisEvent::Next);
});
let tx = event_tx.clone();
player.connect_previous(move |_player| {
let _ = tx.send(MprisEvent::Previous);
});
let tx = event_tx.clone();
player.connect_stop(move |_player| {
let _ = tx.send(MprisEvent::Stop);
});
let tx = event_tx.clone();
player.connect_seek(move |_player, offset| {
let _ = tx.send(MprisEvent::Seek(offset.as_micros()));
});
tokio::task::spawn_local(player.run());
while let Some(cmd) = command_rx.recv().await {
match cmd {
MprisCommand::Metadata {
title,
artists,
album,
duration_ms,
art_url,
} => {
let mut builder = Metadata::builder()
.title(&title)
.artist(artists.iter().map(|s| s.as_str()).collect::<Vec<_>>())
.album(&album)
.length(Time::from_millis(duration_ms as i64));
if let Some(url) = &art_url {
builder = builder.art_url(url);
}
let metadata = builder.build();
if let Err(e) = player.set_metadata(metadata).await {
eprintln!("MPRIS: Failed to set metadata: {}", e);
}
}
MprisCommand::PlaybackStatus(is_playing) => {
let status = if is_playing {
PlaybackStatus::Playing
} else {
PlaybackStatus::Paused
};
if let Err(e) = player.set_playback_status(status).await {
eprintln!("MPRIS: Failed to set playback status: {}", e);
}
}
MprisCommand::Position(position_ms) => {
player.set_position(Time::from_millis(position_ms as i64));
}
MprisCommand::Volume(volume_percent) => {
let volume = (volume_percent as f64) / 100.0;
if let Err(e) = player.set_volume(volume).await {
eprintln!("MPRIS: Failed to set volume: {}", e);
}
}
MprisCommand::Stopped => {
if let Err(e) = player.set_playback_status(PlaybackStatus::Stopped).await {
eprintln!("MPRIS: Failed to set stopped status: {}", e);
}
}
}
}
});
});
Ok(Self {
event_rx: std::sync::Mutex::new(Some(event_rx)),
command_tx,
})
}
pub fn take_event_rx(&self) -> Option<mpsc::UnboundedReceiver<MprisEvent>> {
self.event_rx.lock().ok()?.take()
}
pub fn set_metadata(
&self,
title: &str,
artists: &[String],
album: &str,
duration_ms: u32,
art_url: Option<String>,
) {
let _ = self.command_tx.send(MprisCommand::Metadata {
title: title.to_string(),
artists: artists.to_vec(),
album: album.to_string(),
duration_ms,
art_url,
});
}
pub fn set_playback_status(&self, is_playing: bool) {
let _ = self
.command_tx
.send(MprisCommand::PlaybackStatus(is_playing));
}
#[allow(dead_code)] pub fn set_position(&self, position_ms: u64) {
let _ = self.command_tx.send(MprisCommand::Position(position_ms));
}
pub fn set_volume(&self, volume_percent: u8) {
let _ = self.command_tx.send(MprisCommand::Volume(volume_percent));
}
pub fn set_stopped(&self) {
let _ = self.command_tx.send(MprisCommand::Stopped);
}
}