use std::boxed::Box;
use std::result;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use crossbeam_channel::Sender;
use dbus::{
arg::{Array, RefArg},
ffidisp::stdintf::org_freedesktop_dbus::{Properties, PropertiesPropertiesChanged},
ffidisp::{BusType, Connection, ConnectionItem},
message::SignalArgs,
Message,
};
use regex::Regex;
use serde_derive::Deserialize;
use crate::blocks::{Block, ConfigBlock, Update};
use crate::config::{LogicalDirection, Scrolling, SharedConfig};
use crate::de::deserialize_duration;
use crate::errors::*;
use crate::formatting::value::Value;
use crate::formatting::FormatTemplate;
use crate::input::{I3BarEvent, MouseButton};
use crate::scheduler::Task;
use crate::subprocess::spawn_child_async;
use crate::util::pseudo_uuid;
use crate::widgets::{
rotatingtext::RotatingTextWidget, text::TextWidget, I3BarWidget, Spacing, State,
};
#[derive(Debug, Clone)]
struct Player {
bus_name: String,
interface_name: String,
playback_status: PlaybackStatus,
artist: Option<String>,
title: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
enum PlaybackStatus {
Playing,
Paused,
Stopped,
Unknown,
}
impl Default for PlaybackStatus {
fn default() -> Self {
PlaybackStatus::Unknown
}
}
pub struct Music {
id: usize,
play_id: usize,
next_id: usize,
prev_id: usize,
collapsed_id: usize,
current_song_widget: RotatingTextWidget,
prev: Option<TextWidget>,
play: Option<TextWidget>,
next: Option<TextWidget>,
on_collapsed_click_widget: TextWidget,
on_collapsed_click: Option<String>,
on_click: Option<String>,
dbus_conn: Connection,
marquee: bool,
marquee_interval: Duration,
smart_trim: bool,
max_width: usize,
separator: String,
seek_step: i64,
players: Arc<Mutex<Vec<Player>>>,
hide_when_empty: bool,
send: Sender<Task>,
format: FormatTemplate,
scrolling: Scrolling,
}
impl Music {
fn smart_trim(&self, artist: String, title: String) -> String {
let mut artist: String = artist;
let mut title: String = title;
let textlen =
title.chars().count() + self.separator.chars().count() + artist.chars().count();
if title.is_empty() {
artist.truncate(self.max_width);
} else if artist.is_empty() {
title.truncate(self.max_width);
} else {
let overshoot = (textlen - self.max_width) as f32;
let substance = (textlen - 3) as f32;
let tlen = title.chars().count();
let tblm = tlen as f32 / substance;
let mut tnum = (overshoot * tblm).ceil() as usize;
let alen = artist.chars().count();
let ablm = alen as f32 / substance;
let mut anum = (overshoot * ablm).ceil() as usize;
if anum < tnum && anum <= 3 && (tnum + anum < tlen) {
anum = 0;
tnum += anum;
}
if tnum < anum && tnum <= 3 && (anum + tnum < alen) {
tnum = 0;
anum += tnum;
}
let mut ttrc = tlen - tnum;
if !(1..5001).contains(&ttrc) {
ttrc = 1
}
let mut atrc = alen - anum;
if !(1..5001).contains(&atrc) {
atrc = 1
}
let tidx = title
.char_indices()
.nth(ttrc)
.unwrap_or((title.len(), 'a'))
.0;
title.truncate(tidx);
let aidx = artist
.char_indices()
.nth(atrc)
.unwrap_or((artist.len(), 'a'))
.0;
artist.truncate(aidx);
}
format!("{}{}{}", title, self.separator, artist)
}
}
#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields, default)]
pub struct MusicConfig {
pub player: Option<String>,
pub max_width: usize,
pub dynamic_width: bool,
pub marquee: bool,
#[serde(deserialize_with = "deserialize_duration")]
pub marquee_interval: Duration,
#[serde(deserialize_with = "deserialize_duration")]
pub marquee_speed: Duration,
pub smart_trim: bool,
pub separator: String,
pub buttons: Vec<String>,
pub on_collapsed_click: Option<String>,
pub seek_step: i64,
pub interface_name_exclude: Vec<String>,
pub hide_when_empty: bool,
pub format: String,
}
impl Default for MusicConfig {
fn default() -> Self {
Self {
player: None,
max_width: 21,
dynamic_width: false,
marquee: true,
marquee_interval: Duration::from_secs(10),
marquee_speed: Duration::from_millis(500),
smart_trim: false,
separator: " - ".to_string(),
buttons: Vec::new(),
on_collapsed_click: None,
seek_step: 1000,
interface_name_exclude: Vec::new(),
hide_when_empty: false,
format: "{combo}".to_string(),
}
}
}
impl ConfigBlock for Music {
type Config = MusicConfig;
fn new(
id: usize,
block_config: Self::Config,
shared_config: SharedConfig,
send: Sender<Task>,
) -> Result<Self> {
let play_id = pseudo_uuid();
let prev_id = pseudo_uuid();
let next_id = pseudo_uuid();
let collapsed_id = pseudo_uuid();
let send2 = send.clone();
let send3 = send.clone();
let c = Connection::get_private(BusType::Session)
.block_error("music", "failed to establish D-Bus connection")?;
let interface_name_exclude_regexps =
compile_regexps(block_config.clone().interface_name_exclude)
.block_error("music", "failed to parse exclude patterns")?;
let mut initial_players: Vec<Player> = Vec::new();
let m = Message::new_method_call(
"org.freedesktop.DBus",
"/",
"org.freedesktop.DBus",
"ListNames",
)
.unwrap();
let r = c.send_with_reply_and_block(m, 500).unwrap();
let arr: Array<&str, _> = r.get1().unwrap();
for name in arr {
if ignored_player(
&name,
&interface_name_exclude_regexps,
block_config.clone().player,
) {
continue;
}
let m = Message::new_method_call(
"org.freedesktop.DBus",
"/",
"org.freedesktop.DBus",
"GetNameOwner",
)
.unwrap();
let r = c.send_with_reply_and_block(m.append1(name), 500).unwrap();
let bn: &str = r.read1().ok().unwrap();
if !initial_players.iter().any(|p| p.bus_name == bn) {
let p = c.with_path(name, "/org/mpris/MediaPlayer2", 500);
let data = p.get("org.mpris.MediaPlayer2.Player", "Metadata");
let (title, artist) = match data {
Err(_) => (String::new(), String::new()),
Ok(data) => {
extract_from_metadata(&data).unwrap_or((String::new(), String::new()))
}
};
let data = p.get("org.mpris.MediaPlayer2.Player", "PlaybackStatus");
let status = match data {
Err(_) => PlaybackStatus::Unknown,
Ok(data) => {
let data: Box<dyn RefArg> = data;
extract_playback_status(&data)
}
};
initial_players.push(Player {
bus_name: bn.to_string(),
interface_name: name.to_string(),
playback_status: status,
artist: Some(artist),
title: Some(title),
});
}
}
let players_original = Arc::new(Mutex::new(initial_players));
let players_copy = players_original.clone();
let players_copy2 = players_original.clone();
let players_copy3 = players_original;
thread::Builder::new().name("music".into()).spawn(move || {
let c = Connection::get_private(BusType::Session).unwrap();
c.add_match("interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='/org/mpris/MediaPlayer2'").unwrap();
loop {
for msg in c.incoming(100_000) {
if msg.sender().is_some() {
if let Some(signal) = PropertiesPropertiesChanged::from_message(&msg) {
let mut players = players_copy2.lock().expect("failed to acquire lock for `players`");
let player = players.iter_mut().find(|p| p.bus_name == msg.sender().unwrap().to_string());
if player.is_none() {
continue;
}
let p = player.unwrap();
let mut updated = false;
let raw_metadata = signal.changed_properties.get("Metadata");
if let Some(data) = raw_metadata {
let (title, artist) =
extract_from_metadata(&data.0).unwrap_or((String::new(), String::new()));
if p.artist != Some(artist.clone()) {
p.artist = Some(artist);
updated = true;
}
if p.title != Some(title.clone()) {
p.title = Some(title);
updated = true;
}
};
let raw_metadata = signal.changed_properties.get("PlaybackStatus");
if let Some(data) = raw_metadata {
let new_status = extract_playback_status(&data.0);
if p.playback_status != new_status {
p.playback_status = new_status;
updated = true;
}
};
let raw_metadata = signal.changed_properties.get("PlayerNames");
if let Some(data) = raw_metadata {
let mut playerctl_playerlist = data.0.as_iter().unwrap().peekable();
if playerctl_playerlist.peek().is_none() {
p.artist = None;
p.title = None;
updated = true;
}
};
if updated {
send.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
}
}
}
}
}
}).unwrap();
let preferred_player = block_config.clone().player;
thread::Builder::new().name("music".into()).spawn(move || {
let c = Connection::get_private(BusType::Session).unwrap();
c.add_match("interface='org.freedesktop.DBus',member='NameOwnerChanged',path='/org/freedesktop/DBus',arg0namespace='org.mpris.MediaPlayer2'")
.unwrap();
c.incoming(10_000).next();
loop {
for ci in c.iter(100_000) {
if let ConnectionItem::Signal(x) = ci {
let (name, old_owner, new_owner): (&str, &str, &str) = x.read3().unwrap();
let mut players = players_copy3.lock().expect("failed to acquire lock for `players`");
if !old_owner.is_empty() && new_owner.is_empty() {
if let Some(pos) = players.iter().position(|p| p.bus_name == old_owner) {
players.remove(pos);
send2.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
}
} else if old_owner.is_empty() && !new_owner.is_empty() && !ignored_player(name, &interface_name_exclude_regexps, preferred_player.clone()) && !players.iter().any(|p| p.bus_name == new_owner) {
players.push(Player {
bus_name: new_owner.to_string(),
interface_name: name.to_string(),
playback_status: PlaybackStatus::Unknown,
artist: None,
title: None,
});
send2.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
}
}
}
}
}).unwrap();
let mut play: Option<TextWidget> = None;
let mut prev: Option<TextWidget> = None;
let mut next: Option<TextWidget> = None;
for button in block_config.buttons {
match &*button {
"play" => {
play = Some(
TextWidget::new(id, play_id, shared_config.clone())
.with_icon("music_play")?
.with_state(State::Info)
.with_spacing(Spacing::Hidden),
)
}
"next" => {
next = Some(
TextWidget::new(id, next_id, shared_config.clone())
.with_icon("music_next")?
.with_state(State::Info)
.with_spacing(Spacing::Hidden),
)
}
"prev" => {
prev = Some(
TextWidget::new(id, prev_id, shared_config.clone())
.with_icon("music_prev")?
.with_state(State::Info)
.with_spacing(Spacing::Hidden),
)
}
x => {
return Err(BlockError(
"music".to_owned(),
format!("unknown music button identifier: '{}'", x),
))
}
};
}
fn compile_regexps(patterns: Vec<String>) -> result::Result<Vec<Regex>, regex::Error> {
patterns.iter().map(|p| Regex::new(&p)).collect()
}
Ok(Music {
id,
play_id,
prev_id,
next_id,
collapsed_id,
current_song_widget: RotatingTextWidget::new(
id,
id,
Duration::new(block_config.marquee_interval.as_secs(), 0),
Duration::new(0, block_config.marquee_speed.subsec_nanos()),
block_config.max_width,
block_config.dynamic_width,
shared_config.clone(),
)
.with_icon("music")?
.with_state(State::Info)
.with_spacing(Spacing::Hidden),
prev,
play,
next,
on_click: None,
on_collapsed_click_widget: TextWidget::new(id, collapsed_id, shared_config.clone())
.with_icon("music")?
.with_state(State::Info)
.with_spacing(Spacing::Hidden),
on_collapsed_click: block_config.on_collapsed_click,
dbus_conn: Connection::get_private(BusType::Session)
.block_error("music", "failed to establish D-Bus connection")?,
marquee: block_config.marquee,
marquee_interval: block_config.marquee_interval,
smart_trim: block_config.smart_trim,
max_width: block_config.max_width,
separator: block_config.separator,
seek_step: block_config.seek_step,
players: players_copy,
hide_when_empty: block_config.hide_when_empty,
send: send3,
format: FormatTemplate::from_string(&block_config.format)?,
scrolling: shared_config.scrolling,
})
}
fn override_on_click(&mut self) -> Option<&mut Option<String>> {
Some(&mut self.on_click)
}
}
impl Block for Music {
fn id(&self) -> usize {
self.id
}
fn update(&mut self) -> Result<Option<Update>> {
let (rotation_in_progress, time_to_next_rotation) = if self.marquee {
self.current_song_widget.next()?
} else {
(false, None)
};
let players = self
.players
.lock()
.block_error("music", "failed to acquire lock for `players`")?;
let metadata = match players.first() {
Some(m) => m,
None => {
self.current_song_widget.set_text(String::from(""));
return Ok(None);
}
};
let interface_name = metadata.clone().interface_name;
let split: Vec<&str> = interface_name.split('.').collect();
let player_name = split[3].to_string();
let artist = metadata.clone().artist.unwrap_or_else(|| String::from(""));
let title = metadata.clone().title.unwrap_or_else(|| String::from(""));
let combo =
if (title.chars().count() + self.separator.chars().count() + artist.chars().count())
< self.max_width
|| !self.smart_trim
{
format!("{}{}{}", title, self.separator, artist)
} else {
self.smart_trim(artist.clone(), title.clone())
};
let values = map!(
"artist" => Value::from_string(artist.clone()),
"title" => Value::from_string(title.clone()),
"combo" => Value::from_string(combo),
"player" => Value::from_string(player_name),
"avail" => Value::from_string(players.len().to_string()),
);
if !(rotation_in_progress) {
if title.is_empty() && artist.is_empty() {
self.current_song_widget.set_text(String::new());
} else {
self.current_song_widget
.set_text(self.format.render(&values)?);
}
}
if let Some(ref mut play) = self.play {
play.set_icon(match metadata.playback_status {
PlaybackStatus::Playing => "music_pause",
PlaybackStatus::Paused => "music_play",
PlaybackStatus::Stopped => "music_play",
PlaybackStatus::Unknown => "music_play",
})?
}
if let Some(t) = time_to_next_rotation {
Ok(Some(Update::Every(t)))
} else if self.marquee {
Ok(Some(self.marquee_interval.into()))
} else {
Ok(None)
}
}
fn click(&mut self, event: &I3BarEvent) -> Result<()> {
if let Some(event_id) = event.instance {
let action = match event_id {
id if id == self.play_id => "PlayPause",
id if id == self.next_id => "Next",
id if id == self.prev_id => "Previous",
id if id == self.id => "",
id if id == self.collapsed_id => "",
_ => return Ok(()),
};
let mut players = self
.players
.lock()
.block_error("music", "failed to acquire lock for `players`")?;
match event.button {
MouseButton::Left => {
if !action.is_empty() && players.len() > 0 {
let metadata = players.first().unwrap();
let m = Message::new_method_call(
metadata.interface_name.clone(),
"/org/mpris/MediaPlayer2",
"org.mpris.MediaPlayer2.Player",
action,
)
.block_error("music", "failed to create D-Bus method call")?;
self.dbus_conn
.send(m)
.block_error("music", "failed to call method via D-Bus")?;
} else if event_id == self.collapsed_id && self.on_collapsed_click.is_some() {
let cmd = self.on_collapsed_click.as_ref().unwrap();
spawn_child_async("sh", &["-c", cmd])
.block_error("music", "could not spawn child")?;
} else if event_id == self.id {
if let Some(ref cmd) = self.on_click {
spawn_child_async("sh", &["-c", cmd])
.block_error("music", "could not spawn child")?;
}
}
}
MouseButton::Right => {
if (event_id == self.id || event_id == self.collapsed_id) && players.len() > 0 {
players.rotate_left(1);
self.send.send(Task {
id: self.id,
update_time: Instant::now(),
})?;
}
}
_ => {
if event_id == self.id && players.len() > 0 {
let metadata = players.first().unwrap();
let m = Message::new_method_call(
metadata.interface_name.clone(),
"/org/mpris/MediaPlayer2",
"org.mpris.MediaPlayer2.Player",
"Seek",
)
.block_error("music", "failed to create D-Bus method call")?;
use LogicalDirection::*;
match self.scrolling.to_logical_direction(event.button) {
Some(Up) => {
self.dbus_conn
.send(m.append1(self.seek_step * 1000))
.block_error("music", "failed to call method via D-Bus")?;
}
Some(Down) => {
self.dbus_conn
.send(m.append1(self.seek_step * -1000))
.block_error("music", "failed to call method via D-Bus")?;
}
None => {}
}
}
}
}
}
Ok(())
}
fn view(&self) -> Vec<&dyn I3BarWidget> {
let players = self
.players
.lock()
.expect("failed to acquire lock for `players`");
if players.len() <= 1 && self.current_song_widget.is_empty() && self.hide_when_empty {
vec![]
} else if players.len() > 0 && !self.current_song_widget.is_empty() {
let mut elements: Vec<&dyn I3BarWidget> = Vec::new();
elements.push(&self.current_song_widget);
if let Some(ref prev) = self.prev {
elements.push(prev);
}
if let Some(ref play) = self.play {
elements.push(play);
}
if let Some(ref next) = self.next {
elements.push(next);
}
elements
} else if self.current_song_widget.is_empty() {
vec![&self.on_collapsed_click_widget]
} else {
vec![&self.current_song_widget]
}
}
}
fn extract_playback_status(value: &dyn RefArg) -> PlaybackStatus {
if let Some(status) = value.as_str() {
match status {
"Playing" => PlaybackStatus::Playing,
"Paused" => PlaybackStatus::Paused,
"Stopped" => PlaybackStatus::Stopped,
_ => PlaybackStatus::Unknown,
}
} else {
PlaybackStatus::Unknown
}
}
fn extract_artist_from_value(value: &dyn RefArg) -> Result<&str> {
if let Some(artist) = value.as_str() {
Ok(artist)
} else {
extract_artist_from_value(
value
.as_iter()
.block_error("music", "failed to extract artist")?
.next()
.block_error("music", "failed to extract artist")?,
)
}
}
#[allow(clippy::borrowed_box)]
fn extract_from_metadata(metadata: &Box<dyn RefArg>) -> Result<(String, String)> {
let mut title = String::new();
let mut artist = String::new();
let mut iter = metadata
.as_iter()
.block_error("music", "failed to extract metadata")?;
while let Some(key) = iter.next() {
let value = iter
.next()
.block_error("music", "failed to extract metadata")?;
match key
.as_str()
.block_error("music", "failed to extract metadata")?
{
"xesam:artist" => artist = String::from(extract_artist_from_value(value)?),
"xesam:title" => {
title = String::from(
value
.as_str()
.block_error("music", "failed to extract metadata")?,
)
}
_ => {}
};
}
Ok((title, artist))
}
fn ignored_player(
name: &str,
interface_name_exclude_regexps: &[Regex],
preferred_player: Option<String>,
) -> bool {
if let Some(p) = preferred_player {
if !name.starts_with(&format!("org.mpris.MediaPlayer2.{}", p)) {
return true;
}
}
if !name.starts_with("org.mpris.MediaPlayer2") {
return true;
}
if interface_name_exclude_regexps
.iter()
.any(|regex| regex.is_match(&name))
{
return true;
}
false
}