use chrono::prelude::*; use helpers::*; use serde::{Deserialize, Serialize}; use std::{fmt, time::Duration}; /// All information about a track. This is returned by the `currentsong`, `queue`, or /// `playlistinfo` commands. #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] #[serde(default)] pub struct Track { pub file: String, #[serde(rename = "artistsort")] pub artist_sort: Option, #[serde(rename = "albumartist")] pub album_artist: Option, #[serde(rename = "albumsort")] pub album_sort: Option, #[serde(rename = "albumartistsort")] pub album_artist_sort: Option, #[serde(deserialize_with = "de_string_or_vec")] #[serde(rename = "performer")] pub performers: Vec, pub genre: Option, pub title: Option, #[serde(deserialize_with = "de_position")] pub track: Option, pub album: Option, pub artist: Option, pub pos: u32, pub id: u32, #[serde(rename = "last-modified")] pub last_modified: Option>, #[serde(rename = "originaldate")] pub original_date: Option, pub format: Option, #[serde(deserialize_with = "de_time_float")] pub duration: Option, pub label: Option, pub date: Option, #[serde(deserialize_with = "de_position")] pub disc: Option, pub musicbraiz_trackid: Option, pub musicbrainz_albumid: Option, pub musicbrainz_albumartistid: Option, pub musicbrainz_artistid: Option, pub musicbrainz_releasetrackid: Option, pub musicbrainz_trackid: Option, pub composer: Option, } /// 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(Serialize, 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(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] pub struct Position { pub item_position: u16, pub total_items: Option, } 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(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] #[serde(default)] pub struct Status { pub volume: Option, #[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, pub songid: Option, pub nextsong: Option, pub nextsongid: Option, #[serde(deserialize_with = "de_time_float")] pub elapsed: Option, #[serde(deserialize_with = "de_time_float")] pub duration: Option, pub bitrate: Option, pub xfade: Option, // 0 if unset pub mixrampdb: f32, pub mixrampdelay: Option, // “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, pub updating_db: Option, pub error: Option, } /// An object in the file system, as returned by the `listfiles` command. /// /// For directories, the `size` will be `-1`, and [`is_directory`] is provided to check that. /// /// [`is_directory`]: #method.is_directory /// /// Properly parsing `listfiles` into a Vector of some enum with variants for file and directory /// would make the deserialization code a lot more complicated, so I’m not interested in doing that /// at this point in time. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct File { #[serde(alias = "directory", rename = "file")] pub name: String, #[serde(rename = "last-modified")] pub last_modified: DateTime, #[serde(default = "minus_one")] pub size: i64, } fn minus_one() -> i64 { -1 } impl File { /// Returns true if this file is a directory. /// Internally, this just checks if `size == -1`. pub fn is_directory(&self) -> bool { self.size == -1 } } #[derive(Serialize, 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(Serialize, 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 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, 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 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, 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 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, 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) } }