mpdclient 0.2.0

Rust interface to MPD using libmpdclient
Documentation
use std::ffi::CString;

use database::Database;
use error::{Error, MpdError, Result};
use message::Channel;
use mpdclient_sys::{
    mpd_connection, mpd_connection_cmp_server_version, mpd_connection_free,
    mpd_connection_get_error, mpd_connection_get_server_version, mpd_connection_get_settings,
    mpd_connection_new, mpd_connection_set_keepalive, mpd_connection_set_timeout,
    mpd_send_list_playlists,
};
use player::Player;
use playlist::StoredPlaylist;
use queue::Queue;
use replaygain::ReplayGain;
use search::Search;
use stats::Stats;
use status::Status;

use crate::{
    connection::{idle::Idle, mixer::Mixer},
    entity::playlist::PlaylistReceiver,
    error,
    settings::Settings,
};

pub use message::Message;
pub use playlist::QueueSaveMode;
pub use queue::Position;
pub use replaygain::ReplayGainMode;
pub use search::Constraint;
pub use status::{ConsumeState, SingleState, State};

/// MPD database functions
pub mod database;

/// MPD idle functions
pub mod idle;

/// MPD messaging/channel functions
pub mod message;

/// MPD mixer functions
pub mod mixer;

/// MPD player functions
pub mod player;

/// MPD playlist functions
pub mod playlist;

/// MPD queue control functions
pub mod queue;

/// MPD replaygain functions and types
pub mod replaygain;

/// MPD search functions
pub mod search;

/// MPD stats
pub mod stats;

/// MPD status fuctions
pub mod status;

// TODO: async connection
// TODO: get_fd

/// Representation of a connection to a MPD server.
#[derive(Debug)]
pub struct Connection {
    // Using an Option here to allow override the automatically called drop() function
    // https://doc.rust-lang.org/stable/nomicon/destructors.html#destructors
    inner: Option<*mut mpd_connection>,
}

impl Connection {
    /// Internal library function to get easy access to the underlying [`mpd_connection`] "object"
    /// pointer.
    pub(crate) fn connection(&self) -> *mut mpd_connection {
        self.inner.unwrap_or_else(|| unreachable!())
    }

    /// Internal library function to wrap a direct ffi function, that may return `NULL` on
    /// error
    ///
    /// # Returns
    ///
    /// Returns an `Result` with the return type from the ffi function as `Ok` and [`Error::Mpd`]
    /// as `Err`
    pub(crate) fn get_error<T>(&self, func: impl Fn() -> T) -> Result<T> {
        let ret = func();
        let conn_err = MpdError::from_sys(
            unsafe { mpd_connection_get_error(self.connection()) },
            self.connection(),
        );
        if let Some(error) = conn_err {
            Err(Error::from_mpd(error, self.connection()))
        } else {
            Ok(ret)
        }
    }

    /// Internal library function to wrap a direct ffi function, that returns `false` on
    /// error
    ///
    /// # Returns
    ///
    /// Returns an `Result` with the return type from the ffi function as `Ok` and [`Error::Mpd`]
    /// as `Err`
    pub(crate) fn get_bool_error(&self, func: impl Fn() -> bool) -> Result<()> {
        if func() {
            return Ok(());
        }
        // unreachable!(), because error is ensured with func() == false
        Err(Error::from_mpd(
            MpdError::from_sys(
                unsafe { mpd_connection_get_error(self.connection()) },
                self.connection(),
            )
            .unwrap_or_else(|| unreachable!()),
            self.connection(),
        ))
    }

    /// Creates a new [`Connection`], allowing to modify all possible parameters.
    ///
    /// For all default connection parameters [`Connection::new()`] can be used.
    ///
    /// # Arguments
    ///
    /// - `host`: If `None`, libmpdclient will use `MPD_HOST` if set, or `"localhost"` if not
    /// - `port`: If `0`, libmpdclient will use `MPD_PORT` if set, or `6600` if not
    /// - `timeout` (in ms): If `0`, libmpdclient will use `MPD_TIMEOUT` if set, or `30000` if not
    ///
    /// # Errors
    ///
    /// This function will return an error if
    /// - [`Error::Mpd`] MPD returns an error
    #[doc(alias = "mpd_connection_new")]
    pub fn connect(host: Option<&str>, port: u16, timeout: u32) -> Result<Self> {
        let port = u32::from(port);
        let inner = unsafe {
            let connection = if let Some(host) = host {
                let h = CString::new(host)?;
                mpd_connection_new(h.as_ptr(), port, timeout)
            } else {
                mpd_connection_new(std::ptr::null(), port, timeout)
            };
            let err = MpdError::from_sys(mpd_connection_get_error(connection), connection);
            if let Some(error) = err {
                return Err(Error::from_mpd(error, connection));
            }
            connection
        };
        Ok(Connection { inner: Some(inner) })
    }

    /// Creates a new [`Connection`] with the default parameters or the environment variables
    /// `MPD_HOST` (host), `MPD_PORT` (port) and `MPD_TIMEOUT` (timeout) if available.
    ///
    /// To control the parameters directly [`Connection::connect()`] can be used.
    ///
    /// # Errors
    ///
    /// This function will return an error if
    /// - [`Error::Mpd`] MPD returns an error
    #[doc(alias = "mpd_connection_new")]
    pub fn new() -> Result<Self> {
        Self::connect(None, 0, 0)
    }

    /// Returns the MPD protocol version as Tuple.
    ///
    /// # Errors
    ///
    /// This function will return an error if
    /// - [`Error::Mpd`] MPD returns an error
    pub fn version(&self) -> Result<(u32, u32, u32)> {
        unsafe {
            let ver_ptr =
                self.get_error(|| mpd_connection_get_server_version(self.connection()))?;
            Ok((*ver_ptr, *ver_ptr.add(1), *ver_ptr.add(2)))
        }
    }

    /// Returns the relative state of the server protocol version to a version.
    ///
    /// # Errors
    ///
    /// This function will return an error if
    /// - [`Error::Mpd`] MPD returns an error
    pub fn cmp_version(&self, version: (u32, u32, u32)) -> Result<ProtoVerCmp> {
        unsafe {
            let i = self.get_error(|| {
                mpd_connection_cmp_server_version(
                    self.connection(),
                    version.0,
                    version.1,
                    version.2,
                )
            })?;

            Ok(match i {
                -1 => ProtoVerCmp::Older,
                0 => ProtoVerCmp::Equal,
                1 => ProtoVerCmp::Newer,
                _ => unreachable!("mpd_connection_cmp_server_version only returns -1, 0, and 1"),
            })
        }
    }

    /// Returns the [`Settings`].
    ///
    /// # Errors
    ///
    /// Returns an [`Error::Mpd`] if MPD returns an error.
    pub fn settings(&self) -> Result<Settings> {
        let settings =
            unsafe { self.get_error(|| mpd_connection_get_settings(self.connection()))? };
        Ok(Settings::from(settings))
    }

    /// Enables/disables TCP keepalive. May be required for connections to persist on some Networks.
    ///
    /// Disabled by default.
    ///
    /// # Errors
    ///
    /// This function will return an [`Error::KeepAlive`] if keepalive failed (`SO_KEEPALIVE`
    /// on setsockopt)
    pub fn set_keepalive(&self, keepalive: bool) -> Result<()> {
        if unsafe { mpd_connection_set_keepalive(self.connection(), keepalive) } {
            Ok(())
        } else {
            Err(Error::KeepAlive)
        }
    }

    /// Sets the timeout in ms.
    ///
    /// # Errors
    ///
    /// Returns an [`Error::TimeoutNull`] if the requested timeout is `0`
    pub fn set_timeout(&self, timeout: u32) -> Result<()> {
        if timeout == 0 {
            return Err(Error::TimeoutNull);
        }
        unsafe { mpd_connection_set_timeout(self.connection(), timeout) }
        Ok(())
    }

    /// Returns the stored playlists of the server as an iterator.
    ///
    /// # Errors
    ///
    /// Returns an [`Error::Mpd`] if MPD returns and error.
    pub fn stored_playlists(&self) -> Result<PlaylistReceiver<'_>> {
        self.get_bool_error(|| unsafe { mpd_send_list_playlists(self.connection()) })?;
        Ok(PlaylistReceiver::new(self))
    }

    // --- Submodules ---
    /// Read and control the queue
    #[must_use]
    pub fn queue(&self) -> Queue<'_> {
        Queue::new(self)
    }

    /// Returns the current status.
    ///
    /// # Errors
    ///
    /// Returns an [`Error::Status`] if MPD returns an error.
    pub fn status(&self) -> Result<Status> {
        Status::new(self)
    }

    /// Read and control the database.
    #[must_use]
    pub fn database(&self) -> Database<'_> {
        Database::new(self)
    }

    /// Read and control a playlist stored.
    ///
    /// # Errors
    ///
    /// Returns an [`Error::Nul`] if `name` is an invalid [`std::ffi::CStr`]
    pub fn playlist(&self, name: &str) -> Result<StoredPlaylist<'_>> {
        StoredPlaylist::new(self, name)
    }

    /// Read and control the replaygain.
    #[must_use]
    pub fn replaygain(&self) -> ReplayGain<'_> {
        ReplayGain::new(self)
    }

    /// Use the search functions.
    #[must_use]
    pub fn search(&self) -> Search<'_> {
        Search::new(self)
    }

    /// Read database and player stats.
    ///
    /// # Errors
    ///
    /// Returns an [`Error::Mpd`] if MPD returns an error.
    pub fn stats(&self) -> Result<Stats> {
        Stats::new(self)
    }

    /// Get the currently playing [`crate::Song`] and control the player.
    #[must_use]
    pub fn player(&self) -> Player<'_> {
        Player::new(self)
    }

    /// Interface to connect to [`Channel`]s.
    #[must_use]
    pub fn channel(&self) -> Channel<'_> {
        Channel::new(self)
    }

    #[must_use]
    /// Controls connection idle mode.
    pub fn idle(&self) -> Idle<'_> {
        Idle::new(self)
    }

    #[must_use]
    /// Controls connection volume.
    pub fn mixer(&self) -> Mixer<'_> {
        Mixer::new(self)
    }
}

#[doc(hidden)]
impl Drop for Connection {
    /// Custom drop is used, because [`mpd_connection_free`] gracefully disconnects the client
    /// from the server
    fn drop(&mut self) {
        unsafe {
            mpd_connection_free(self.inner.take().unwrap());
        }
    }
}

unsafe impl Send for Connection {}

/// Describes results from a protocol version comparison
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProtoVerCmp {
    /// The server protocol version is **older** than the version
    Older,
    /// The server protocol version is **equal** to the version
    Equal,
    /// The server protocol version is **newer** to the version
    Newer,
}

#[cfg(test)]
mod tests {
    use crate::Connection;

    #[test]
    fn drop() {
        {
            let _conn = Connection::new();
        }
    }
}