Merge pull request 'zbus music player' (#44) from zbus-music-player into main
Reviewed-on: #44
This commit is contained in:
commit
2144a2ab3d
|
@ -11,9 +11,11 @@ required-features = ["bin"]
|
|||
|
||||
[dependencies]
|
||||
canary-magpie = { path = "../magpie", optional = true }
|
||||
mpris = { version = "2.0.0-rc3", optional = true }
|
||||
futures-util = { version = "0.3", optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
smol = { version = "1.2", optional = true }
|
||||
zbus = { version = "3.5", optional = true }
|
||||
|
||||
[features]
|
||||
bin = ["dep:canary-magpie", "dep:mpris"]
|
||||
bin = ["dep:canary-magpie", "dep:futures-util", "dep:smol", "dep:zbus"]
|
||||
|
|
|
@ -34,9 +34,6 @@ pub enum LoopStatus {
|
|||
pub struct ProgressChanged {
|
||||
/// Current position into the track in seconds.
|
||||
pub position: f32,
|
||||
|
||||
/// Length of the current track in seconds.
|
||||
pub length: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
@ -48,7 +45,7 @@ pub struct AlbumInfo {
|
|||
pub artists: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TrackInfo {
|
||||
/// The title of the current track.
|
||||
pub title: Option<String>,
|
||||
|
@ -58,6 +55,9 @@ pub struct TrackInfo {
|
|||
|
||||
/// The optional track number on the disc the album the track appears on.
|
||||
pub track_number: Option<i32>,
|
||||
|
||||
/// Length of the track in seconds.
|
||||
pub length: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
|
|
|
@ -1,60 +1,64 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use canary_music_player::*;
|
||||
use canary_magpie::client::MagpieClient;
|
||||
use canary_magpie::protocol::{CreatePanel, MagpieServerMsg};
|
||||
use canary_music_player::*;
|
||||
use mpris::PlayerFinder;
|
||||
|
||||
pub struct MetadataTracker {
|
||||
pub mod mpris;
|
||||
|
||||
use mpris::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Metadata {
|
||||
pub album: AlbumInfo,
|
||||
pub track: TrackInfo,
|
||||
}
|
||||
|
||||
impl From<&mpris::Metadata> for MetadataTracker {
|
||||
fn from(metadata: &mpris::Metadata) -> Self {
|
||||
impl<'a> From<MetadataMap<'a>> for Metadata {
|
||||
fn from(map: MetadataMap<'a>) -> Self {
|
||||
let album = AlbumInfo {
|
||||
title: metadata.album_name().map(ToString::to_string),
|
||||
artists: metadata
|
||||
.album_artists()
|
||||
.unwrap_or(Vec::new())
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
title: map
|
||||
.get("xesam:album")
|
||||
.and_then(|v| TryFrom::try_from(v).ok()),
|
||||
artists: map
|
||||
.get("xesam:albumArtist")
|
||||
.cloned()
|
||||
.and_then(|v| TryFrom::try_from(v).ok())
|
||||
.unwrap_or(Vec::new()),
|
||||
};
|
||||
|
||||
let track = TrackInfo {
|
||||
title: metadata.title().map(ToString::to_string),
|
||||
artists: metadata
|
||||
.artists()
|
||||
.unwrap_or(Vec::new())
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
track_number: metadata.track_number(),
|
||||
title: map
|
||||
.get("xesam:title")
|
||||
.and_then(|v| TryFrom::try_from(v).ok()),
|
||||
artists: map
|
||||
.get("xesam:artist")
|
||||
.cloned()
|
||||
.and_then(|v| TryFrom::try_from(v).ok())
|
||||
.unwrap_or(Vec::new()),
|
||||
track_number: map
|
||||
.get("xesam:trackNumber")
|
||||
.and_then(|v| TryFrom::try_from(v).ok()),
|
||||
length: map
|
||||
.get("xesam:length")
|
||||
.and_then(|v| i64::try_from(v).ok())
|
||||
.map(|us| us as f32 / 1_000_000.0), // 1,000,000 microseconds in a second
|
||||
};
|
||||
|
||||
Self { album, track }
|
||||
}
|
||||
}
|
||||
|
||||
impl MetadataTracker {
|
||||
pub fn new(magpie: &mut MagpieClient, metadata: &mpris::Metadata) -> Self {
|
||||
impl Metadata {
|
||||
pub fn new(magpie: &mut MagpieClient, metadata: MetadataMap) -> Self {
|
||||
let new: Self = metadata.into();
|
||||
magpie.send_json_message(0, &InMsg::AlbumChanged(new.album.clone()));
|
||||
magpie.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
|
||||
magpie.send_json_message(
|
||||
0,
|
||||
&InMsg::ProgressChanged(ProgressChanged {
|
||||
position: 0.0,
|
||||
length: metadata.length().map(|l| l.as_secs_f32()),
|
||||
}),
|
||||
);
|
||||
new
|
||||
}
|
||||
|
||||
pub fn update(&mut self, messenger: &mut MagpieClient, metadata: &mpris::Metadata) {
|
||||
pub fn update(&mut self, messenger: &mut MagpieClient, metadata: MetadataMap) {
|
||||
let new: Self = metadata.into();
|
||||
|
||||
if self.album != new.album {
|
||||
|
@ -63,19 +67,58 @@ impl MetadataTracker {
|
|||
|
||||
if self.track != new.track {
|
||||
messenger.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
|
||||
messenger.send_json_message(
|
||||
0,
|
||||
&InMsg::ProgressChanged(ProgressChanged {
|
||||
position: 0.0,
|
||||
length: metadata.length().map(|l| l.as_secs_f32()),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
*self = new;
|
||||
}
|
||||
}
|
||||
|
||||
async fn player_main(
|
||||
player: &PlayerProxy<'_>,
|
||||
magpie: &mut MagpieClient,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use futures_util::StreamExt;
|
||||
let mut playback_status = player.receive_playback_status_changed().await.fuse();
|
||||
let mut metadata_tracker = player.receive_metadata_changed().await.fuse();
|
||||
|
||||
let mut metadata = Metadata::new(magpie, player.metadata().await?);
|
||||
|
||||
loop {
|
||||
futures_util::select! {
|
||||
// TODO also update volume, shuffle status, and loop status
|
||||
status = playback_status.next() => {
|
||||
let status = match status {
|
||||
Some(v) => v,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let status = status.get().await?;
|
||||
let status = match status.as_str() {
|
||||
"Playing" => Some(PlaybackStatus::Playing),
|
||||
"Paused" => Some(PlaybackStatus::Paused),
|
||||
"Stopped" => Some(PlaybackStatus::Stopped),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(status) = status {
|
||||
magpie.send_json_message(0, &InMsg::PlaybackStatusChanged(status));
|
||||
}
|
||||
}
|
||||
new_metadata = metadata_tracker.next() => {
|
||||
let new_metadata = match new_metadata {
|
||||
Some(v) => v,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let new_metadata = new_metadata.get().await?;
|
||||
metadata.update(magpie, new_metadata);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let module_path = args
|
||||
|
@ -83,105 +126,67 @@ fn main() {
|
|||
.expect("Please pass a path to a Canary script!")
|
||||
.to_owned();
|
||||
|
||||
let player_finder = PlayerFinder::new().expect("Could not connect to D-Bus");
|
||||
|
||||
let mut magpie = MagpieClient::new().unwrap();
|
||||
let protocol = "tebibyte-media.desktop.music-player-controller".to_string();
|
||||
let script = std::path::PathBuf::from(&module_path);
|
||||
let msg = CreatePanel { id: 0, protocol, script };
|
||||
let msg = MagpieServerMsg::CreatePanel(msg);
|
||||
magpie.messenger.send(&msg).unwrap();
|
||||
|
||||
let mut first_loop = true;
|
||||
let mut connected = false;
|
||||
smol::block_on(async {
|
||||
let dbus = zbus::Connection::session().await.unwrap();
|
||||
|
||||
loop {
|
||||
if !first_loop {
|
||||
let wait = std::time::Duration::from_secs(1);
|
||||
std::thread::sleep(wait);
|
||||
}
|
||||
|
||||
first_loop = false;
|
||||
|
||||
if connected {
|
||||
println!("Disconnected from MPRIS");
|
||||
let msg = InMsg::Disconnected;
|
||||
magpie.send_json_message(0, &msg);
|
||||
connected = false;
|
||||
}
|
||||
|
||||
println!("Connecting to MPRIS...");
|
||||
|
||||
let player = match player_finder.find_active() {
|
||||
Ok(player) => player,
|
||||
Err(err) => {
|
||||
eprintln!("Couldn't find player: {:?}", err);
|
||||
continue;
|
||||
}
|
||||
let protocol = "tebibyte-media.desktop.music-player-controller".to_string();
|
||||
let script = std::path::PathBuf::from(&module_path);
|
||||
let msg = CreatePanel {
|
||||
id: 0,
|
||||
protocol,
|
||||
script,
|
||||
};
|
||||
let msg = MagpieServerMsg::CreatePanel(msg);
|
||||
magpie.messenger.send(&msg).unwrap();
|
||||
|
||||
println!(
|
||||
"Connected to \"{}\" ({})",
|
||||
player.identity(),
|
||||
player.bus_name()
|
||||
);
|
||||
connected = true;
|
||||
magpie.send_json_message(0, &InMsg::Connected);
|
||||
|
||||
let metadata = player.get_metadata().unwrap();
|
||||
let mut metadata_tracker = MetadataTracker::new(&mut magpie, &metadata);
|
||||
|
||||
let mut events = match player.events() {
|
||||
Ok(events) => events,
|
||||
Err(err) => {
|
||||
eprintln!("Player events D-Bus error: {:?}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let mut first_loop = true;
|
||||
let mut connected = false;
|
||||
|
||||
loop {
|
||||
let event = match events.next() {
|
||||
None => break,
|
||||
Some(Ok(e)) => e,
|
||||
Some(Err(err)) => {
|
||||
eprintln!("D-Bus error while reading player events: {:?}", err);
|
||||
if !first_loop {
|
||||
let wait = std::time::Duration::from_secs(1);
|
||||
std::thread::sleep(wait);
|
||||
}
|
||||
|
||||
first_loop = false;
|
||||
|
||||
if connected {
|
||||
println!("Disconnected from MPRIS");
|
||||
let msg = InMsg::Disconnected;
|
||||
magpie.send_json_message(0, &msg);
|
||||
connected = false;
|
||||
}
|
||||
|
||||
println!("Connecting to MPRIS...");
|
||||
|
||||
let player = match find_player(&dbus).await {
|
||||
Ok(Some(player)) => player,
|
||||
Ok(None) => {
|
||||
eprintln!("Couldn't find player");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
use mpris::Event::*;
|
||||
let in_msg = match event {
|
||||
Playing => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Playing)),
|
||||
Paused => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Paused)),
|
||||
Stopped => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Stopped)),
|
||||
LoopingChanged(status) => {
|
||||
use mpris::LoopStatus::*;
|
||||
let status = match status {
|
||||
None => LoopStatus::None,
|
||||
Track => LoopStatus::Track,
|
||||
Playlist => LoopStatus::Playlist,
|
||||
};
|
||||
|
||||
Some(InMsg::LoopingChanged(status))
|
||||
}
|
||||
ShuffleToggled(shuffle) => Some(InMsg::ShuffleChanged { shuffle }),
|
||||
VolumeChanged(volume) => Some(InMsg::VolumeChanged {
|
||||
volume: volume as f32,
|
||||
}),
|
||||
PlayerShutDown => None,
|
||||
TrackChanged(ref metadata) => {
|
||||
metadata_tracker.update(&mut magpie, metadata);
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Unhandled MPRIS message: {:?}", event);
|
||||
None
|
||||
Err(err) => {
|
||||
eprintln!("D-Bus error while finding player: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(msg) = in_msg {
|
||||
magpie.send_json_message(0, &msg);
|
||||
println!(
|
||||
"Connected to \"{}\" ({})",
|
||||
player.path().as_str(),
|
||||
player.destination().as_str()
|
||||
);
|
||||
connected = true;
|
||||
magpie.send_json_message(0, &InMsg::Connected);
|
||||
|
||||
match player_main(&player, &mut magpie).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
eprintln!("D-Bus error while connected to player: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use zbus::fdo::DBusProxy;
|
||||
use zbus::zvariant::Value;
|
||||
use zbus::{dbus_proxy, Connection, Result};
|
||||
|
||||
pub type MetadataMap<'a> = HashMap<String, Value<'a>>;
|
||||
|
||||
#[dbus_proxy(
|
||||
interface = "org.mpris.MediaPlayer2.Player",
|
||||
default_path = "/org/mpris/MediaPlayer2"
|
||||
)]
|
||||
trait Player {
|
||||
fn next(&self) -> Result<()>;
|
||||
fn previous(&self) -> Result<()>;
|
||||
fn pause(&self) -> Result<()>;
|
||||
fn play_pause(&self) -> Result<()>;
|
||||
fn stop(&self) -> Result<()>;
|
||||
fn play(&self) -> Result<()>;
|
||||
|
||||
#[dbus_proxy(property)]
|
||||
fn playback_status(&self) -> Result<String>;
|
||||
|
||||
#[dbus_proxy(property)]
|
||||
fn position(&self) -> Result<i64>;
|
||||
|
||||
#[dbus_proxy(property)]
|
||||
fn metadata(&self) -> Result<MetadataMap>;
|
||||
}
|
||||
|
||||
pub async fn find_player(connection: &Connection) -> Result<Option<PlayerProxy>> {
|
||||
let dbus = DBusProxy::new(connection).await?;
|
||||
let names = dbus.list_names().await?;
|
||||
|
||||
for name in names {
|
||||
let name = name.as_str().to_string();
|
||||
if name.starts_with("org.mpris.MediaPlayer2") {
|
||||
let player = PlayerProxy::builder(connection)
|
||||
.destination(name)?
|
||||
.build()
|
||||
.await?;
|
||||
return Ok(Some(player));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
|
@ -336,17 +336,17 @@ impl MusicPlayerWidget {
|
|||
.map(|s| s.as_str())
|
||||
.unwrap_or("<album here>"),
|
||||
);
|
||||
|
||||
if let Some(length) = info.length {
|
||||
self.duration.set_text(&Self::format_time(length));
|
||||
} else {
|
||||
self.duration.set_text("--:--");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_progress(&mut self, progress: ProgressChanged) {
|
||||
self.position_secs = progress.position;
|
||||
self.position_dirty = true;
|
||||
|
||||
if let Some(length) = progress.length {
|
||||
self.duration.set_text(&Self::format_time(length));
|
||||
} else {
|
||||
self.duration.set_text("--:--");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_playback_status(&mut self, status: PlaybackStatus) {
|
||||
|
|
Loading…
Reference in New Issue