mparsed 0.2.0

Structs and logic to deserialize mpd (music player daemon) responses with serde
Documentation
use chrono::prelude::*;
use helpers::*;
use serde::Deserialize;
use std::{fmt, time::Duration};

/// All information about a track. This is returned by the `currentsong` or `queue` commands.
#[derive(Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(default)]
pub struct Track {
  pub file: String,
  #[serde(rename = "artistsort")]
  pub artist_sort: Option<String>,
  #[serde(rename = "albumartist")]
  pub album_artist: Option<String>,
  #[serde(rename = "albumsort")]
  pub album_sort: Option<String>,
  #[serde(rename = "albumartistsort")]
  pub album_artist_sort: Option<String>,
  #[serde(deserialize_with = "de_string_or_vec")]
  #[serde(rename = "performer")]
  pub performers: Vec<String>,
  pub genre: Option<String>,
  pub title: Option<String>,
  #[serde(deserialize_with = "de_position")]
  pub track: Option<Position>,
  pub album: Option<String>,
  pub artist: Option<String>,
  pub pos: u32,
  pub id: u32,
  #[serde(rename = "last-modified")]
  pub last_modified: Option<DateTime<FixedOffset>>,
  #[serde(rename = "originaldate")]
  pub original_date: Option<u16>,
  pub format: Option<String>,
  #[serde(deserialize_with = "de_time_float")]
  pub duration: Option<Duration>,
  pub label: Option<String>,
  pub date: Option<u16>,
  #[serde(deserialize_with = "de_position")]
  pub disc: Option<Position>,
  pub musicbraiz_trackid: Option<String>,
  pub musicbrainz_albumid: Option<String>,
  pub musicbrainz_albumartistid: Option<String>,
  pub musicbrainz_artistid: Option<String>,
  pub musicbrainz_releasetrackid: Option<String>,
  pub musicbrainz_trackid: Option<String>,
  pub composer: Option<String>,
}

/// An empty struct that can be used as the response data for commands that only ever return `OK`
/// or an error message, e.g. `consume` or other boolean toggles.
#[derive(Deserialize, PartialEq, Debug)]
pub struct UnitResponse {}

/// The position of an item in a list with an optional total length.
/// Can be used for e.g. track number,
/// where `Position { 3, 12 }` represents track 3 of a CD with 12 tracks,
/// or disc numbers in a multi-disc set.
#[derive(Deserialize, Clone, Debug, Default, PartialEq)]
pub struct Position {
  pub item_position: u16,
  pub total_items: Option<u16>,
}

impl fmt::Display for Position {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    match self.total_items {
      Some(n) => write!(f, "{}/{}", self.item_position, n),
      None => write!(f, "{}", self.item_position),
    }
  }
}

/// Current status as returned by `status`.
///
/// Regarding optional `volume`:
/// The volume is None if mpd is running without local audio output,
/// i.e. on a server with no pulse or alsa output configured.
/// Output via httpd might be used instead
/// but will result in empty `volume` and `audio` fields.
#[derive(Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(default)]
pub struct Status {
  pub volume: Option<u8>,
  #[serde(deserialize_with = "de_bint")]
  pub repeat: bool,
  #[serde(deserialize_with = "de_bint")]
  pub random: bool,
  // TODO: make enum
  pub single: u8,
  #[serde(deserialize_with = "de_bint")]
  pub consume: bool,
  // an empty playlist still has an ID
  pub playlist: u32,
  // mpd returns 0 if there is no current playlist
  pub playlistlength: u32,
  #[serde(deserialize_with = "de_state")]
  pub state: State,
  pub song: Option<u32>,
  pub songid: Option<u32>,
  pub nextsong: Option<u32>,
  pub nextsongid: Option<u32>,
  #[serde(deserialize_with = "de_time_float")]
  pub elapsed: Option<Duration>,
  #[serde(deserialize_with = "de_time_float")]
  pub duration: Option<Duration>,
  pub bitrate: Option<u16>,
  pub xfade: Option<u8>,
  // 0 if unset
  pub mixrampdb: f32,
  pub mixrampdelay: Option<u8>,
  // “audio: The format emitted by the decoder plugin during playback, format: samplerate:bits:channels.
  // See Global Audio Format for a detailed explanation.”
  // TODO: make struct
  pub audio: Option<String>,
  pub updating_db: Option<u32>,
  pub error: Option<String>,
}

#[derive(Deserialize, Clone, Debug, PartialEq)]
pub struct File {
  #[serde(alias = "directory", rename = "file")]
  pub name: String,
  #[serde(rename = "last-modified")]
  pub last_modified: DateTime<FixedOffset>,
  #[serde(default)]
  pub size: usize,
}

impl File {
  pub fn is_directory(&self) -> bool {
    self.size == 0
  }
}

#[derive(Deserialize, Clone, Debug, PartialEq)]
pub enum State {
  Stop,
  Play,
  Pause,
}

// Default implementation so I can derive default for containing structs.
impl Default for State {
  fn default() -> Self {
    Self::Stop
  }
}

/// Database statistics as returned by the `stats` command.
#[derive(Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(default)]
pub struct Stats {
  pub artists: u32,
  pub albums: u32,
  pub songs: u32,
  #[serde(deserialize_with = "de_time_int")]
  pub uptime: Duration,
  #[serde(deserialize_with = "de_time_int")]
  pub db_playtime: Duration,
  // TODO: this is a unix era. use some datetime for it
  pub db_update: u32,
  #[serde(deserialize_with = "de_time_int")]
  pub playtime: Duration,
}

/// Deserialization helpers to handle the quirks of mpd’s output.
mod helpers {
  use super::*;
  use crate::SEPARATOR;
  use serde::{de, Deserialize};
  use std::{str::FromStr, time::Duration};

  /// Deserialize time from an integer that represents the seconds.
  /// mpd uses int for the database stats (e.g. total time played).
  pub fn de_time_int<'de, D>(deserializer: D) -> Result<Duration, D::Error>
  where
    D: de::Deserializer<'de>,
  {
    u64::deserialize(deserializer).map(Duration::from_secs)
  }

  /// Deserialize time from a float that represents the seconds.
  /// mpd uses floats for the current status (e.g. time elapsed in song).
  pub fn de_time_float<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
  where
    D: de::Deserializer<'de>,
  {
    f64::deserialize(deserializer).map(Duration::from_secs_f64).map(Some)
  }

  /// Deserialize the playback state.
  pub fn de_state<'de, D>(deserializer: D) -> Result<State, D::Error>
  where
    D: de::Deserializer<'de>,
  {
    match String::deserialize(deserializer)?.as_ref() {
      "play" => Ok(State::Play),
      "pause" => Ok(State::Pause),
      "stop" => Ok(State::Stop),
      s => Err(de::Error::invalid_value(
        de::Unexpected::Str(s),
        &"expected one of play, pause, or stop",
      )),
    }
  }

  pub fn de_string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
  where
    D: de::Deserializer<'de>,
  {
    String::deserialize(deserializer).map(|s| s.split(SEPARATOR).map(std::string::String::from).collect())
  }

  /// mpd uses bints (0 or 1) to represent booleans,
  /// so we need a special parser for those.
  pub fn de_bint<'de, D>(deserializer: D) -> Result<bool, D::Error>
  where
    D: de::Deserializer<'de>,
  {
    match u8::deserialize(deserializer)? {
      0 => Ok(false),
      1 => Ok(true),
      n => Err(de::Error::invalid_value(de::Unexpected::Unsigned(n as u64), &"zero or one")),
    }
  }

  /// Deserialize a position with an optional total length.
  /// The input string here is either a number or two numbers separated by `SEPARATOR`.
  pub fn de_position<'de, D>(deserializer: D) -> Result<Option<Position>, D::Error>
  where
    D: de::Deserializer<'de>,
  {
    let s = String::deserialize(deserializer)?;
    let mut ints = s.split(SEPARATOR).filter_map(|s| u16::from_str(s).ok());
    if let Some(n) = ints.next() {
      return Ok(Some(Position {
        item_position: n,
        total_items: ints.next(),
      }));
    }
    Ok(None)
  }
}