use super::prelude::*;
use crate::wrappers::DisplaySlice;
use regex::Regex;
use std::fmt;
use zbus::fdo::{DBusProxy, NameOwnerChanged, PropertiesChanged};
use zbus::names::{OwnedBusName, OwnedUniqueName};
use zbus::{MatchRule, MessageStream};
mod zbus_mpris;
mod zbus_playerctld;
make_log_macro!(debug, "music");
const PLAY_PAUSE_BTN: &str = "play_pause_btn";
const NEXT_BTN: &str = "next_btn";
const PREV_BTN: &str = "prev_btn";
#[derive(Deserialize, Debug, SmartDefault)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
pub format: FormatConfig,
pub format_alt: Option<FormatConfig>,
pub player: PlayerName,
#[default(vec!["playerctld".into()])]
pub interface_name_exclude: Vec<String>,
#[default(" - ".into())]
pub separator: String,
#[default(1.into())]
pub seek_step_secs: Seconds<false>,
pub seek_forward_step_secs: Option<Seconds<false>>,
pub seek_backward_step_secs: Option<Seconds<false>>,
#[default(5.0)]
pub volume_step: f64,
}
#[derive(Deserialize, Debug, Clone, SmartDefault)]
#[serde(untagged)]
pub enum PlayerName {
Single(String),
#[default]
Multiple(Vec<String>),
}
pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
let mut actions = api.get_actions()?;
api.set_default_actions(&[
(MouseButton::Left, Some(PLAY_PAUSE_BTN), "play_pause"),
(MouseButton::Left, Some(NEXT_BTN), "next"),
(MouseButton::Left, Some(PREV_BTN), "prev"),
(MouseButton::Right, None, "next_player"),
(MouseButton::WheelUp, None, "seek_forward"),
(MouseButton::WheelDown, None, "seek_backward"),
(MouseButton::Left, None, "toggle_format"),
])?;
let dbus_conn = new_dbus_connection().await?;
let mut format = config
.format
.with_default(" $icon {$combo.str(max_w:25,rot_interval:0.5) $play |}")?;
let mut format_alt = match &config.format_alt {
Some(f) => Some(f.with_default("")?),
None => None,
};
let volume_step = config.volume_step.clamp(0.0, 50.0) / 100.0;
let seek_forward_step = config
.seek_forward_step_secs
.unwrap_or(config.seek_step_secs)
.0
.as_micros() as i64;
let seek_backward_step = -(config
.seek_backward_step_secs
.unwrap_or(config.seek_step_secs)
.0
.as_micros() as i64);
let new_btn = |icon: &str, instance: &'static str| -> Result<Value> {
Ok(Value::icon(icon.to_string()).with_instance(instance))
};
let values = map! {
"icon" => Value::icon("music"),
"next" => new_btn("music_next", NEXT_BTN)?,
"prev" => new_btn("music_prev", PREV_BTN)?,
};
let preferred_players = match config.player.clone() {
PlayerName::Single(name) => vec![name],
PlayerName::Multiple(names) => names,
};
let exclude_regex = config
.interface_name_exclude
.iter()
.map(|r| Regex::new(r))
.collect::<Result<Vec<_>, _>>()
.error("Invalid regex")?;
let playerctld_proxy = zbus_playerctld::PlayerctldProxy::new(&dbus_conn)
.await
.error("Failed to create PlayerctldProxy")?;
let mut players = get_players(&dbus_conn, &preferred_players, &exclude_regex).await?;
let mut cur_player = None;
if let Ok(playerctld_players) = playerctld_proxy.player_names().await {
for playerctld_player in playerctld_players {
if let Some(pos) = players
.iter()
.position(|p| p.bus_name.as_str() == playerctld_player)
{
cur_player = Some(pos);
break;
}
}
} else {
for (i, player) in players.iter().enumerate() {
cur_player = Some(i);
if player.status == Some(PlaybackStatus::Playing) {
break;
}
}
}
let mut properties_stream = MessageStream::for_match_rule(
MatchRule::builder()
.msg_type(zbus::message::Type::Signal)
.interface("org.freedesktop.DBus.Properties")
.and_then(|x| x.member("PropertiesChanged"))
.and_then(|x| x.path("/org/mpris/MediaPlayer2"))
.unwrap()
.build(),
&dbus_conn,
None,
)
.await
.error("Failed to add match rule")?;
let mut name_owner_changed_stream = MessageStream::for_match_rule(
MatchRule::builder()
.msg_type(zbus::message::Type::Signal)
.interface("org.freedesktop.DBus")
.and_then(|x| x.member("NameOwnerChanged"))
.and_then(|x| x.arg0ns("org.mpris.MediaPlayer2"))
.unwrap()
.build(),
&dbus_conn,
None,
)
.await
.error("Failed to add match rule")?;
let mut active_player_change_end_stream = playerctld_proxy
.receive_active_player_change_end()
.await
.error("Failed to create ActivePlayerChangeEndStream")?;
loop {
debug!("available players: {}", DisplaySlice(&players));
let avail = players.len();
let player = cur_player.map(|c| players.get_mut(c).unwrap());
match player {
Some(player) => {
let mut values = values.clone();
values.insert("avail".into(), Value::number(avail));
values.insert("cur".into(), Value::number(cur_player.unwrap() + 1));
values.insert(
"player".into(),
Value::text(
extract_player_name(player.bus_name.as_str())
.unwrap()
.into(),
),
);
let (state, play_icon) = match player.status {
Some(PlaybackStatus::Playing) => (State::Info, "music_pause"),
_ => (State::Idle, "music_play"),
};
values.insert("play".into(), new_btn(play_icon, PLAY_PAUSE_BTN)?);
if let Some(url) = &player.metadata.url {
values.insert("url".into(), Value::text(url.clone()));
}
match (
&player.metadata.title,
&player.metadata.artist,
&player.metadata.url,
) {
(Some(t), None, _) => {
values.insert("combo".into(), Value::text(t.clone()));
values.insert("title".into(), Value::text(t.clone()));
}
(None, Some(a), _) => {
values.insert("combo".into(), Value::text(a.clone()));
values.insert("artist".into(), Value::text(a.clone()));
}
(Some(t), Some(a), _) => {
values.insert(
"combo".into(),
Value::text(format!("{t}{}{a}", config.separator)),
);
values.insert("title".into(), Value::text(t.clone()));
values.insert("artist".into(), Value::text(a.clone()));
}
(None, None, Some(url)) => {
values.insert("combo".into(), Value::text(url.clone()));
}
_ => (),
}
if let Some(volume) = player.volume {
values.insert(
"volume_icon".into(),
Value::icon_progression("volume", volume),
);
values.insert("volume".into(), Value::percents(volume * 100.0));
}
let mut widget = Widget::new().with_format(format.clone());
widget.set_values(values);
widget.state = state;
api.set_widget(widget)?;
}
None => {
let mut widget = Widget::new().with_format(format.clone());
widget.set_values(map!("icon" => Value::icon("music")));
api.set_widget(widget)?;
}
}
loop {
select! {
Some(msg) = properties_stream.next() => {
let msg = msg.unwrap();
let msg = PropertiesChanged::from_message(msg).unwrap();
let args = msg.args().unwrap();
let header = msg.message().header();
let sender = header.sender().unwrap();
if let Some((pos, player)) = players.iter_mut().enumerate().find(|p| &*p.1.owner == sender) {
let props = args.changed_properties;
if let Some(status) = props.get("PlaybackStatus") {
let status: &str = status.downcast_ref().unwrap();
player.status = PlaybackStatus::from_str(status);
}
if let Some(metadata) = props.get("Metadata") {
player.metadata =
zbus_mpris::PlayerMetadata::try_from(metadata.try_to_owned().unwrap()).unwrap();
}
if let Some(volume) = props.get("Volume") {
player.volume = Some(*volume.downcast_ref::<&f64>().unwrap());
}
if player.status == Some(PlaybackStatus::Playing)
&& (
player.metadata.title.is_some()
|| player.metadata.artist.is_some()
|| player.metadata.url.is_some()
) {
cur_player = Some(pos);
}
break;
}
}
Some(msg) = name_owner_changed_stream.next() => {
let msg = msg.unwrap();
let msg = NameOwnerChanged::from_message(msg).unwrap();
let args = msg.args().unwrap();
match (args.old_owner.as_ref(), args.new_owner.as_ref()) {
(None, Some(new)) => {
debug!("new player {} owned by {new}", args.name);
if player_matches(args.name.as_str(), &preferred_players, &exclude_regex) {
match Player::new(&dbus_conn, args.name.to_owned().into(), new.to_owned().into()).await {
Ok(player) => players.push(player),
Err(e) => {
debug!("{e}");
},
}
}
}
(Some(old), None) => {
if let Some(pos) = players.iter().position(|p| &*p.owner == old) {
debug!("removed player {} owned by {old}", args.name);
players.remove(pos);
if let Some(cur) = cur_player {
if players.is_empty() {
cur_player = None;
} else if pos == cur {
cur_player = Some(0);
} else if pos < cur {
cur_player = Some(cur - 1);
}
}
}
}
_ => (),
}
break;
}
Some(msg) = active_player_change_end_stream.next() => {
let args = msg.args().unwrap();
if let Some(pos) = players.iter().position(|p| p.bus_name == args.name){
cur_player = Some(pos);
}
else{
if let Err(e) = playerctld_proxy.shift().await{
debug!("{e}");
}
}
break;
}
Some(action) = actions.recv() => {
if let Some(i) = cur_player {
let player = &players[i];
match action.as_ref() {
"play_pause" => {
player.play_pause().await?;
}
"next" => {
player.next().await?;
}
"prev" => {
player.prev().await?;
}
"next_player" => {
cur_player = Some((i + 1) % players.len());
if let Err(e) = playerctld_proxy.shift().await{
debug!("{e}");
}
break;
}
"seek_forward" => {
player.seek(seek_forward_step).await?;
}
"seek_backward" => {
player.seek(seek_backward_step).await?;
}
"volume_up" => {
player.set_volume(volume_step).await?;
}
"volume_down" => {
player.set_volume(-volume_step).await?;
}
"toggle_format" => {
if let Some(format_alt) = &mut format_alt {
std::mem::swap(format_alt, &mut format);
break;
}
}
_ => (),
}
}
}
}
}
}
}
async fn get_players(
dbus_conn: &zbus::Connection,
preferred_players: &[String],
exclude_regex: &[Regex],
) -> Result<Vec<Player>> {
let proxy = DBusProxy::new(dbus_conn)
.await
.error("failed to create DBusProxy")?;
let names = proxy
.list_names()
.await
.error("failed to list dbus names")?;
let mut players = Vec::new();
for name in names {
if player_matches(name.as_str(), preferred_players, exclude_regex) {
let owner = proxy.get_name_owner(name.as_ref()).await.unwrap();
match Player::new(dbus_conn, name, owner).await {
Ok(player) => players.push(player),
Err(e) => {
debug!("{e}");
}
}
}
}
Ok(players)
}
#[derive(Debug)]
struct Player {
status: Option<PlaybackStatus>,
owner: OwnedUniqueName,
bus_name: OwnedBusName,
player_proxy: zbus_mpris::PlayerProxy<'static>,
metadata: zbus_mpris::PlayerMetadata,
volume: Option<f64>,
}
impl Player {
async fn new(
dbus_conn: &zbus::Connection,
bus_name: OwnedBusName,
owner: OwnedUniqueName,
) -> Result<Player> {
debug!("creating Player for {bus_name}");
let proxy = zbus_mpris::PlayerProxy::builder(dbus_conn)
.destination(bus_name.clone())
.error("failed to set proxy destination")?
.build()
.await
.error("failed to open player proxy")?;
debug!("querying player metadata");
let metadata = proxy.metadata().await;
debug!("querying player status");
let status = proxy.playback_status().await;
debug!("querying player volume");
let volume = proxy.volume().await;
let metadata = metadata.error("failed to obtain player metadata")?;
let status = status.error("failed to obtain player status")?;
debug!("Player created");
Ok(Self {
status: PlaybackStatus::from_str(&status),
owner,
bus_name,
player_proxy: proxy,
metadata,
volume: volume.ok(),
})
}
async fn play_pause(&self) -> Result<()> {
self.player_proxy
.play_pause()
.await
.error("play_pause() failed")
}
async fn prev(&self) -> Result<()> {
self.player_proxy.previous().await.error("prev() failed")
}
async fn next(&self) -> Result<()> {
self.player_proxy.next().await.error("next() failed")
}
async fn seek(&self, offset: i64) -> Result<()> {
match self.player_proxy.seek(offset).await {
Err(zbus::Error::MethodError(e, _, _))
if e == "org.freedesktop.DBus.Error.NotSupported" =>
{
Ok(())
}
other => dbg!(other).error("seek() failed"),
}
}
async fn set_volume(&self, step_size: f64) -> Result<()> {
if let Some(volume) = self.volume {
self.player_proxy
.set_volume(volume + step_size)
.await
.error("set_volume() failed")?;
}
Ok(())
}
}
impl fmt::Display for Player {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
extract_player_name(&self.bus_name).unwrap().fmt(f)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PlaybackStatus {
Playing,
Paused,
Stopped,
}
impl PlaybackStatus {
fn from_str(s: &str) -> Option<Self> {
match s {
"Paused" => Some(Self::Paused),
"Playing" => Some(Self::Playing),
"Stopped" => Some(Self::Stopped),
_ => None,
}
}
}
fn extract_player_name(full_name: &str) -> Option<&str> {
const NAME_PREFIX: &str = "org.mpris.MediaPlayer2.";
full_name
.starts_with(NAME_PREFIX)
.then(|| &full_name[NAME_PREFIX.len()..])
}
fn player_matches(full_name: &str, preferred_players: &[String], exclude_regex: &[Regex]) -> bool {
let name = match extract_player_name(full_name) {
Some(name) => name,
None => return false,
};
exclude_regex.iter().all(|r| !r.is_match(name))
&& (preferred_players.is_empty()
|| preferred_players.iter().any(|p| name.starts_with(&**p)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_player_name_test() {
assert_eq!(
extract_player_name("org.mpris.MediaPlayer2.firefox.instance852"),
Some("firefox.instance852")
);
assert_eq!(
extract_player_name("not.org.mpris.MediaPlayer2.firefox.instance852"),
None,
);
assert_eq!(
extract_player_name("org.mpris.MediaPlayer3.firefox.instance852"),
None,
);
}
#[test]
fn player_matches_test() {
let exclude = vec![Regex::new("mpd").unwrap(), Regex::new("firefox.*").unwrap()];
assert!(player_matches(
"org.mpris.MediaPlayer2.playerctld",
&[],
&exclude
));
assert!(!player_matches(
"org.mpris.MediaPlayer2.playerctld",
&["spotify".into()],
&exclude
));
assert!(!player_matches(
"org.mpris.MediaPlayer2.firefox.instance852",
&[],
&exclude
));
}
}