1use super::prelude::*;
145use crate::wrappers::DisplaySlice;
146
147use regex::Regex;
148use std::fmt;
149use zbus::fdo::{DBusProxy, NameOwnerChanged, PropertiesChanged};
150use zbus::names::{OwnedBusName, OwnedUniqueName};
151use zbus::{MatchRule, MessageStream};
152
153mod zbus_mpris;
154mod zbus_playerctld;
155
156make_log_macro!(debug, "music");
157
158const PLAY_PAUSE_BTN: &str = "play_pause_btn";
159const NEXT_BTN: &str = "next_btn";
160const PREV_BTN: &str = "prev_btn";
161
162#[derive(Deserialize, Debug, SmartDefault)]
163#[serde(deny_unknown_fields, default)]
164pub struct Config {
165 pub format: FormatConfig,
166 pub format_alt: Option<FormatConfig>,
167 pub player: PlayerName,
168 #[default(vec!["playerctld".into()])]
169 pub interface_name_exclude: Vec<String>,
170 #[default(" - ".into())]
171 pub separator: String,
172 #[default(1.into())]
173 pub seek_step_secs: Seconds<false>,
174 pub seek_forward_step_secs: Option<Seconds<false>>,
175 pub seek_backward_step_secs: Option<Seconds<false>>,
176 #[default(5.0)]
177 pub volume_step: f64,
178}
179
180#[derive(Deserialize, Debug, Clone, SmartDefault)]
181#[serde(untagged)]
182pub enum PlayerName {
183 Single(String),
184 #[default]
185 Multiple(Vec<String>),
186}
187
188pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
189 let mut actions = api.get_actions()?;
190 api.set_default_actions(&[
191 (MouseButton::Left, Some(PLAY_PAUSE_BTN), "play_pause"),
192 (MouseButton::Left, Some(NEXT_BTN), "next"),
193 (MouseButton::Left, Some(PREV_BTN), "prev"),
194 (MouseButton::Right, None, "next_player"),
195 (MouseButton::WheelUp, None, "seek_forward"),
196 (MouseButton::WheelDown, None, "seek_backward"),
197 (MouseButton::Left, None, "toggle_format"),
198 ])?;
199
200 let dbus_conn = new_dbus_connection().await?;
201
202 let mut format = config
203 .format
204 .with_default(" $icon {$combo.str(max_w:25,rot_interval:0.5) $play |}")?;
205 let mut format_alt = match &config.format_alt {
206 Some(f) => Some(f.with_default("")?),
207 None => None,
208 };
209
210 let volume_step = config.volume_step.clamp(0.0, 50.0) / 100.0;
211
212 let seek_forward_step = config
213 .seek_forward_step_secs
214 .unwrap_or(config.seek_step_secs)
215 .0
216 .as_micros() as i64;
217 let seek_backward_step = -(config
218 .seek_backward_step_secs
219 .unwrap_or(config.seek_step_secs)
220 .0
221 .as_micros() as i64);
222
223 let new_btn = |icon: &str, instance: &'static str| -> Result<Value> {
224 Ok(Value::icon(icon.to_string()).with_instance(instance))
225 };
226
227 let values = map! {
228 "icon" => Value::icon("music"),
229 "next" => new_btn("music_next", NEXT_BTN)?,
230 "prev" => new_btn("music_prev", PREV_BTN)?,
231 };
232
233 let preferred_players = match config.player.clone() {
234 PlayerName::Single(name) => vec![name],
235 PlayerName::Multiple(names) => names,
236 };
237 let exclude_regex = config
238 .interface_name_exclude
239 .iter()
240 .map(|r| Regex::new(r))
241 .collect::<Result<Vec<_>, _>>()
242 .error("Invalid regex")?;
243
244 let playerctld_proxy = zbus_playerctld::PlayerctldProxy::new(&dbus_conn)
245 .await
246 .error("Failed to create PlayerctldProxy")?;
247
248 let mut players = get_players(&dbus_conn, &preferred_players, &exclude_regex).await?;
249 let mut cur_player = None;
250 if let Ok(playerctld_players) = playerctld_proxy.player_names().await {
251 for playerctld_player in playerctld_players {
254 if let Some(pos) = players
255 .iter()
256 .position(|p| p.bus_name.as_str() == playerctld_player)
257 {
258 cur_player = Some(pos);
259 break;
260 }
261 }
262 } else {
263 for (i, player) in players.iter().enumerate() {
267 cur_player = Some(i);
268 if player.status == Some(PlaybackStatus::Playing) {
269 break;
270 }
271 }
272 }
273
274 let mut properties_stream = MessageStream::for_match_rule(
275 MatchRule::builder()
276 .msg_type(zbus::message::Type::Signal)
277 .interface("org.freedesktop.DBus.Properties")
278 .and_then(|x| x.member("PropertiesChanged"))
279 .and_then(|x| x.path("/org/mpris/MediaPlayer2"))
280 .unwrap()
281 .build(),
282 &dbus_conn,
283 None,
284 )
285 .await
286 .error("Failed to add match rule")?;
287
288 let mut name_owner_changed_stream = MessageStream::for_match_rule(
289 MatchRule::builder()
290 .msg_type(zbus::message::Type::Signal)
291 .interface("org.freedesktop.DBus")
292 .and_then(|x| x.member("NameOwnerChanged"))
293 .and_then(|x| x.arg0ns("org.mpris.MediaPlayer2"))
294 .unwrap()
295 .build(),
296 &dbus_conn,
297 None,
298 )
299 .await
300 .error("Failed to add match rule")?;
301
302 let mut active_player_change_end_stream = playerctld_proxy
303 .receive_active_player_change_end()
304 .await
305 .error("Failed to create ActivePlayerChangeEndStream")?;
306
307 loop {
308 debug!("available players: {}", DisplaySlice(&players));
309
310 let avail = players.len();
311 let player = cur_player.map(|c| players.get_mut(c).unwrap());
312 match player {
313 Some(player) => {
314 let mut values = values.clone();
315 values.insert("avail".into(), Value::number(avail));
316 values.insert("cur".into(), Value::number(cur_player.unwrap() + 1));
317 values.insert(
318 "player".into(),
319 Value::text(
320 extract_player_name(player.bus_name.as_str())
321 .unwrap()
322 .into(),
323 ),
324 );
325 let (state, play_icon) = match player.status {
326 Some(PlaybackStatus::Playing) => (State::Info, "music_pause"),
327 _ => (State::Idle, "music_play"),
328 };
329 values.insert("play".into(), new_btn(play_icon, PLAY_PAUSE_BTN)?);
330 if let Some(url) = &player.metadata.url {
331 values.insert("url".into(), Value::text(url.clone()));
332 }
333 match (
334 &player.metadata.title,
335 &player.metadata.artist,
336 &player.metadata.url,
337 ) {
338 (Some(t), None, _) => {
339 values.insert("combo".into(), Value::text(t.clone()));
340 values.insert("title".into(), Value::text(t.clone()));
341 }
342 (None, Some(a), _) => {
343 values.insert("combo".into(), Value::text(a.clone()));
344 values.insert("artist".into(), Value::text(a.clone()));
345 }
346 (Some(t), Some(a), _) => {
347 values.insert(
348 "combo".into(),
349 Value::text(format!("{t}{}{a}", config.separator)),
350 );
351 values.insert("title".into(), Value::text(t.clone()));
352 values.insert("artist".into(), Value::text(a.clone()));
353 }
354 (None, None, Some(url)) => {
355 values.insert("combo".into(), Value::text(url.clone()));
356 }
357 _ => (),
358 }
359 if let Some(volume) = player.volume {
360 values.insert(
361 "volume_icon".into(),
362 Value::icon_progression("volume", volume),
363 );
364 values.insert("volume".into(), Value::percents(volume * 100.0));
365 }
366 let mut widget = Widget::new().with_format(format.clone());
367 widget.set_values(values);
368 widget.state = state;
369 api.set_widget(widget)?;
370 }
371 None => {
372 let mut widget = Widget::new().with_format(format.clone());
373 widget.set_values(map!("icon" => Value::icon("music")));
374 api.set_widget(widget)?;
375 }
376 }
377
378 loop {
379 select! {
380 Some(msg) = properties_stream.next() => {
381 let msg = msg.unwrap();
382 let msg = PropertiesChanged::from_message(msg).unwrap();
383 let args = msg.args().unwrap();
384 let header = msg.message().header();
385 let sender = header.sender().unwrap();
386 if let Some((pos, player)) = players.iter_mut().enumerate().find(|p| &*p.1.owner == sender) {
387 let props = args.changed_properties;
388 if let Some(status) = props.get("PlaybackStatus") {
389 let status: &str = status.downcast_ref().unwrap();
390 player.status = PlaybackStatus::from_str(status);
391 }
392 if let Some(metadata) = props.get("Metadata") {
393 player.metadata =
394 zbus_mpris::PlayerMetadata::try_from(metadata.try_to_owned().unwrap()).unwrap();
395 }
396 if let Some(volume) = props.get("Volume") {
397 player.volume = Some(*volume.downcast_ref::<&f64>().unwrap());
398 }
399 if player.status == Some(PlaybackStatus::Playing)
400 && (
401 player.metadata.title.is_some()
402 || player.metadata.artist.is_some()
403 || player.metadata.url.is_some()
404 ) {
405 cur_player = Some(pos);
406 }
407 break;
408 }
409 }
410 Some(msg) = name_owner_changed_stream.next() => {
411 let msg = msg.unwrap();
412 let msg = NameOwnerChanged::from_message(msg).unwrap();
413 let args = msg.args().unwrap();
414 match (args.old_owner.as_ref(), args.new_owner.as_ref()) {
415 (None, Some(new)) => {
416 debug!("new player {} owned by {new}", args.name);
417 if player_matches(args.name.as_str(), &preferred_players, &exclude_regex) {
418 match Player::new(&dbus_conn, args.name.to_owned().into(), new.to_owned().into()).await {
419 Ok(player) => players.push(player),
420 Err(e) => {
421 debug!("{e}");
422 },
423 }
424 }
425 }
426 (Some(old), None) => {
427 if let Some(pos) = players.iter().position(|p| &*p.owner == old) {
428 debug!("removed player {} owned by {old}", args.name);
429 players.remove(pos);
430 if let Some(cur) = cur_player {
431 if players.is_empty() {
432 cur_player = None;
433 } else if pos == cur {
434 cur_player = Some(0);
435 } else if pos < cur {
436 cur_player = Some(cur - 1);
437 }
438 }
439 }
440 }
441 _ => (),
442 }
443 break;
444 }
445 Some(msg) = active_player_change_end_stream.next() => {
446 let args = msg.args().unwrap();
447 if let Some(pos) = players.iter().position(|p| p.bus_name == args.name){
448 cur_player = Some(pos);
449 }
450 else{
451 if let Err(e) = playerctld_proxy.shift().await{
454 debug!("{e}");
455 }
456 }
457 break;
458 }
459 Some(action) = actions.recv() => {
460 if let Some(i) = cur_player {
461 let player = &players[i];
462 match action.as_ref() {
463 "play_pause" => {
464 player.play_pause().await?;
465 }
466 "next" => {
467 player.next().await?;
468 }
469 "prev" => {
470 player.prev().await?;
471 }
472 "next_player" => {
473 cur_player = Some((i + 1) % players.len());
474 if let Err(e) = playerctld_proxy.shift().await{
475 debug!("{e}");
476 }
477 break;
478 }
479 "seek_forward" => {
480 player.seek(seek_forward_step).await?;
481 }
482 "seek_backward" => {
483 player.seek(seek_backward_step).await?;
484 }
485 "volume_up" => {
486 player.set_volume(volume_step).await?;
487 }
488 "volume_down" => {
489 player.set_volume(-volume_step).await?;
490 }
491 "toggle_format" => {
492 if let Some(format_alt) = &mut format_alt {
493 std::mem::swap(format_alt, &mut format);
494 break;
495 }
496 }
497 _ => (),
498 }
499 }
500 }
501 }
502 }
503 }
504}
505
506async fn get_players(
507 dbus_conn: &zbus::Connection,
508 preferred_players: &[String],
509 exclude_regex: &[Regex],
510) -> Result<Vec<Player>> {
511 let proxy = DBusProxy::new(dbus_conn)
512 .await
513 .error("failed to create DBusProxy")?;
514 let names = proxy
515 .list_names()
516 .await
517 .error("failed to list dbus names")?;
518 let mut players = Vec::new();
519 for name in names {
520 if player_matches(name.as_str(), preferred_players, exclude_regex) {
521 let owner = proxy.get_name_owner(name.as_ref()).await.unwrap();
522 match Player::new(dbus_conn, name, owner).await {
523 Ok(player) => players.push(player),
524 Err(e) => {
525 debug!("{e}");
526 }
527 }
528 }
529 }
530 Ok(players)
531}
532
533#[derive(Debug)]
534struct Player {
535 status: Option<PlaybackStatus>,
536 owner: OwnedUniqueName,
537 bus_name: OwnedBusName,
538 player_proxy: zbus_mpris::PlayerProxy<'static>,
539 metadata: zbus_mpris::PlayerMetadata,
540 volume: Option<f64>,
541}
542
543impl Player {
544 async fn new(
545 dbus_conn: &zbus::Connection,
546 bus_name: OwnedBusName,
547 owner: OwnedUniqueName,
548 ) -> Result<Player> {
549 debug!("creating Player for {bus_name}");
550
551 let proxy = zbus_mpris::PlayerProxy::builder(dbus_conn)
552 .destination(bus_name.clone())
553 .error("failed to set proxy destination")?
554 .build()
555 .await
556 .error("failed to open player proxy")?;
557
558 debug!("querying player metadata");
562 let metadata = proxy.metadata().await;
563 debug!("querying player status");
564 let status = proxy.playback_status().await;
565 debug!("querying player volume");
566 let volume = proxy.volume().await;
567
568 let metadata = metadata.error("failed to obtain player metadata")?;
569 let status = status.error("failed to obtain player status")?;
570
571 debug!("Player created");
572
573 Ok(Self {
574 status: PlaybackStatus::from_str(&status),
575 owner,
576 bus_name,
577 player_proxy: proxy,
578 metadata,
579 volume: volume.ok(),
580 })
581 }
582
583 async fn play_pause(&self) -> Result<()> {
584 self.player_proxy
585 .play_pause()
586 .await
587 .error("play_pause() failed")
588 }
589
590 async fn prev(&self) -> Result<()> {
591 self.player_proxy.previous().await.error("prev() failed")
592 }
593
594 async fn next(&self) -> Result<()> {
595 self.player_proxy.next().await.error("next() failed")
596 }
597
598 async fn seek(&self, offset: i64) -> Result<()> {
599 match self.player_proxy.seek(offset).await {
600 Err(zbus::Error::MethodError(e, _, _))
601 if e == "org.freedesktop.DBus.Error.NotSupported" =>
602 {
603 Ok(())
605 }
606 other => dbg!(other).error("seek() failed"),
607 }
608 }
609
610 async fn set_volume(&self, step_size: f64) -> Result<()> {
611 if let Some(volume) = self.volume {
612 self.player_proxy
613 .set_volume(volume + step_size)
614 .await
615 .error("set_volume() failed")?;
616 }
617 Ok(())
618 }
619}
620
621impl fmt::Display for Player {
622 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623 extract_player_name(&self.bus_name).unwrap().fmt(f)
624 }
625}
626
627#[derive(Debug, Clone, Copy, PartialEq, Eq)]
628enum PlaybackStatus {
629 Playing,
630 Paused,
631 Stopped,
632}
633
634impl PlaybackStatus {
635 fn from_str(s: &str) -> Option<Self> {
636 match s {
637 "Paused" => Some(Self::Paused),
638 "Playing" => Some(Self::Playing),
639 "Stopped" => Some(Self::Stopped),
640 _ => None,
641 }
642 }
643}
644
645fn extract_player_name(full_name: &str) -> Option<&str> {
646 const NAME_PREFIX: &str = "org.mpris.MediaPlayer2.";
647 full_name
648 .starts_with(NAME_PREFIX)
649 .then(|| &full_name[NAME_PREFIX.len()..])
650}
651
652fn player_matches(full_name: &str, preferred_players: &[String], exclude_regex: &[Regex]) -> bool {
653 let name = match extract_player_name(full_name) {
654 Some(name) => name,
655 None => return false,
656 };
657
658 exclude_regex.iter().all(|r| !r.is_match(name))
659 && (preferred_players.is_empty()
660 || preferred_players.iter().any(|p| name.starts_with(&**p)))
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666
667 #[test]
668 fn extract_player_name_test() {
669 assert_eq!(
670 extract_player_name("org.mpris.MediaPlayer2.firefox.instance852"),
671 Some("firefox.instance852")
672 );
673 assert_eq!(
674 extract_player_name("not.org.mpris.MediaPlayer2.firefox.instance852"),
675 None,
676 );
677 assert_eq!(
678 extract_player_name("org.mpris.MediaPlayer3.firefox.instance852"),
679 None,
680 );
681 }
682
683 #[test]
684 fn player_matches_test() {
685 let exclude = vec![Regex::new("mpd").unwrap(), Regex::new("firefox.*").unwrap()];
686 assert!(player_matches(
687 "org.mpris.MediaPlayer2.playerctld",
688 &[],
689 &exclude
690 ));
691 assert!(!player_matches(
692 "org.mpris.MediaPlayer2.playerctld",
693 &["spotify".into()],
694 &exclude
695 ));
696 assert!(!player_matches(
697 "org.mpris.MediaPlayer2.firefox.instance852",
698 &[],
699 &exclude
700 ));
701 }
702}