use std::{
fmt::Display,
sync::{LazyLock, Mutex, atomic::Ordering, mpsc::Sender},
time::Instant,
};
use lunar_lib::{error, trace, warn};
use mpris_server::{
LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Property, RootInterface,
Server, Signal, Time, TrackId as ConnectorId, Volume,
zbus::{self},
};
use selene_core::library::{artist::Artist, track::lyric_data::LyricData};
use crate::{
SHUTDOWN,
player::{PlayerCommand, PlayerError, PlayerRequest, PlayerTx},
playlist::{LoopMode, ResolvedTrack, ShuffleMode},
};
pub enum MprisEvent {
CurrentlyPlayingChanged {
currently_playing: Box<ResolvedTrack>,
},
IsPlayingChanged {
is_playing: bool,
changed_at: f64,
},
PlaybackStopped,
LoopStatusChanged {
loop_mode: LoopMode,
},
ShuffleStatusChanged {
shuffle_mode: ShuffleMode,
},
VolumeChanged {
volume: f32,
},
Seeked {
time: f64,
},
}
impl Display for MprisEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MprisEvent::CurrentlyPlayingChanged { .. } => f.write_str("CurrentlyPlayingChanged"),
MprisEvent::IsPlayingChanged { .. } => f.write_str("IsPlayingChanged"),
MprisEvent::PlaybackStopped => f.write_str("PlaybackStopped"),
MprisEvent::LoopStatusChanged { .. } => f.write_str("LoopStatusChanged"),
MprisEvent::ShuffleStatusChanged { .. } => f.write_str("ShuffleStatusChanged"),
MprisEvent::VolumeChanged { .. } => f.write_str("VolumeChanged"),
MprisEvent::Seeked { .. } => f.write_str("Seeked"),
}
}
}
static META_NO_TRACK: LazyLock<Metadata> =
LazyLock::new(|| Metadata::builder().trackid(ConnectorId::NO_TRACK).build());
pub struct MprisConnector {
player_tx: Sender<PlayerRequest>,
state: Mutex<MprisState>,
}
struct MprisState {
currently_playing: Option<ResolvedTrack>,
playback_status: PlaybackStatus,
loop_status: LoopStatus,
shuffle: bool,
metadata: Metadata,
volume: f64,
position_us: i64,
recorded_at: Option<Instant>,
}
impl Default for MprisState {
fn default() -> Self {
Self {
playback_status: PlaybackStatus::Stopped,
loop_status: LoopStatus::None,
shuffle: false,
metadata: META_NO_TRACK.clone(),
volume: 1.0,
position_us: 0,
recorded_at: None,
currently_playing: None,
}
}
}
impl MprisConnector {
fn new(player_tx: Sender<PlayerRequest>) -> Self {
Self {
player_tx,
state: Mutex::new(MprisState::default()),
}
}
fn state(&self) -> std::sync::MutexGuard<'_, MprisState> {
self.state.lock().unwrap()
}
fn position(&self) -> Time {
let state = self.state.lock().unwrap();
if let Some(instant) = state.recorded_at {
Time::from_micros(instant.elapsed().as_micros() as i64 + state.position_us)
} else {
Time::ZERO
}
}
pub fn open(player_tx: Sender<PlayerRequest>) -> tokio::sync::mpsc::Sender<MprisEvent> {
trace!("Opening MPRIS thread");
let (tx, rx) = tokio::sync::mpsc::channel(24);
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let server = Server::new(
"org.mpris.MediaPlayer2.selene",
MprisConnector::new(player_tx),
)
.await
.unwrap();
match MprisConnector::run(rx, server).await {
Ok(()) => {
warn!("MPRIS thread exited")
}
Err(err) => {
error!("MPRIS thread failed with error: {err}")
}
};
});
SHUTDOWN.store(true, Ordering::Relaxed);
});
tx
}
async fn run(
mut event_rx: tokio::sync::mpsc::Receiver<MprisEvent>,
server: Server<MprisConnector>,
) -> Result<(), Box<dyn std::error::Error>> {
while let Some(event) = event_rx.recv().await {
if SHUTDOWN.load(Ordering::Relaxed) {
return Ok(());
}
trace!("MPRIS received event: {event}");
match event {
MprisEvent::CurrentlyPlayingChanged { currently_playing } => {
let metadata = metadata_from_playing(¤tly_playing)?;
{
let mut state = server.imp().state();
state.currently_playing = Some(*currently_playing);
state.metadata = metadata.clone();
state.position_us = 0;
state.recorded_at = Some(Instant::now());
}
if let Err(err) = server
.properties_changed([Property::Metadata(metadata)])
.await
{
error!("MPRIS Error: {err}")
};
}
MprisEvent::PlaybackStopped => {
let metadata = META_NO_TRACK.clone();
{
let mut state = server.imp().state();
state.currently_playing = None;
state.playback_status = PlaybackStatus::Stopped;
state.metadata = metadata.clone();
state.position_us = 0;
state.recorded_at = None;
}
if let Err(err) = server
.properties_changed([
Property::Metadata(metadata),
Property::PlaybackStatus(PlaybackStatus::Stopped),
])
.await
{
error!("MPRIS Error: {err}")
};
}
MprisEvent::IsPlayingChanged {
is_playing,
changed_at,
} => {
let playback_status = if is_playing {
PlaybackStatus::Playing
} else {
PlaybackStatus::Paused
};
{
let mut state = server.imp().state();
state.playback_status = playback_status;
state.position_us = (changed_at * 1_000_000.0) as i64;
state.recorded_at = Some(Instant::now());
}
if let Err(err) = server
.properties_changed([Property::PlaybackStatus(playback_status)])
.await
{
error!("MPRIS Error: {err}")
};
}
MprisEvent::LoopStatusChanged { loop_mode: status } => {
let status = match status {
LoopMode::None => LoopStatus::None,
LoopMode::Loop => LoopStatus::Playlist,
LoopMode::LoopAndReshuffle => LoopStatus::Playlist,
LoopMode::RepeatTrack => LoopStatus::Track,
};
{
let mut state = server.imp().state();
state.loop_status = status
}
if let Err(err) = server
.properties_changed([Property::LoopStatus(status)])
.await
{
error!("MPRIS Error: {err}")
};
}
MprisEvent::ShuffleStatusChanged {
shuffle_mode: status,
} => {
let status = !matches!(status, ShuffleMode::None);
{
let mut state = server.imp().state();
state.shuffle = status
}
if let Err(err) = server.properties_changed([Property::Shuffle(status)]).await {
error!("MPRIS Error: {err}")
};
}
MprisEvent::VolumeChanged { volume } => {
{
let mut state = server.imp().state();
state.volume = volume as f64
}
if let Err(err) = server
.properties_changed([Property::Volume(volume as f64)])
.await
{
error!("MPRIS Error: {err}")
};
}
MprisEvent::Seeked { time } => {
let micros = (time * 1_000_000.0) as i64;
let time = Time::from_micros(micros);
{
let mut state = server.imp().state();
state.position_us = micros;
state.recorded_at = Some(Instant::now())
}
if let Err(err) = server.emit(Signal::Seeked { position: time }).await {
error!("MPRIS Error: {err}")
}
}
}
}
Ok(())
}
}
impl RootInterface for MprisConnector {
async fn raise(&self) -> zbus::fdo::Result<()> {
Ok(())
}
async fn quit(&self) -> zbus::fdo::Result<()> {
SHUTDOWN.store(true, Ordering::Relaxed);
Ok(())
}
async fn can_quit(&self) -> zbus::fdo::Result<bool> {
Ok(true)
}
async fn fullscreen(&self) -> zbus::fdo::Result<bool> {
Err(zbus::fdo::Error::ZBus(zbus::Error::Unsupported))
}
async fn set_fullscreen(&self, _fullscreen: bool) -> zbus::Result<()> {
Err(zbus::Error::Unsupported)
}
async fn can_set_fullscreen(&self) -> zbus::fdo::Result<bool> {
Ok(false)
}
async fn can_raise(&self) -> zbus::fdo::Result<bool> {
Ok(false)
}
async fn has_track_list(&self) -> zbus::fdo::Result<bool> {
Ok(false)
}
async fn identity(&self) -> zbus::fdo::Result<String> {
Ok("Selene".to_owned())
}
async fn desktop_entry(&self) -> zbus::fdo::Result<String> {
Err(zbus::fdo::Error::ZBus(zbus::Error::Unsupported))
}
async fn supported_uri_schemes(&self) -> zbus::fdo::Result<Vec<String>> {
Ok(["file", "selene"].into_iter().map(str::to_owned).collect())
}
async fn supported_mime_types(&self) -> zbus::fdo::Result<Vec<String>> {
Ok([
"audio/flac",
"audio/ogg",
"audio/opus",
"audio/wav",
"audio/mpeg",
]
.into_iter()
.map(str::to_owned)
.collect())
}
}
impl From<PlayerError> for mpris_server::zbus::fdo::Error {
fn from(value: PlayerError) -> Self {
Self::Failed(value.to_string())
}
}
impl From<PlayerError> for mpris_server::zbus::Error {
fn from(value: PlayerError) -> Self {
Self::Failure(value.to_string())
}
}
impl PlayerInterface for MprisConnector {
async fn next(&self) -> zbus::fdo::Result<()> {
self.player_tx.command(PlayerCommand::Next)?;
Ok(())
}
async fn previous(&self) -> zbus::fdo::Result<()> {
self.player_tx.command(PlayerCommand::Previous)?;
Ok(())
}
async fn pause(&self) -> zbus::fdo::Result<()> {
self.player_tx
.command(PlayerCommand::SetIsPlaying { is_playing: false })?;
Ok(())
}
async fn play_pause(&self) -> zbus::fdo::Result<()> {
self.player_tx
.command(PlayerCommand::TogglePlaying { callback: None })?;
Ok(())
}
async fn stop(&self) -> zbus::fdo::Result<()> {
self.player_tx.command(PlayerCommand::Stop)?;
Ok(())
}
async fn play(&self) -> zbus::fdo::Result<()> {
self.player_tx
.command(PlayerCommand::SetIsPlaying { is_playing: true })?;
Ok(())
}
async fn seek(&self, offset: Time) -> zbus::fdo::Result<()> {
let seconds = offset.as_micros() as f64 / 1_000_000.0;
self.player_tx.command(PlayerCommand::Seek {
seconds,
increment: true,
callback: None,
})?;
Ok(())
}
async fn set_position(&self, track_id: ConnectorId, position: Time) -> zbus::fdo::Result<()> {
let state = self.state.lock().unwrap();
if let Some(tracklist_track) = &state.currently_playing {
let id = tracklist_track.mpris_id();
if id == track_id {
let seconds = position.as_micros() as f64 / 1_000_000.0;
self.player_tx.command(PlayerCommand::Seek {
seconds,
increment: false,
callback: None,
})?;
}
}
Ok(())
}
async fn open_uri(&self, _uri: String) -> zbus::fdo::Result<()> {
Err(zbus::fdo::Error::ZBus(zbus::Error::Unsupported))
}
async fn playback_status(&self) -> zbus::fdo::Result<PlaybackStatus> {
Ok(self.state.lock().unwrap().playback_status)
}
async fn loop_status(&self) -> zbus::fdo::Result<LoopStatus> {
Ok(self.state.lock().unwrap().loop_status)
}
async fn set_loop_status(&self, loop_status: LoopStatus) -> zbus::Result<()> {
let loop_mode = match loop_status {
LoopStatus::None => LoopMode::None,
LoopStatus::Playlist => LoopMode::Loop,
LoopStatus::Track => LoopMode::RepeatTrack,
};
self.player_tx
.command(PlayerCommand::PlaylistSetLoopMode { loop_mode })?;
Ok(())
}
async fn rate(&self) -> zbus::fdo::Result<PlaybackRate> {
Ok(1.0)
}
async fn set_rate(&self, _rate: PlaybackRate) -> zbus::Result<()> {
Err(zbus::Error::Unsupported)
}
async fn shuffle(&self) -> zbus::fdo::Result<bool> {
Ok(self.state.lock().unwrap().shuffle)
}
async fn set_shuffle(&self, shuffle: bool) -> zbus::Result<()> {
let shuffle_mode = if shuffle {
ShuffleMode::Full
} else {
ShuffleMode::None
};
self.player_tx
.command(PlayerCommand::PlaylistSetShuffleMode { shuffle_mode })?;
Ok(())
}
async fn metadata(&self) -> zbus::fdo::Result<Metadata> {
Ok(self.state.lock().unwrap().metadata.clone())
}
async fn volume(&self) -> zbus::fdo::Result<Volume> {
Ok(self.state.lock().unwrap().volume)
}
async fn set_volume(&self, volume: Volume) -> zbus::Result<()> {
self.player_tx.command(PlayerCommand::SetVolume {
volume: volume as f32,
increment: false,
callback: None,
})?;
Ok(())
}
async fn position(&self) -> zbus::fdo::Result<Time> {
Ok(self.position())
}
async fn minimum_rate(&self) -> zbus::fdo::Result<PlaybackRate> {
Ok(1.0)
}
async fn maximum_rate(&self) -> zbus::fdo::Result<PlaybackRate> {
Ok(1.0)
}
async fn can_go_next(&self) -> zbus::fdo::Result<bool> {
Ok(true)
}
async fn can_go_previous(&self) -> zbus::fdo::Result<bool> {
Ok(true)
}
async fn can_play(&self) -> zbus::fdo::Result<bool> {
Ok(true)
}
async fn can_pause(&self) -> zbus::fdo::Result<bool> {
Ok(true)
}
async fn can_seek(&self) -> zbus::fdo::Result<bool> {
Ok(true)
}
async fn can_control(&self) -> zbus::fdo::Result<bool> {
Ok(true)
}
}
fn metadata_from_playing(playing: &ResolvedTrack) -> Result<Metadata, Box<dyn std::error::Error>> {
let mut metadata = Metadata::new();
let ResolvedTrack {
track,
position: _,
album,
album_artists,
artists,
} = playing;
let track_meta = playing.track.metadata();
let container = playing.track.container().unwrap();
let url = format!("file://{}", container.path().to_str().unwrap());
metadata.set_url(Some(url));
metadata.set_trackid(Some(playing.mpris_id()));
if let Some(album) = album {
let track_ref = album
.track_refs()
.iter()
.find(|t| t.id == track.id())
.unwrap();
metadata.set_disc_number(track_ref.disc_num.map(|v| v as i32));
metadata.set_track_number(track_ref.track_num.map(|v| v as i32));
metadata.set_album_artist(Some(
album_artists
.as_ref()
.expect("Album artists should be some if album is some")
.iter()
.map(Artist::name),
));
metadata.set_album(Some(&album.name));
};
metadata.set_artist(Some(artists.iter().map(Artist::name)));
if let Some(and_then) = track_meta
.lyric_data
.as_ref()
.and_then(LyricData::to_lyrics)
{
metadata.set_lyrics(Some(and_then));
}
if let Some(cover_art) = &track_meta.cover_art() {
let cover_file = cover_art.cache_file(512, 512)?;
let url = format!("file://{}", cover_file.to_str().unwrap());
metadata.set_art_url(Some(url));
}
if !track_meta.genre.is_empty() {
metadata.set_genre(Some(track_meta.genre.clone()));
}
metadata.set_title(Some(track_meta.safe_title()));
let length =
Time::from_micros((container.stream().duration().unwrap_or(0.0) * 1_000_000.0) as i64);
metadata.set_length(Some(length));
for (key, value) in &track_meta.other {
metadata.set(&format!("xesam:{key}"), Some(value.to_owned()));
}
Ok(metadata)
}