wip bring back mpris
This commit is contained in:
parent
67e4eb9ff2
commit
ce1aed758f
3 changed files with 405 additions and 6 deletions
|
@ -1,3 +1,6 @@
|
||||||
|
mod player;
|
||||||
|
pub use player::Player;
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use gtk::glib;
|
use gtk::glib;
|
||||||
use tracing::{event, Level};
|
use tracing::{event, Level};
|
||||||
|
|
390
src/mpris/player.rs
Normal file
390
src/mpris/player.rs
Normal file
|
@ -0,0 +1,390 @@
|
||||||
|
use crate::{model::Song, ui::Window};
|
||||||
|
use adw::prelude::*;
|
||||||
|
use gtk::glib::SendWeakRef;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tracing::{event, Level};
|
||||||
|
use zbus::object_server::InterfaceRef;
|
||||||
|
use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value};
|
||||||
|
|
||||||
|
const MICROSECONDS: f64 = 1e6; // in a second
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MetadataMap {
|
||||||
|
// mpris
|
||||||
|
track_id: Option<OwnedObjectPath>,
|
||||||
|
length: Option<i64>,
|
||||||
|
art_url: Option<url::Url>,
|
||||||
|
// xesam
|
||||||
|
album: Option<String>,
|
||||||
|
//album_artist: Option<Vec<String>>,
|
||||||
|
artist: Option<Vec<String>>,
|
||||||
|
//as_text: Option<String>,
|
||||||
|
//audio_bpm: Option<i32>,
|
||||||
|
//auto_rating: Option<f32>,
|
||||||
|
//comment: Option<Vec<String>>,
|
||||||
|
//composer: Option<Vec<String>>,
|
||||||
|
content_created: Option<chrono::NaiveDateTime>,
|
||||||
|
//disc_number: Option<i32>,
|
||||||
|
//first_used: Option<chrono::DateTime>,
|
||||||
|
genre: Option<Vec<String>>,
|
||||||
|
//last_used: Option<chrono::DateTime>,
|
||||||
|
//lyricist: Option<Vec<String>>,
|
||||||
|
title: Option<String>,
|
||||||
|
track_number: Option<i32>,
|
||||||
|
//url: Option<String>,
|
||||||
|
//use_count: Option<String>,
|
||||||
|
user_rating: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MetadataMap {
|
||||||
|
fn from_playbin_song(song: Option<&Song>) -> Self {
|
||||||
|
song.map(|song| MetadataMap {
|
||||||
|
// use a unique growing counter to identify tracks
|
||||||
|
track_id: Some({
|
||||||
|
format!("/eu/callcc/audrey/Track/{}", song.counter())
|
||||||
|
.try_into()
|
||||||
|
.unwrap()
|
||||||
|
}),
|
||||||
|
length: Some(song.duration() * MICROSECONDS as i64),
|
||||||
|
//art_url: Some(song.cover_art_url()), // FIXME: this would leak credentials
|
||||||
|
album: Some(song.album()),
|
||||||
|
artist: Some(vec![song.artist()]),
|
||||||
|
//content_created: song.year().map(|year| chrono::NaiveDate::from_yo_opt(year, 1).unwrap()), // FIXME: replace this unwrap with Some(Err) -> None
|
||||||
|
//genre: Some(song.genre.iter().collect()),
|
||||||
|
title: Some(song.title()),
|
||||||
|
//track_number: song.track().map(|u| u as i32),
|
||||||
|
//user_rating: Some(if song.starred().is_none() { 0.0 } else { 1.0 }),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_hash_map(&self) -> HashMap<&'static str, OwnedValue> {
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
if let Some(track_id) = &self.track_id {
|
||||||
|
map.insert(
|
||||||
|
"mpris:trackid",
|
||||||
|
Value::new(track_id.as_ref()).try_into().unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(art_url) = &self.art_url {
|
||||||
|
map.insert(
|
||||||
|
"mpris:artUrl",
|
||||||
|
Value::new(art_url.to_string()).try_into().unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(length) = &self.length {
|
||||||
|
map.insert("mpris:length", Value::new(length).try_into().unwrap());
|
||||||
|
}
|
||||||
|
if let Some(album) = &self.album {
|
||||||
|
map.insert("xesam:album", Value::new(album).try_into().unwrap());
|
||||||
|
}
|
||||||
|
if let Some(artist) = &self.artist {
|
||||||
|
map.insert("xesam:artist", Value::new(artist).try_into().unwrap());
|
||||||
|
}
|
||||||
|
if let Some(content_created) = &self.content_created {
|
||||||
|
map.insert(
|
||||||
|
"xesam:contentCreated",
|
||||||
|
Value::new(content_created.format("%+").to_string())
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(genre) = &self.genre {
|
||||||
|
map.insert("xesam:genre", Value::new(genre).try_into().unwrap());
|
||||||
|
}
|
||||||
|
if let Some(track_number) = self.track_number {
|
||||||
|
map.insert(
|
||||||
|
"xesam:trackNumber",
|
||||||
|
Value::new(track_number).try_into().unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(title) = &self.title {
|
||||||
|
map.insert("xesam:title", Value::new(title).try_into().unwrap());
|
||||||
|
}
|
||||||
|
if let Some(user_rating) = self.user_rating {
|
||||||
|
map.insert(
|
||||||
|
"xesam:userRating",
|
||||||
|
Value::new(user_rating).try_into().unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Player {
|
||||||
|
metadata: MetadataMap,
|
||||||
|
window: SendWeakRef<Window>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Player {
|
||||||
|
pub async fn new(
|
||||||
|
object_server: &zbus::ObjectServer,
|
||||||
|
playbin: &Window,
|
||||||
|
) -> Result<InterfaceRef<Player>, zbus::Error> {
|
||||||
|
let player = Self {
|
||||||
|
metadata: MetadataMap::from_playbin_song(None),
|
||||||
|
window: playbin.downgrade().into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
object_server.at("/org/mpris/MediaPlayer2", player).await?;
|
||||||
|
|
||||||
|
Ok(object_server
|
||||||
|
.interface::<_, Self>("/org/mpris/MediaPlayer2")
|
||||||
|
.await?)
|
||||||
|
|
||||||
|
/*
|
||||||
|
playbin.connect_new_track(glib::clone!(
|
||||||
|
#[strong]
|
||||||
|
player_ref,
|
||||||
|
move |_, song| {
|
||||||
|
let metadata = MetadataMap::from_playbin_song(Some(song));
|
||||||
|
|
||||||
|
let player_ref = player_ref.clone();
|
||||||
|
glib::spawn_future_local(async move {
|
||||||
|
let mut player = player_ref.get_mut().await;
|
||||||
|
player.metadata = metadata;
|
||||||
|
player
|
||||||
|
.metadata_changed(player_ref.signal_emitter())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
playbin.connect_seeked(glib::clone!(
|
||||||
|
#[strong]
|
||||||
|
player_ref,
|
||||||
|
move |_, position| {
|
||||||
|
let player_ref = player_ref.clone();
|
||||||
|
glib::spawn_future_local(async move {
|
||||||
|
player_ref
|
||||||
|
.seeked((position * MICROSECONDS) as i64)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
playbin.connect_notify_future_local(
|
||||||
|
"play-queue-length",
|
||||||
|
glib::clone!(
|
||||||
|
#[strong]
|
||||||
|
player_ref,
|
||||||
|
move |_, _| {
|
||||||
|
let player_ref = player_ref.clone();
|
||||||
|
async move {
|
||||||
|
let player = player_ref.get_mut().await;
|
||||||
|
// properties that depend on the play queue length
|
||||||
|
player
|
||||||
|
.can_go_next_changed(player_ref.signal_emitter())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
player
|
||||||
|
.can_go_previous_changed(player_ref.signal_emitter())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
player
|
||||||
|
.can_play_changed(player_ref.signal_emitter())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
playbin.connect_notify_future_local(
|
||||||
|
"state",
|
||||||
|
glib::clone!(
|
||||||
|
#[strong]
|
||||||
|
player_ref,
|
||||||
|
move |_, _| {
|
||||||
|
let player_ref = player_ref.clone();
|
||||||
|
async move {
|
||||||
|
let player = player_ref.get_mut().await;
|
||||||
|
// properties that depend on the playbin state
|
||||||
|
player
|
||||||
|
.playback_status_changed(player_ref.signal_emitter())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
playbin.connect_notify_future_local(
|
||||||
|
"volume",
|
||||||
|
glib::clone!(
|
||||||
|
#[strong]
|
||||||
|
player_ref,
|
||||||
|
move |_, _| {
|
||||||
|
let player_ref = player_ref.clone();
|
||||||
|
async move {
|
||||||
|
let player = player_ref.get_mut().await;
|
||||||
|
player
|
||||||
|
.volume_changed(player_ref.signal_emitter())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window(&self) -> Window {
|
||||||
|
self.window.upgrade().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus::interface(name = "org.mpris.MediaPlayer2.Player")]
|
||||||
|
impl Player {
|
||||||
|
fn next(&self) -> zbus::fdo::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous(&self) -> zbus::fdo::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pause(&self) -> zbus::fdo::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play_pause(&self) -> zbus::fdo::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&self) -> zbus::fdo::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play(&self) -> zbus::fdo::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seek(&self, _offset: i64) -> zbus::fdo::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_position(&self, _track_id: ObjectPath<'_>, _position: i64) -> zbus::fdo::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_uri(&self, _s: String) -> zbus::fdo::Result<()> {
|
||||||
|
Err(zbus::fdo::Error::NotSupported("OpenUri".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn playback_status(&self) -> String {
|
||||||
|
if self.window().idle_active() {
|
||||||
|
"Stopped"
|
||||||
|
} else if self.window().pause() {
|
||||||
|
"Paused"
|
||||||
|
} else {
|
||||||
|
"Playing"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn loop_status(&self) -> zbus::fdo::Result<String> {
|
||||||
|
Ok("None".into()) // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn set_loop_status(&self, _loop_status: &str) -> zbus::Result<()> {
|
||||||
|
Err(zbus::fdo::Error::NotSupported("setting LoopStatus".into()).into()) // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn rate(&self) -> zbus::fdo::Result<f64> {
|
||||||
|
Ok(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn set_rate(&self, _rate: f64) -> zbus::Result<()> {
|
||||||
|
// A value of 0.0 should not be set by the client. If it is, the media player should act as though Pause was called.
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
// FIXME: https://github.com/dbus2/zbus/issues/992
|
||||||
|
fn shuffle(&self) -> zbus::fdo::Result<bool> {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
// FIXME: see above
|
||||||
|
fn set_shuffle(&self, _shuffle: bool) -> zbus::Result<()> {
|
||||||
|
Err(zbus::fdo::Error::NotSupported("setting Shuffle".into()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn metadata(&self) -> HashMap<&'static str, OwnedValue> {
|
||||||
|
self.metadata.as_hash_map()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn volume(&self) -> f64 {
|
||||||
|
self.window().volume() as f64 / 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn set_volume(&self, _volume: f64) -> zbus::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property(emits_changed_signal = "false"))]
|
||||||
|
fn position(&self) -> i64 {
|
||||||
|
(self.window().time_pos() * MICROSECONDS as f64) as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
true // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn can_go_previous(&self) -> bool {
|
||||||
|
true // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn can_play(&self) -> bool {
|
||||||
|
true // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn can_pause(&self) -> bool {
|
||||||
|
true // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn can_seek(&self) -> bool {
|
||||||
|
true // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property(emits_changed_signal = "const"))]
|
||||||
|
fn can_control(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Player {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
event!(Level::DEBUG, "dropping MprisPlayer");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
mod imp {
|
mod imp {
|
||||||
use crate::model::Song;
|
use crate::model::Song;
|
||||||
use crate::mpv;
|
use crate::{mpris, mpv};
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use adw::subclass::prelude::*;
|
use adw::subclass::prelude::*;
|
||||||
use glib::subclass::InitializingObject;
|
use glib::subclass::InitializingObject;
|
||||||
|
@ -9,6 +9,7 @@ mod imp {
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use tracing::{event, span, Level};
|
use tracing::{event, span, Level};
|
||||||
|
use zbus::object_server::InterfaceRef;
|
||||||
|
|
||||||
#[derive(gtk::CompositeTemplate, glib::Properties)]
|
#[derive(gtk::CompositeTemplate, glib::Properties)]
|
||||||
#[template(resource = "/eu/callcc/audrey/window.ui")]
|
#[template(resource = "/eu/callcc/audrey/window.ui")]
|
||||||
|
@ -64,6 +65,8 @@ mod imp {
|
||||||
|
|
||||||
buffering_timeout: Cell<Option<glib::SourceId>>,
|
buffering_timeout: Cell<Option<glib::SourceId>>,
|
||||||
time_pos_notify_timeout: Cell<Option<glib::SourceId>>,
|
time_pos_notify_timeout: Cell<Option<glib::SourceId>>,
|
||||||
|
|
||||||
|
mpris_player: RefCell<Option<InterfaceRef<mpris::Player>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Window {
|
impl Default for Window {
|
||||||
|
@ -116,6 +119,8 @@ mod imp {
|
||||||
|
|
||||||
buffering_timeout: Default::default(),
|
buffering_timeout: Default::default(),
|
||||||
time_pos_notify_timeout: Default::default(),
|
time_pos_notify_timeout: Default::default(),
|
||||||
|
|
||||||
|
mpris_player: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,11 +223,12 @@ mod imp {
|
||||||
crate::Mpris::setup(conn.object_server(), &window)
|
crate::Mpris::setup(conn.object_server(), &window)
|
||||||
.await
|
.await
|
||||||
.expect("could not serve mpris");
|
.expect("could not serve mpris");
|
||||||
/*
|
|
||||||
crate::mpris::Player::setup(conn.object_server(), &window.imp().playbin)
|
window.imp().mpris_player.replace(Some(
|
||||||
|
mpris::Player::new(conn.object_server(), &window)
|
||||||
.await
|
.await
|
||||||
.expect("could not serve mpris player");
|
.expect("could not serve mpris player"),
|
||||||
FIXME */
|
));
|
||||||
|
|
||||||
// always set up handlers before requesting service name
|
// always set up handlers before requesting service name
|
||||||
conn.request_name("org.mpris.MediaPlayer2.audrey")
|
conn.request_name("org.mpris.MediaPlayer2.audrey")
|
||||||
|
|
Loading…
Reference in a new issue