//! JSON parsing logic for properties returned by //! [[`Event::PropertyChange`], and used internally in `MpvExt` //! to parse the response from `Mpv::get_property()`. //! //! This module is used to parse the json data from the `data` field of //! known properties. Mpv has about 1000 different properties //! as of `v0.38.0`, so this module will only implement the most common ones. // TODO: reuse this logic for providing a more typesafe response API to `Mpv::get_property()` // Although this data is currently of type `Option<` use std::collections::HashMap; use serde::{Deserialize, Serialize}; use crate::{MpvDataType, MpvError, PlaylistEntry}; /// An incomplete list of properties that mpv can return. /// /// Unimplemented properties will be returned with it's data /// as a `Property::Unknown` variant. /// /// See for /// the upstream list of properties. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Property { Path(Option), Pause(bool), PlaybackTime(Option), Duration(Option), Metadata(Option>), Playlist(Vec), PlaylistPos(Option), LoopFile(LoopProperty), LoopPlaylist(LoopProperty), TimePos(f64), TimeRemaining(f64), Speed(f64), Volume(f64), Mute(bool), Unknown { name: String, data: Option, }, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum LoopProperty { N(usize), Inf, No, } /// Parse a highlevel [`Property`] object from mpv data. /// /// This is intended to be used with the `data` field of /// `Event::PropertyChange` and the response from `Mpv::get_property_value()`. pub fn parse_property(name: &str, data: Option) -> Result { match name { "path" => { let path = match data { Some(MpvDataType::String(s)) => Some(s), Some(MpvDataType::Null) => None, Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "String".to_owned(), received: data, }) } None => { return Err(MpvError::MissingMpvData); } }; Ok(Property::Path(path)) } "pause" => { let pause = match data { Some(MpvDataType::Bool(b)) => b, Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "bool".to_owned(), received: data, }) } None => { return Err(MpvError::MissingMpvData); } }; Ok(Property::Pause(pause)) } "playback-time" => { let playback_time = match data { Some(MpvDataType::Double(d)) => Some(d), None | Some(MpvDataType::Null) => None, Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "f64".to_owned(), received: data, }) } }; Ok(Property::PlaybackTime(playback_time)) } "duration" => { let duration = match data { Some(MpvDataType::Double(d)) => Some(d), None | Some(MpvDataType::Null) => None, Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "f64".to_owned(), received: data, }) } }; Ok(Property::Duration(duration)) } "metadata" => { let metadata = match data { Some(MpvDataType::HashMap(m)) => Some(m), None | Some(MpvDataType::Null) => None, Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "HashMap".to_owned(), received: data, }) } }; Ok(Property::Metadata(metadata)) } "playlist" => { let playlist = match data { Some(MpvDataType::Array(a)) => mpv_array_to_playlist(&a)?, None => Vec::new(), Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "Array".to_owned(), received: data, }) } }; Ok(Property::Playlist(playlist)) } "playlist-pos" => { let playlist_pos = match data { Some(MpvDataType::Usize(u)) => Some(u), Some(MpvDataType::MinusOne) => None, Some(MpvDataType::Null) => None, None => None, Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "usize or -1".to_owned(), received: data, }) } }; Ok(Property::PlaylistPos(playlist_pos)) } "loop-file" => { let loop_file = match data.to_owned() { Some(MpvDataType::Usize(n)) => Some(LoopProperty::N(n)), Some(MpvDataType::Bool(b)) => match b { true => Some(LoopProperty::Inf), false => Some(LoopProperty::No), }, Some(MpvDataType::String(s)) => match s.as_str() { "inf" => Some(LoopProperty::Inf), _ => None, }, _ => None, } .ok_or(match data { Some(data) => MpvError::DataContainsUnexpectedType { expected_type: "'inf', bool, or usize".to_owned(), received: data, }, None => MpvError::MissingMpvData, })?; Ok(Property::LoopFile(loop_file)) } "loop-playlist" => { let loop_playlist = match data.to_owned() { Some(MpvDataType::Usize(n)) => Some(LoopProperty::N(n)), Some(MpvDataType::Bool(b)) => match b { true => Some(LoopProperty::Inf), false => Some(LoopProperty::No), }, Some(MpvDataType::String(s)) => match s.as_str() { "inf" => Some(LoopProperty::Inf), _ => None, }, _ => None, } .ok_or(match data { Some(data) => MpvError::DataContainsUnexpectedType { expected_type: "'inf', bool, or usize".to_owned(), received: data, }, None => MpvError::MissingMpvData, })?; Ok(Property::LoopPlaylist(loop_playlist)) } "time-pos" => { let time_pos = match data { Some(MpvDataType::Double(d)) => d, Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "f64".to_owned(), received: data, }) } None => { return Err(MpvError::MissingMpvData); } }; Ok(Property::TimePos(time_pos)) } "time-remaining" => { let time_remaining = match data { Some(MpvDataType::Double(d)) => d, Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "f64".to_owned(), received: data, }) } None => { return Err(MpvError::MissingMpvData); } }; Ok(Property::TimeRemaining(time_remaining)) } "speed" => { let speed = match data { Some(MpvDataType::Double(d)) => d, Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "f64".to_owned(), received: data, }) } None => { return Err(MpvError::MissingMpvData); } }; Ok(Property::Speed(speed)) } "volume" => { let volume = match data { Some(MpvDataType::Double(d)) => d, Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "f64".to_owned(), received: data, }) } None => { return Err(MpvError::MissingMpvData); } }; Ok(Property::Volume(volume)) } "mute" => { let mute = match data { Some(MpvDataType::Bool(b)) => b, Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "bool".to_owned(), received: data, }) } None => { return Err(MpvError::MissingMpvData); } }; Ok(Property::Mute(mute)) } // TODO: add missing cases _ => Ok(Property::Unknown { name: name.to_owned(), data, }), } } fn mpv_data_to_playlist_entry( map: &HashMap, ) -> Result { let filename = match map.get("filename") { Some(MpvDataType::String(s)) => s.to_string(), Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "String".to_owned(), received: data.clone(), }) } None => return Err(MpvError::MissingMpvData), }; let title = match map.get("title") { Some(MpvDataType::String(s)) => s.to_string(), Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "String".to_owned(), received: data.clone(), }) } None => return Err(MpvError::MissingMpvData), }; let current = match map.get("current") { Some(MpvDataType::Bool(b)) => *b, Some(data) => { return Err(MpvError::DataContainsUnexpectedType { expected_type: "bool".to_owned(), received: data.clone(), }) } None => return Err(MpvError::MissingMpvData), }; Ok(PlaylistEntry { id: 0, filename, title, current, }) } fn mpv_array_to_playlist(array: &[MpvDataType]) -> Result, MpvError> { array .iter() .map(|value| match value { MpvDataType::HashMap(map) => mpv_data_to_playlist_entry(map), _ => Err(MpvError::DataContainsUnexpectedType { expected_type: "HashMap".to_owned(), received: value.clone(), }), }) .enumerate() .map(|(id, entry)| entry.map(|entry| PlaylistEntry { id, ..entry })) .collect() }