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