mpris-notifier 0.1.2

Generates XDG desktop notifications for MPRIS status changes (track changes)
use crate::dbus::{DBusConnection, DBusError};
use rustbus::message_builder::MarshalledMessage;
use rustbus::wire::unmarshal::traits::Variant;
use std::collections::HashMap;
use std::str::FromStr;

const MPRIS_INTERFACE: &str = "org.mpris.MediaPlayer2.Player";
const MPRIS_SIGNAL_INTERFACE: &str = "org.freedesktop.DBus.Properties";
const MPRIS_SIGNAL_MEMBER: &str = "PropertiesChanged";
const MPRIS_SIGNAL_OBJECT: &str = "/org/mpris/MediaPlayer2";

#[derive(Debug, Clone)]
pub struct PlayerMetadata {
    pub status: PlayerStatus,
    pub track_id: String,

    pub album: Option<String>,
    pub album_artists: Option<Vec<String>>,
    pub art_url: Option<String>,
    pub artists: Option<Vec<String>>,
    pub title: Option<String>,
    pub track_number: Option<u32>,
    pub track_url: Option<String>,
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum PlayerStatus {
    Playing,
    Paused,
    Stopped,
}

impl FromStr for PlayerStatus {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "Playing" => Ok(PlayerStatus::Playing),
            "Paused" => Ok(PlayerStatus::Paused),
            "Stopped" => Ok(PlayerStatus::Stopped),
            _ => Err(()),
        }
    }
}

impl TryFrom<MarshalledMessage> for PlayerMetadata {
    type Error = DBusError;

    fn try_from(message: MarshalledMessage) -> Result<Self, Self::Error> {
        let mut parser = message.body.parser();
        let interface_name: &str = parser.get()?;
        if interface_name != MPRIS_INTERFACE {
            return Err(DBusError::Invalid(format!(
                "wrong interface type '{}'",
                interface_name
            )));
        }

        let outer: HashMap<String, Variant> = parser.get()?;
        let inner: HashMap<String, Variant> = outer
            .get("Metadata")
            .ok_or_else(|| DBusError::Invalid("Missing Metadata envelope".to_string()))?
            .get()?;

        Ok(Self {
            status: PlayerStatus::from_str(
                outer
                    .get("PlaybackStatus")
                    .ok_or_else(|| {
                        DBusError::Invalid("Missing PlaybackStatus envelope".to_string())
                    })?
                    .get()?,
            )
            .unwrap_or(PlayerStatus::Stopped),
            track_id: inner["mpris:trackid"].get()?,

            album: inner.get("xesam:album").and_then(|x| x.get().ok()),
            album_artists: inner.get("xesam:albumArtist").and_then(|x| x.get().ok()),
            art_url: inner.get("mpris:artUrl").and_then(|x| x.get().ok()),
            artists: inner.get("xesam:artist").and_then(|x| x.get().ok()),
            title: inner.get("xesam:title").and_then(|x| x.get().ok()),
            track_number: inner.get("xesam:trackNumber").and_then(|x| x.get().ok()),
            track_url: inner.get("xesam:url").and_then(|x| x.get().ok()),
        })
    }
}

// Convenience method to subscribe a DBusConnection to MPRIS player property
// change events (e.g., track changes).
pub fn subscribe_mpris(dbus: &mut DBusConnection) -> Result<(), DBusError> {
    dbus.subscribe(
        MPRIS_SIGNAL_INTERFACE,
        MPRIS_SIGNAL_MEMBER,
        MPRIS_SIGNAL_OBJECT,
    )
}