empress 1.6.0

A D-Bus MPRIS daemon for controlling media players.
use std::{
    collections::HashMap,
    fmt::Debug,
    time::{Duration, Instant},
};

use anyhow::{anyhow, Context};
use dbus::{
    arg::{Append, AppendAll, Arg, Get, ReadAll, RefArg, Variant},
    nonblock::{stdintf::org_freedesktop_dbus::Properties, Proxy, SyncConnection},
    strings::{BusName, Interface, Member, Path},
};
use log::{log_enabled, trace, Level};

use super::{mpris, mpris::player::PlaybackStatus};
use crate::{Offset, Result};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Player {
    pub status: PlaybackStatus,
    pub last_update: Instant,
    pub bus: BusName<'static>,
}

impl Player {
    pub async fn new(
        last_update: Instant,
        name: impl Into<BusName<'static>>,
        conn: &SyncConnection,
    ) -> Result<Self> {
        let mut ret = Self {
            status: PlaybackStatus::Stopped,
            last_update,
            bus: name.into(),
        };

        ret.status = ret.playback_status(conn).await?;

        Ok(ret)
    }

    async fn call<A: Debug + AppendAll, T: Debug + ReadAll + 'static>(
        &self,
        interface: &Interface<'_>,
        method: &Member<'_>,
        conn: &SyncConnection,
        args: A,
    ) -> Result<T> {
        let proxy = Proxy::new(&self.bus, &*mpris::ENTRY_PATH, Duration::from_secs(2), conn);

        let args_dbg = if log_enabled!(Level::Trace) {
            Some(format!("{:?}", args))
        } else {
            None
        };

        let res = proxy
            .method_call(interface, method, args)
            .await
            .with_context(|| {
                format!(
                    "calling {}::{} on player {} failed",
                    interface, method, self.bus
                )
            });

        if let Some(args_dbg) = args_dbg {
            trace!(
                "call {}::{} on {} with {} returned {:?}",
                interface,
                method,
                self.bus,
                args_dbg,
                res
            );
        }

        res
    }

    async fn get<T: Debug + for<'b> Get<'b> + 'static>(
        &self,
        interface: &Interface<'_>,
        prop: &Member<'_>,
        conn: &SyncConnection,
    ) -> Result<T> {
        let proxy = Proxy::new(&self.bus, &*mpris::ENTRY_PATH, Duration::from_secs(2), conn);

        let res = proxy.get(interface, prop).await.with_context(|| {
            format!(
                "getting {}::{} on player {} failed",
                interface, prop, self.bus
            )
        });

        trace!(
            "get {}::{} on {} returned {:?}",
            interface,
            prop,
            self.bus,
            res
        );

        res
    }

    async fn set<T: Debug + Arg + Append>(
        &self,
        interface: &Interface<'_>,
        prop: &Member<'_>,
        conn: &SyncConnection,
        value: T,
    ) -> Result<()> {
        let proxy = Proxy::new(&self.bus, &*mpris::ENTRY_PATH, Duration::from_secs(2), conn);

        let value_dbg = if log_enabled!(Level::Trace) {
            Some(format!("{:?}", value))
        } else {
            None
        };

        let res = proxy
            .set(&*mpris::player::INTERFACE, prop, value)
            .await
            .with_context(|| {
                format!(
                    "setting {}::{} on player {} failed",
                    interface, prop, self.bus
                )
            });

        if let Some(value_dbg) = value_dbg {
            trace!(
                "set {}::{} on {} to {} returned {:?}",
                interface,
                prop,
                self.bus,
                value_dbg,
                res
            );
        }

        res
    }

    //////// Methods under MediaPlayer2.Player ////////

    pub async fn next(self, conn: &SyncConnection) -> Result<Self> {
        self.call(&*mpris::player::INTERFACE, &*mpris::player::NEXT, conn, ())
            .await?;

        Ok(Self {
            last_update: Instant::now(),
            ..self
        })
    }

    pub async fn previous(self, conn: &SyncConnection) -> Result<Self> {
        self.call(
            &*mpris::player::INTERFACE,
            &*mpris::player::PREVIOUS,
            conn,
            (),
        )
        .await?;

        Ok(Self {
            last_update: Instant::now(),
            ..self
        })
    }

    pub async fn pause(self, conn: &SyncConnection) -> Result<Self> {
        self.call(&*mpris::player::INTERFACE, &*mpris::player::PAUSE, conn, ())
            .await?;

        Ok(Self {
            status: PlaybackStatus::Paused,
            last_update: Instant::now(),
            ..self
        })
    }

    pub async fn stop(self, conn: &SyncConnection) -> Result<Self> {
        self.call(&*mpris::player::INTERFACE, &*mpris::player::STOP, conn, ())
            .await?;

        Ok(Self {
            status: PlaybackStatus::Stopped,
            last_update: Instant::now(),
            ..self
        })
    }

    pub async fn play(self, conn: &SyncConnection) -> Result<Self> {
        self.call(&*mpris::player::INTERFACE, &*mpris::player::PLAY, conn, ())
            .await?;

        Ok(Self {
            status: PlaybackStatus::Playing,
            last_update: Instant::now(),
            ..self
        })
    }

    #[allow(clippy::cast_possible_truncation)]
    pub async fn set_position(
        self,
        conn: &SyncConnection,
        id: Path<'_>,
        secs: i64,
    ) -> Result<Self> {
        self.call(
            &*mpris::player::INTERFACE,
            &*mpris::player::SET_POSITION,
            conn,
            (id, secs),
        )
        .await?;

        Ok(Self {
            last_update: Instant::now(),
            ..self
        })
    }

    //////// Properties under MediaPlayer2 ////////

    pub async fn identity(&self, conn: &SyncConnection) -> Result<String> {
        self.get(&*mpris::root::INTERFACE, &*mpris::root::IDENTITY, conn)
            .await
    }

    //////// Properties under MediaPlayer2.Player ////////

    pub async fn playback_status(&self, conn: &SyncConnection) -> Result<PlaybackStatus> {
        self.get(
            &*mpris::player::INTERFACE,
            &*mpris::player::PLAYBACK_STATUS,
            conn,
        )
        .await
        .and_then(|s: String| s.parse().context("Invalid playback status"))
    }

    pub async fn metadata(
        &self,
        conn: &SyncConnection,
    ) -> Result<HashMap<String, Variant<Box<dyn RefArg>>>> {
        self.get(&*mpris::player::INTERFACE, &*mpris::player::METADATA, conn)
            .await
    }

    pub async fn volume(&self, conn: &SyncConnection) -> Result<f64> {
        self.get(&*mpris::player::INTERFACE, &*mpris::player::VOLUME, conn)
            .await
    }

    pub async fn set_volume(&self, conn: &SyncConnection, vol: f64) -> Result<()> {
        self.set(
            &*mpris::player::INTERFACE,
            &*mpris::player::VOLUME,
            conn,
            vol,
        )
        .await
    }

    #[allow(clippy::cast_precision_loss)]
    pub async fn position(&self, conn: &SyncConnection) -> Result<i64> {
        self.get(&*mpris::player::INTERFACE, &*mpris::player::POSITION, conn)
            .await
    }

    pub async fn can_go_next(&self, conn: &SyncConnection) -> Result<bool> {
        self.get(
            &*mpris::player::INTERFACE,
            &*mpris::player::CAN_GO_NEXT,
            conn,
        )
        .await
    }

    pub async fn can_go_previous(&self, conn: &SyncConnection) -> Result<bool> {
        self.get(
            &*mpris::player::INTERFACE,
            &*mpris::player::CAN_GO_PREVIOUS,
            conn,
        )
        .await
    }

    pub async fn can_play(&self, conn: &SyncConnection) -> Result<bool> {
        self.get(&*mpris::player::INTERFACE, &*mpris::player::CAN_PLAY, conn)
            .await
    }

    pub async fn can_pause(&self, conn: &SyncConnection) -> Result<bool> {
        self.get(&*mpris::player::INTERFACE, &*mpris::player::CAN_PAUSE, conn)
            .await
    }

    pub async fn can_seek(&self, conn: &SyncConnection) -> Result<bool> {
        self.get(&*mpris::player::INTERFACE, &*mpris::player::CAN_SEEK, conn)
            .await
    }

    pub async fn can_control(&self, conn: &SyncConnection) -> Result<bool> {
        self.get(
            &*mpris::player::INTERFACE,
            &*mpris::player::CAN_CONTROL,
            conn,
        )
        .await
    }

    //////// Empress-specific wrapper methods ////////

    pub async fn try_next(self, conn: &SyncConnection) -> Result<Option<Self>> {
        Ok(if self.can_go_next(conn).await? {
            Some(self.next(conn).await?)
        } else {
            None
        })
    }

    pub async fn try_previous(self, conn: &SyncConnection) -> Result<Option<Self>> {
        Ok(if self.can_go_previous(conn).await? {
            Some(self.previous(conn).await?)
        } else {
            None
        })
    }

    pub async fn try_pause(self, conn: &SyncConnection) -> Result<Option<Self>> {
        Ok(
            if self.playback_status(conn).await? == PlaybackStatus::Playing
                && self.can_pause(conn).await?
            {
                Some(self.pause(conn).await?)
            } else {
                None
            },
        )
    }

    pub async fn try_play_pause(self, conn: &SyncConnection) -> Result<Option<Self>> {
        Ok(match self.playback_status(conn).await? {
            PlaybackStatus::Playing if self.can_pause(conn).await? => Some(self.pause(conn).await?),
            PlaybackStatus::Paused if self.can_play(conn).await? => Some(self.play(conn).await?),
            _ => None,
        })
    }

    pub async fn try_stop(self, conn: &SyncConnection) -> Result<Option<Self>> {
        Ok(match self.playback_status(conn).await? {
            PlaybackStatus::Playing | PlaybackStatus::Paused if self.can_control(conn).await? => {
                Some(self.stop(conn).await?)
            },
            _ => None,
        })
    }

    pub async fn try_play(self, conn: &SyncConnection) -> Result<Option<Self>> {
        Ok(
            if self.playback_status(conn).await? != PlaybackStatus::Playing
                && self.can_play(conn).await?
            {
                Some(self.play(conn).await?)
            } else {
                None
            },
        )
    }

    #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
    pub async fn try_seek(self, conn: &SyncConnection, to: Offset) -> Result<Option<(Self, f64)>> {
        Ok(if self.can_seek(conn).await? {
            let meta = self.metadata(conn).await?;

            let pos = match to {
                Offset::Relative(p) => self.position(conn).await? + (p * 1e6).round() as i64,
                Offset::Absolute(p) => (p * 1e6).round() as i64,
            };

            Some((
                self.set_position(
                    conn,
                    Path::new(
                        meta.get(mpris::track_list::ATTR_TRACKID)
                            .ok_or_else(|| anyhow!("missing track ID in metadata"))?
                            .as_str()
                            .ok_or_else(|| anyhow!("track ID wasn't a string"))?,
                    )
                    .map_err(|s| anyhow!("track ID {:?} was not valid", s))?,
                    pos,
                )
                .await?,
                pos as f64 / 1e6,
            ))
        } else {
            None
        })
    }

    pub async fn try_set_volume(
        self,
        conn: &SyncConnection,
        vol: Offset,
    ) -> Result<Option<(Self, f64)>> {
        let (vol, set) = match vol {
            Offset::Relative(v) => {
                let prev = self.volume(conn).await?;
                let next = prev + v;

                if (next - prev).abs() > 1e-5 {
                    (next, true)
                } else {
                    (prev, false)
                }
            },
            Offset::Absolute(v) => (v, true),
        };

        if !vol.is_finite() {
            return Err(anyhow!("Invalid volume {:?}", vol));
        }

        Ok(if set {
            if self.can_control(conn).await? {
                // Safety check
                let vol = vol.max(0.0).min(1.0);

                self.set_volume(conn, vol).await?;

                Some((self, vol))
            } else {
                None
            }
        } else {
            Some((self, vol))
        })
    }
}

impl PartialOrd for Player {
    fn partial_cmp(&self, rhs: &Player) -> Option<std::cmp::Ordering> { Some(self.cmp(rhs)) }
}

impl Ord for Player {
    fn cmp(&self, rhs: &Player) -> std::cmp::Ordering {
        self.status
            .cmp(&rhs.status)
            .then_with(|| rhs.last_update.cmp(&self.last_update))
            .then_with(|| self.bus.cmp(&rhs.bus))
    }
}