2026-04-03 00:09:03 +02:00
|
|
|
use cosmic::iced::futures::{self, SinkExt, Stream};
|
|
|
|
|
use cosmic::iced::{Subscription, stream};
|
|
|
|
|
use mpris_server::zbus::{Result, fdo};
|
2025-01-18 08:47:47 -07:00
|
|
|
use mpris_server::{
|
|
|
|
|
LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Property, RootInterface,
|
|
|
|
|
Server, Signal, Time, TrackId, Volume,
|
|
|
|
|
};
|
2026-04-03 00:09:03 +02:00
|
|
|
use std::any::TypeId;
|
|
|
|
|
use std::{future, process};
|
2025-11-09 21:33:47 -05:00
|
|
|
use tokio::sync::{Mutex, mpsc};
|
2025-01-18 08:47:47 -07:00
|
|
|
|
2026-04-03 00:09:03 +02:00
|
|
|
use crate::config::RepeatState;
|
|
|
|
|
use crate::{Message, MprisEvent, MprisMeta, MprisState};
|
2025-01-18 08:47:47 -07:00
|
|
|
|
|
|
|
|
impl MprisMeta {
|
|
|
|
|
fn metadata(&self) -> Metadata {
|
|
|
|
|
let mut meta = Metadata::builder()
|
|
|
|
|
//TODO: better track id
|
|
|
|
|
.trackid(
|
|
|
|
|
mpris_server::TrackId::try_from(format!(
|
|
|
|
|
"/com/system76/CosmicPlayer/pid{}/TrackList/0",
|
|
|
|
|
process::id()
|
|
|
|
|
))
|
|
|
|
|
.unwrap(),
|
|
|
|
|
)
|
|
|
|
|
.length(Time::from_micros(self.duration_micros));
|
|
|
|
|
if let Some(url) = &self.url_opt {
|
|
|
|
|
meta = meta.url(url.clone());
|
|
|
|
|
}
|
|
|
|
|
if !self.album.is_empty() {
|
|
|
|
|
meta = meta.album(&self.album);
|
|
|
|
|
}
|
|
|
|
|
if let Some(album_art) = &self.album_art_opt {
|
|
|
|
|
meta = meta.art_url(album_art.clone());
|
|
|
|
|
}
|
|
|
|
|
if !self.artists.is_empty() {
|
|
|
|
|
meta = meta.artist(&self.artists);
|
|
|
|
|
}
|
|
|
|
|
//TODO: content_created
|
|
|
|
|
if !self.title.is_empty() {
|
|
|
|
|
meta = meta.title(&self.title);
|
|
|
|
|
}
|
|
|
|
|
//TODO .disc_number(self.disc_number)
|
|
|
|
|
if self.track_number > 0 {
|
|
|
|
|
meta = meta.track_number(self.track_number);
|
|
|
|
|
}
|
|
|
|
|
//TODO: track count?
|
|
|
|
|
//TODO: more keys, see https://docs.rs/mpris-server/0.8.1/mpris_server/builder/struct.MetadataBuilder.html
|
|
|
|
|
meta.build()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl MprisState {
|
|
|
|
|
fn playback_status(&self) -> PlaybackStatus {
|
|
|
|
|
if self.paused {
|
|
|
|
|
PlaybackStatus::Paused
|
|
|
|
|
} else {
|
|
|
|
|
PlaybackStatus::Playing
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-27 02:02:46 -08:00
|
|
|
|
|
|
|
|
fn loop_status(&self) -> LoopStatus {
|
2025-11-27 20:43:40 -08:00
|
|
|
match self.repeat_state {
|
|
|
|
|
RepeatState::Disabled => LoopStatus::None,
|
|
|
|
|
RepeatState::Track => LoopStatus::Track,
|
2025-11-27 02:02:46 -08:00
|
|
|
}
|
|
|
|
|
}
|
2025-01-18 08:47:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct Player {
|
|
|
|
|
msg_tx: Mutex<futures::channel::mpsc::Sender<Message>>,
|
|
|
|
|
meta: Mutex<MprisMeta>,
|
|
|
|
|
state: Mutex<MprisState>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Player {
|
|
|
|
|
async fn message(&self, message: Message) -> fdo::Result<()> {
|
|
|
|
|
self.msg_tx
|
|
|
|
|
.lock()
|
|
|
|
|
.await
|
|
|
|
|
.send(message)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|err| fdo::Error::Failed(err.to_string()))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RootInterface for Player {
|
|
|
|
|
async fn raise(&self) -> fdo::Result<()> {
|
|
|
|
|
log::info!("Raise");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn quit(&self) -> fdo::Result<()> {
|
|
|
|
|
log::info!("Quit");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn can_quit(&self) -> fdo::Result<bool> {
|
|
|
|
|
log::info!("CanQuit");
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn fullscreen(&self) -> fdo::Result<bool> {
|
|
|
|
|
log::info!("Fullscreen");
|
|
|
|
|
let state = self.state.lock().await;
|
|
|
|
|
Ok(state.fullscreen)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn set_fullscreen(&self, fullscreen: bool) -> Result<()> {
|
|
|
|
|
log::info!("SetFullscreen({})", fullscreen);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn can_set_fullscreen(&self) -> fdo::Result<bool> {
|
|
|
|
|
log::info!("CanSetFullscreen");
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn can_raise(&self) -> fdo::Result<bool> {
|
|
|
|
|
log::info!("CanRaise");
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn has_track_list(&self) -> fdo::Result<bool> {
|
|
|
|
|
log::info!("HasTrackList");
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn identity(&self) -> fdo::Result<String> {
|
|
|
|
|
log::info!("Identity");
|
|
|
|
|
Ok("COSMIC Player".to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn desktop_entry(&self) -> fdo::Result<String> {
|
|
|
|
|
log::info!("DesktopEntry");
|
|
|
|
|
Ok("com.system76.CosmicPlayer".to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn supported_uri_schemes(&self) -> fdo::Result<Vec<String>> {
|
|
|
|
|
log::info!("SupportedUriSchemes");
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn supported_mime_types(&self) -> fdo::Result<Vec<String>> {
|
|
|
|
|
log::info!("SupportedMimeTypes");
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PlayerInterface for Player {
|
|
|
|
|
async fn next(&self) -> fdo::Result<()> {
|
|
|
|
|
log::info!("Next");
|
2026-05-07 18:53:08 -04:00
|
|
|
self.message(Message::PlayNext).await
|
2025-01-18 08:47:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn previous(&self) -> fdo::Result<()> {
|
|
|
|
|
log::info!("Previous");
|
2026-05-07 18:53:08 -04:00
|
|
|
self.message(Message::PlayPrev).await
|
2025-01-18 08:47:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn pause(&self) -> fdo::Result<()> {
|
|
|
|
|
log::info!("Pause");
|
|
|
|
|
self.message(Message::Pause).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn play_pause(&self) -> fdo::Result<()> {
|
|
|
|
|
log::info!("PlayPause");
|
|
|
|
|
self.message(Message::PlayPause).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn stop(&self) -> fdo::Result<()> {
|
|
|
|
|
log::info!("Stop");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn play(&self) -> fdo::Result<()> {
|
|
|
|
|
log::info!("Play");
|
|
|
|
|
self.message(Message::Play).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn seek(&self, offset: Time) -> fdo::Result<()> {
|
|
|
|
|
log::info!("Seek({:?})", offset);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn set_position(&self, track_id: TrackId, position: Time) -> fdo::Result<()> {
|
|
|
|
|
log::info!("SetPosition({}, {:?})", track_id, position);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn open_uri(&self, uri: String) -> fdo::Result<()> {
|
|
|
|
|
log::info!("OpenUri({})", uri);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn playback_status(&self) -> fdo::Result<PlaybackStatus> {
|
|
|
|
|
log::info!("PlaybackStatus");
|
|
|
|
|
let state = self.state.lock().await;
|
|
|
|
|
Ok(state.playback_status())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn loop_status(&self) -> fdo::Result<LoopStatus> {
|
|
|
|
|
log::info!("LoopStatus");
|
2025-11-27 02:02:46 -08:00
|
|
|
let state = self.state.lock().await;
|
|
|
|
|
Ok(state.loop_status())
|
2025-01-18 08:47:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn set_loop_status(&self, loop_status: LoopStatus) -> Result<()> {
|
|
|
|
|
log::info!("SetLoopStatus({})", loop_status);
|
2025-11-27 20:43:40 -08:00
|
|
|
let repeat_state = match loop_status {
|
|
|
|
|
LoopStatus::None => RepeatState::Disabled,
|
2025-11-27 21:03:57 -08:00
|
|
|
LoopStatus::Track | LoopStatus::Playlist => RepeatState::Track,
|
2025-11-27 02:02:46 -08:00
|
|
|
};
|
|
|
|
|
self.message(Message::RepeatToggled(repeat_state)).await?;
|
2025-01-18 08:47:47 -07:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn rate(&self) -> fdo::Result<PlaybackRate> {
|
|
|
|
|
log::info!("Rate");
|
|
|
|
|
Ok(1.0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn set_rate(&self, rate: PlaybackRate) -> Result<()> {
|
|
|
|
|
log::info!("SetRate({})", rate);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn shuffle(&self) -> fdo::Result<bool> {
|
|
|
|
|
log::info!("Shuffle");
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn set_shuffle(&self, shuffle: bool) -> Result<()> {
|
|
|
|
|
log::info!("SetShuffle({})", shuffle);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn metadata(&self) -> fdo::Result<Metadata> {
|
|
|
|
|
log::info!("Metadata");
|
|
|
|
|
let meta = self.meta.lock().await;
|
|
|
|
|
Ok(meta.metadata())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn volume(&self) -> fdo::Result<Volume> {
|
|
|
|
|
log::info!("Volume");
|
|
|
|
|
let state = self.state.lock().await;
|
|
|
|
|
Ok(state.volume)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn set_volume(&self, volume: Volume) -> Result<()> {
|
|
|
|
|
log::info!("SetVolume({})", volume);
|
|
|
|
|
self.message(Message::AudioVolume(volume)).await?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn position(&self) -> fdo::Result<Time> {
|
|
|
|
|
log::info!("Position");
|
|
|
|
|
let state = self.state.lock().await;
|
|
|
|
|
Ok(Time::from_micros(state.position_micros))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn minimum_rate(&self) -> fdo::Result<PlaybackRate> {
|
|
|
|
|
log::info!("MinimumRate");
|
|
|
|
|
Ok(1.0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn maximum_rate(&self) -> fdo::Result<PlaybackRate> {
|
|
|
|
|
log::info!("MaximumRate");
|
|
|
|
|
Ok(1.0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn can_go_next(&self) -> fdo::Result<bool> {
|
|
|
|
|
log::info!("CanGoNext");
|
2026-05-07 18:53:08 -04:00
|
|
|
Ok(true)
|
2025-01-18 08:47:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn can_go_previous(&self) -> fdo::Result<bool> {
|
|
|
|
|
log::info!("CanGoPrevious");
|
2026-05-07 18:53:08 -04:00
|
|
|
Ok(true)
|
2025-01-18 08:47:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn can_play(&self) -> fdo::Result<bool> {
|
|
|
|
|
log::info!("CanPlay");
|
|
|
|
|
Ok(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn can_pause(&self) -> fdo::Result<bool> {
|
|
|
|
|
log::info!("CanPause");
|
|
|
|
|
Ok(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn can_seek(&self) -> fdo::Result<bool> {
|
|
|
|
|
log::info!("CanSeek");
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn can_control(&self) -> fdo::Result<bool> {
|
|
|
|
|
log::info!("CanControl");
|
|
|
|
|
Ok(true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*TODO: implement mpris tracklist
|
|
|
|
|
impl TrackListInterface for Player {
|
|
|
|
|
async fn get_tracks_metadata(&self, track_ids: Vec<TrackId>) -> fdo::Result<Vec<Metadata>> {
|
|
|
|
|
log::info!("GetTracksMetadata({:?})", track_ids);
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn add_track(
|
|
|
|
|
&self,
|
|
|
|
|
uri: Uri,
|
|
|
|
|
after_track: TrackId,
|
|
|
|
|
set_as_current: bool,
|
|
|
|
|
) -> fdo::Result<()> {
|
|
|
|
|
log::info!("AddTrack({}, {}, {})", uri, after_track, set_as_current);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn remove_track(&self, track_id: TrackId) -> fdo::Result<()> {
|
|
|
|
|
log::info!("RemoveTrack({})", track_id);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn go_to(&self, track_id: TrackId) -> fdo::Result<()> {
|
|
|
|
|
log::info!("GoTo({})", track_id);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn tracks(&self) -> fdo::Result<Vec<TrackId>> {
|
|
|
|
|
log::info!("Tracks");
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn can_edit_tracks(&self) -> fdo::Result<bool> {
|
|
|
|
|
log::info!("CanEditTracks");
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/*TODO: implement mpris playlists
|
|
|
|
|
impl PlaylistsInterface for Player {
|
|
|
|
|
async fn activate_playlist(&self, playlist_id: PlaylistId) -> fdo::Result<()> {
|
|
|
|
|
log::info!("ActivatePlaylist({})", playlist_id);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn get_playlists(
|
|
|
|
|
&self,
|
|
|
|
|
index: u32,
|
|
|
|
|
max_count: u32,
|
|
|
|
|
order: PlaylistOrdering,
|
|
|
|
|
reverse_order: bool,
|
|
|
|
|
) -> fdo::Result<Vec<Playlist>> {
|
|
|
|
|
log::info!(
|
|
|
|
|
"GetPlaylists({}, {}, {}, {})",
|
|
|
|
|
index, max_count, order, reverse_order
|
|
|
|
|
);
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn playlist_count(&self) -> fdo::Result<u32> {
|
|
|
|
|
log::info!("PlaylistCount");
|
|
|
|
|
Ok(0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn orderings(&self) -> fdo::Result<Vec<PlaylistOrdering>> {
|
|
|
|
|
log::info!("Orderings");
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn active_playlist(&self) -> fdo::Result<Option<Playlist>> {
|
|
|
|
|
log::info!("ActivePlaylist");
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
*/
|
|
|
|
|
|
2026-03-20 16:17:24 -04:00
|
|
|
fn watcher_stream() -> impl Stream<Item = Message> {
|
|
|
|
|
stream::channel(
|
|
|
|
|
5,
|
|
|
|
|
move |mut msg_tx: futures::channel::mpsc::Sender<Message>| async move {
|
2025-01-18 08:47:47 -07:00
|
|
|
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
|
2026-03-20 16:17:24 -04:00
|
|
|
let meta: MprisMeta = MprisMeta::default();
|
|
|
|
|
let state: MprisState = MprisState::default();
|
2025-01-18 08:47:47 -07:00
|
|
|
msg_tx
|
|
|
|
|
.send(Message::MprisChannel(meta.clone(), state.clone(), event_tx))
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
match Server::new(
|
|
|
|
|
&format!("org.mpris.MediaPlayer2.cosmic-player.pid{}", process::id()),
|
|
|
|
|
Player {
|
|
|
|
|
msg_tx: Mutex::new(msg_tx),
|
|
|
|
|
meta: Mutex::new(meta),
|
|
|
|
|
state: Mutex::new(state),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(server) => {
|
|
|
|
|
log::info!("running mpris server");
|
|
|
|
|
while let Some(event) = event_rx.recv().await {
|
|
|
|
|
let mut props = Vec::new();
|
|
|
|
|
let mut sigs = Vec::new();
|
|
|
|
|
match event {
|
|
|
|
|
MprisEvent::Meta(new) => {
|
|
|
|
|
let mut old = server.imp().meta.lock().await;
|
|
|
|
|
let new_metadata = new.metadata();
|
|
|
|
|
if new_metadata != old.metadata() {
|
|
|
|
|
props.push(Property::Metadata(new_metadata));
|
|
|
|
|
}
|
|
|
|
|
*old = new;
|
|
|
|
|
}
|
|
|
|
|
MprisEvent::State(new) => {
|
|
|
|
|
let mut old = server.imp().state.lock().await;
|
|
|
|
|
if new.fullscreen != old.fullscreen {
|
|
|
|
|
props.push(Property::Fullscreen(new.fullscreen));
|
|
|
|
|
}
|
|
|
|
|
let new_playback_status = new.playback_status();
|
|
|
|
|
if new_playback_status != old.playback_status() {
|
|
|
|
|
props.push(Property::PlaybackStatus(new_playback_status));
|
|
|
|
|
}
|
|
|
|
|
if new.volume != old.volume {
|
|
|
|
|
props.push(Property::Volume(new.volume));
|
|
|
|
|
}
|
|
|
|
|
if new.position_micros != old.position_micros {
|
|
|
|
|
sigs.push(Signal::Seeked {
|
|
|
|
|
position: Time::from_micros(new.position_micros),
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-11-27 02:02:46 -08:00
|
|
|
let new_loop_status = new.loop_status();
|
|
|
|
|
if new_loop_status != old.loop_status() {
|
|
|
|
|
props.push(Property::LoopStatus(new_loop_status));
|
|
|
|
|
}
|
2025-01-18 08:47:47 -07:00
|
|
|
*old = new;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !props.is_empty() {
|
|
|
|
|
let _ = server.properties_changed(props).await;
|
|
|
|
|
}
|
|
|
|
|
for sig in sigs {
|
|
|
|
|
let _ = server.emit(sig).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
future::pending().await
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::warn!("failed to start mpris server: {err}");
|
|
|
|
|
future::pending().await
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-03-20 16:17:24 -04:00
|
|
|
|
|
|
|
|
#[cold]
|
|
|
|
|
pub fn subscription() -> Subscription<Message> {
|
|
|
|
|
struct MprisSubscription;
|
|
|
|
|
Subscription::run_with(TypeId::of::<MprisSubscription>(), |_| watcher_stream())
|
|
|
|
|
}
|