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