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}