mpdclient 0.2.0

Rust interface to MPD using libmpdclient
Documentation
use std::{
    ffi::CStr,
    time::{Duration, SystemTime, UNIX_EPOCH},
};

#[cfg(feature = "protocol_0_24")]
use mpdclient_sys::mpd_song_get_added;
use mpdclient_sys::{
    mpd_song, mpd_song_get_audio_format, mpd_song_get_duration, mpd_song_get_duration_ms,
    mpd_song_get_end, mpd_song_get_id, mpd_song_get_last_modified, mpd_song_get_pos,
    mpd_song_get_prio, mpd_song_get_start, mpd_song_get_tag, mpd_song_get_uri,
};

use crate::{
    AudioFormat, Tag,
    error::{Error, Result},
};

use super::{Entity, EntityReceiver};

/// Song ID within the queue.
pub type Id = u32;

/// A Song in the MPD Database, can be added to a playlist or the queue.
#[derive(Debug)]
pub struct Song {
    inner: *mut mpd_song,
}

impl Song {
    pub(super) fn new(mpd_song: *mut mpd_song) -> Self {
        Self { inner: mpd_song }
    }

    /// Receives one pending [Song] after a command
    /// !internal Only use after requesting exactly one [`Song`]
    pub(crate) fn extract_one(mut entity_recv: EntityReceiver) -> Result<Self> {
        let Some(entity) = entity_recv.next() else {
            return Err(Error::NoEntity);
        };
        let Entity::Song(song) = entity? else {
            return Err(Error::EntityReturnType);
        };
        if entity_recv.next().is_some() {
            return Err(Error::MultipleEntities);
        }
        Ok(song)
    }

    /// Receives all pending [Song]s after a command
    /// !internal Only use after requesting only [`Song`]s
    pub(crate) fn extract_all(entity_recv: EntityReceiver) -> Result<Vec<Self>> {
        let mut songs: Vec<Song> = vec![];
        for entity in entity_recv {
            let Entity::Song(song) = entity? else {
                return Err(Error::EntityReturnType);
            };
            songs.push(song);
        }
        Ok(songs)
    }

    /// Returns the URI of the song.  This is either a path relative to the MPD music directory
    /// (without leading slash), or an URL with a scheme, e.g. a HTTP URL for a radio stream.
    #[must_use]
    pub fn uri(&self) -> String {
        unsafe {
            CStr::from_ptr(mpd_song_get_uri(self.inner))
                .to_string_lossy()
                .to_string()
        }
    }

    /// Access to the metadata [`Tag`]s of the [`Song`]
    #[must_use]
    pub fn tag(&self, tag: Tag) -> ReturnTag<'_> {
        ReturnTag::new(self, tag)
    }

    /// Returns the duration in seconds.
    #[must_use]
    pub fn duration(&self) -> Option<u32> {
        let dur = unsafe { mpd_song_get_duration(self.inner) };
        if dur == 0 { None } else { Some(dur) }
    }

    /// Returns the duration in milliseconds.
    #[must_use]
    pub fn duration_ms(&self) -> Option<u32> {
        let dur = unsafe { mpd_song_get_duration_ms(self.inner) };
        if dur == 0 { None } else { Some(dur) }
    }

    /// Returns the start point of the song within the actual file in seconds.
    #[must_use]
    pub fn start(&self) -> u32 {
        unsafe { mpd_song_get_start(self.inner) }
    }

    /// Returns the end point of the song within the actual file in seconds. For `None`, the song
    /// plays to the end of the file.
    #[must_use]
    pub fn end(&self) -> Option<u32> {
        let end = unsafe { mpd_song_get_end(self.inner) };
        if end == 0 { None } else { Some(end) }
    }

    /// Returns the time the song file was last modified.
    // allow because it should be correct in the c library
    #[allow(clippy::cast_sign_loss)]
    #[must_use]
    pub fn last_modified(&self) -> Option<SystemTime> {
        let time = unsafe { mpd_song_get_last_modified(self.inner) };
        if time == 0 {
            None
        } else {
            Some(UNIX_EPOCH + Duration::from_secs(time as u64))
        }
    }

    /// Returns the time the song was added to the database.
    #[cfg(feature = "protocol_0_24")]
    // allow because it should be correct in the c library
    #[allow(clippy::cast_sign_loss)]
    #[must_use]
    pub fn added(&self) -> Option<SystemTime> {
        let time = unsafe { mpd_song_get_added(self.inner) };
        if time == 0 {
            None
        } else {
            Some(UNIX_EPOCH + Duration::from_secs(time as u64))
        }
    }

    // only works locally, useless?
    // /// Sets the position in the queue. Only possible if the song is in the queue.
    // ///
    // /// Useful for applying values from
    // /// [`Queue::changes()`](crate::connection::queue::Queue::changes())
    // pub fn set_pos(&self, position: u32) {
    //     unsafe {
    //         mpd_song_set_pos(self.inner, position);
    //     }
    // }

    /// Returns the current position in the queue. If the song is not received from the queue, the
    /// value is invalid, even though the function doesn't indicate it.
    #[must_use]
    pub fn pos(&self) -> u32 {
        unsafe { mpd_song_get_pos(self.inner) }
    }

    /// Returns the unique id in the queue. If the song is not received from the queue, the
    /// value is invalid, even though the function doesn't indicate it.
    #[must_use]
    pub fn id(&self) -> Id {
        unsafe { mpd_song_get_id(self.inner) }
    }

    /// Returns the priority in the queue. If the song is not received from the queue, the
    /// value is invalid, even though the function doesn't indicate it.
    #[must_use]
    pub fn prio(&self) -> u32 {
        unsafe { mpd_song_get_prio(self.inner) }
    }

    /// Returns the [`AudioFormat`].
    #[must_use]
    pub fn audio_format(&self) -> Option<AudioFormat> {
        unsafe {
            let format = mpd_song_get_audio_format(self.inner);
            if format.is_null() {
                return None;
            }
            Some(AudioFormat::from(*format))
        }
    }
}

unsafe impl Send for Song {}

// --- TypeTags ---
/// Iterator to get all values of a [`Tag`].
#[derive(Debug)]
pub struct ReturnTag<'a> {
    song: &'a Song,
    tag: Tag,
    pass: u32,
}

impl<'a> ReturnTag<'a> {
    pub(crate) fn new(song: &'a Song, tag: Tag) -> Self {
        Self { song, tag, pass: 0 }
    }

    fn get_tag(&self, idx: u32) -> Option<String> {
        unsafe {
            let value = mpd_song_get_tag(self.song.inner, self.tag as i32, idx);
            if value.is_null() {
                None
            } else {
                Some(CStr::from_ptr(value).to_string_lossy().to_string())
            }
        }
    }

    /// Shortcut to collect all tags in a `Vec`
    #[must_use]
    pub fn receive_all(self) -> Vec<String> {
        self.collect()
    }
}

impl Iterator for ReturnTag<'_> {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        let tag = self.get_tag(self.pass);
        self.pass += 1;
        tag
    }
}