mpdclient 0.2.0

Rust interface to MPD using libmpdclient
Documentation
use std::ffi::CStr;
use std::{ffi::CString, fmt::Display};

use mpdclient_sys::{
    mpd_consume_state, mpd_consume_state_MPD_CONSUME_OFF, mpd_consume_state_MPD_CONSUME_ON,
    mpd_consume_state_MPD_CONSUME_ONESHOT, mpd_consume_state_MPD_CONSUME_UNKNOWN,
    mpd_lookup_consume_state, mpd_lookup_single_state, mpd_parse_consume_state,
    mpd_parse_single_state, mpd_run_status, mpd_single_state, mpd_single_state_MPD_SINGLE_OFF,
    mpd_single_state_MPD_SINGLE_ON, mpd_single_state_MPD_SINGLE_ONESHOT,
    mpd_single_state_MPD_SINGLE_UNKNOWN, mpd_state, mpd_state_MPD_STATE_PAUSE,
    mpd_state_MPD_STATE_PLAY, mpd_state_MPD_STATE_STOP, mpd_status, mpd_status_get_audio_format,
    mpd_status_get_consume_state, mpd_status_get_crossfade, mpd_status_get_elapsed_ms,
    mpd_status_get_error, mpd_status_get_kbit_rate, mpd_status_get_mixrampdb,
    mpd_status_get_mixrampdelay, mpd_status_get_next_song_id, mpd_status_get_next_song_pos,
    mpd_status_get_partition, mpd_status_get_queue_length, mpd_status_get_queue_version,
    mpd_status_get_random, mpd_status_get_repeat, mpd_status_get_single_state,
    mpd_status_get_song_id, mpd_status_get_song_pos, mpd_status_get_state,
    mpd_status_get_total_time, mpd_status_get_update_id, mpd_status_get_volume,
};

use super::{Connection, queue::Version};
use crate::{AudioFormat, Error, entity::song::Id, error::Result, i32_to_id};

/// Intermediate to bundle MPD status functions, mostly player related.
pub struct Status {
    inner: *mut mpd_status,
}

impl Status {
    pub(super) fn new(connection: &Connection) -> Result<Self> {
        let status = unsafe {
            let status = mpd_run_status(connection.connection());
            if status.is_null() {
                let c_str = CStr::from_ptr(mpd_status_get_error(status));
                return Err(Error::Status(c_str.to_string_lossy().to_string()));
            }
            status
        };
        Ok(Self { inner: status })
    }

    /// Returns the current volume of MPD.
    #[must_use]
    pub fn volume(&self) -> Option<i32> {
        let volume = unsafe { mpd_status_get_volume(self.inner) };
        if volume == -1 { None } else { Some(volume) }
    }

    /// Returns if reapeat mode is active.
    #[must_use]
    pub fn repeat(&self) -> bool {
        unsafe { mpd_status_get_repeat(self.inner) }
    }

    /// Returns if random mode is active.
    #[must_use]
    pub fn random(&self) -> bool {
        unsafe { mpd_status_get_random(self.inner) }
    }

    /// Returns the current [`SingleState`].
    #[must_use]
    pub fn single_state(&self) -> SingleState {
        SingleState::from(unsafe { mpd_status_get_single_state(self.inner) })
    }

    /// Returns the current [`ConsumeState`].
    #[must_use]
    pub fn consume_state(&self) -> ConsumeState {
        ConsumeState::from(unsafe { mpd_status_get_consume_state(self.inner) })
    }

    /// Returns the length of the current queue
    #[must_use]
    pub fn queue_length(&self) -> u32 {
        unsafe { mpd_status_get_queue_length(self.inner) }
    }

    /// Returns the current version of the queue. Used to get updates to the queue since a version
    /// with
    ///
    /// - [`Queue::changes_meta()`](crate::connection::queue::Queue::changes_meta())
    /// - [`Queue::changes_meta_range()`](crate::connection::queue::Queue::changes_meta_range())
    /// - [`Queue::changes()`](crate::connection::queue::Queue::changes())
    /// - [`Queue::changes_range()`](crate::connection::queue::Queue::changes_range())
    #[must_use]
    pub fn queue_version(&self) -> Version {
        unsafe { mpd_status_get_queue_version(self.inner) }
    }

    /// Returns the current playback [State]
    #[must_use]
    pub fn state(&self) -> State {
        State::from(unsafe { mpd_status_get_state(self.inner) })
    }

    /// Returns the crossfade setting in seconds. `None` means crossfade is disabled.
    #[must_use]
    pub fn crossfade(&self) -> Option<u32> {
        let crsfd = unsafe { mpd_status_get_crossfade(self.inner) };

        if crsfd == 0 { None } else { Some(crsfd) }
    }

    /// Returns the mixramp setting. `None` means mixramp is disabled.
    #[must_use]
    pub fn mixrampdb(&self) -> Option<f32> {
        let mixdb = unsafe { mpd_status_get_mixrampdb(self.inner) };

        if mixdb == 0.0 { None } else { Some(mixdb) }
    }

    /// Returns the mixrampdelay setting in fractional seconds. `None` means the delay is disabled.
    #[must_use]
    pub fn mixrampdelay(&self) -> Option<f32> {
        let mixdelay = unsafe { mpd_status_get_mixrampdelay(self.inner) };

        if mixdelay < 0.0 { None } else { Some(mixdelay) }
    }

    /// Returns the position of the currently playing song. `None` means no song is playing.
    #[allow(clippy::cast_sign_loss)]
    #[must_use]
    pub fn current_song_pos(&self) -> Option<u32> {
        let pos = unsafe { mpd_status_get_song_pos(self.inner) };

        if pos == -1 { None } else { Some(pos as u32) }
    }

    /// Returns the id of the currently playing song. `None` means no song is playing.
    #[must_use]
    pub fn current_song_id(&self) -> Option<Id> {
        let id = unsafe { mpd_status_get_song_id(self.inner) };

        if id == -1 { None } else { Some(i32_to_id(id)) }
    }

    /// Returns the position of the next song. `None` means there is no next song.
    #[allow(clippy::cast_sign_loss)]
    #[must_use]
    pub fn next_song_pos(&self) -> Option<u32> {
        let pos = unsafe { mpd_status_get_next_song_pos(self.inner) };

        if pos == -1 { None } else { Some(pos as u32) }
    }

    /// Returns the id of the next song. `None` means there is no next song.
    #[must_use]
    pub fn next_song_id(&self) -> Option<Id> {
        let id = unsafe { mpd_status_get_next_song_id(self.inner) };

        if id == -1 { None } else { Some(i32_to_id(id)) }
    }

    /// Returns the elapsed milliseconds of the current song.
    #[must_use]
    pub fn elapsed_ms(&self) -> u32 {
        unsafe { mpd_status_get_elapsed_ms(self.inner) }
    }

    /// Returns the total seconds of the current song.
    #[must_use]
    pub fn total_time(&self) -> u32 {
        unsafe { mpd_status_get_total_time(self.inner) }
    }

    /// Returns the bit rate of the current song in kbit. `None` means the rate is unknown.
    #[must_use]
    pub fn kbit_rate(&self) -> Option<u32> {
        let kbit = unsafe { mpd_status_get_kbit_rate(self.inner) };

        if kbit == 0 { None } else { Some(kbit) }
    }

    /// Returns the [`AudioFormat`] of the current song. `None` means MPD is not playing or the
    /// format is unknown.
    #[must_use]
    pub fn audio_format(&self) -> Option<AudioFormat> {
        unsafe {
            let audio_format = mpd_status_get_audio_format(self.inner);
            if audio_format.is_null() {
                None
            } else {
                Some(AudioFormat::from(*audio_format))
            }
        }
    }

    /// Returns if MPD is currently updating the database.
    #[must_use]
    pub fn update(&self) -> bool {
        let id = unsafe { mpd_status_get_update_id(self.inner) };
        match id {
            0 => false,
            1 => true,
            _ => unreachable!(),
        }
    }

    /// Returns the name of the current partition. `None` means the server didn't specify one.
    #[must_use]
    pub fn partition(&self) -> Option<String> {
        unsafe {
            let ptr = mpd_status_get_partition(self.inner);
            if ptr.is_null() {
                None
            } else {
                Some(CStr::from_ptr(ptr).to_string_lossy().to_string())
            }
        }
    }
}

// --- State ---

/// Playback state
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State {
    /// Not playing
    Stop = 1,

    /// Playing
    Play = 2,

    /// Playing, but paused
    Pause = 3,
}

impl From<mpd_state> for State {
    fn from(value: mpd_state) -> Self {
        #[allow(non_upper_case_globals)]
        match value {
            mpd_state_MPD_STATE_STOP => Self::Stop,
            mpd_state_MPD_STATE_PLAY => Self::Play,
            mpd_state_MPD_STATE_PAUSE => Self::Pause,
            _ => unreachable!(),
        }
    }
}

// --- SingleState ---

/// Possible single states
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::cast_possible_wrap)]
pub enum SingleState {
    /// Disabled, usual playback, queue is shuffled after completly played.
    Off = mpd_single_state_MPD_SINGLE_OFF as isize,

    /// Playback stops after current song, or repeated if repeat mode is enabled.
    On = mpd_single_state_MPD_SINGLE_ON as isize,

    /// Single state is enabled for a single song, the disabled.
    Oneshot = mpd_single_state_MPD_SINGLE_ONESHOT as isize,
}

impl From<mpd_single_state> for SingleState {
    fn from(value: mpd_single_state) -> Self {
        #[allow(non_upper_case_globals)]
        match value {
            mpd_single_state_MPD_SINGLE_OFF => Self::Off,
            mpd_single_state_MPD_SINGLE_ON => Self::On,
            mpd_single_state_MPD_SINGLE_ONESHOT => Self::Oneshot,
            _ => unreachable!(),
        }
    }
}

impl TryFrom<&str> for SingleState {
    type Error = Error;

    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
        let state = unsafe { mpd_parse_single_state(CString::new(value)?.as_ptr()) };
        if state == mpd_single_state_MPD_SINGLE_UNKNOWN {
            Err(Error::Unknown("SingleState".to_string()))
        } else {
            Ok(Self::from(state))
        }
    }
}

impl Display for SingleState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", unsafe {
            CStr::from_ptr(mpd_lookup_single_state(*self as u32))
                .to_string_lossy()
                .to_string()
        })
    }
}

// --- ConsumeState ---

/// Song consumation state.
///
/// Consumed songs are removed from the queue after they played.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::cast_possible_wrap)]
pub enum ConsumeState {
    /// Disabled
    Off = mpd_consume_state_MPD_CONSUME_OFF as isize,

    /// Enabled
    On = mpd_consume_state_MPD_CONSUME_ON as isize,

    /// Consume is enabled for a single song, the disabled.
    Oneshot = mpd_consume_state_MPD_CONSUME_ONESHOT as isize,
}

impl From<mpd_consume_state> for ConsumeState {
    fn from(value: mpd_consume_state) -> Self {
        #[allow(non_upper_case_globals)]
        match value {
            mpd_consume_state_MPD_CONSUME_OFF => Self::Off,
            mpd_consume_state_MPD_CONSUME_ON => Self::On,
            mpd_consume_state_MPD_CONSUME_ONESHOT => Self::Oneshot,
            _ => unreachable!(),
        }
    }
}

impl TryFrom<&str> for ConsumeState {
    type Error = Error;

    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
        let state = unsafe { mpd_parse_consume_state(CString::new(value)?.as_ptr()) };
        if state == mpd_consume_state_MPD_CONSUME_UNKNOWN {
            Err(Error::Unknown("ConsumeState".to_string()))
        } else {
            Ok(Self::from(state))
        }
    }
}

impl Display for ConsumeState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", unsafe {
            CStr::from_ptr(mpd_lookup_consume_state(*self as u32))
                .to_string_lossy()
                .to_string()
        })
    }
}

#[cfg(test)]
mod tests {
    use crate::{ConsumeState, SingleState};

    #[test]
    fn single_state_display() {
        assert_eq!(SingleState::Off.to_string(), "0");
        assert_eq!(SingleState::On.to_string(), "1");
        assert_eq!(SingleState::Oneshot.to_string(), "oneshot");
    }

    #[test]
    fn single_state_parse() -> eyre::Result<()> {
        assert_eq!(SingleState::try_from("0")?, SingleState::Off);
        assert_eq!(SingleState::try_from("1")?, SingleState::On);
        assert_eq!(SingleState::try_from("oneshot")?, SingleState::Oneshot);
        Ok(())
    }

    #[test]
    fn consume_state_display() {
        assert_eq!(ConsumeState::Off.to_string(), "0");
        assert_eq!(ConsumeState::On.to_string(), "1");
        assert_eq!(ConsumeState::Oneshot.to_string(), "oneshot");
    }

    #[test]
    fn consume_state_parse() -> eyre::Result<()> {
        assert_eq!(ConsumeState::try_from("0")?, ConsumeState::Off);
        assert_eq!(ConsumeState::try_from("1")?, ConsumeState::On);
        assert_eq!(ConsumeState::try_from("oneshot")?, ConsumeState::Oneshot);
        Ok(())
    }
}