mpvipc_async/
property_parser.rs

1//! JSON parsing logic for properties returned by
2//! [`Event::PropertyChange`], and used internally in `MpvExt`
3//! to parse the response from `Mpv::get_property()`.
4//!
5//! This module is used to parse the json data from the `data` field of
6//! known properties. Mpv has about 1000 different properties
7//! as of `v0.38.0`, so this module will only implement the most common ones.
8
9// TODO: reuse this logic for providing a more typesafe response API to `Mpv::get_property()`
10//       Although this data is currently of type `Option<`
11
12use std::collections::HashMap;
13
14use serde::{Deserialize, Serialize};
15
16use crate::{MpvDataType, MpvError, PlaylistEntry};
17
18/// An incomplete list of properties that mpv can return.
19///
20/// Unimplemented properties will be returned with it's data
21/// as a `Property::Unknown` variant.
22///
23/// See <https://mpv.io/manual/master/#properties> for
24/// the upstream list of properties.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26#[serde(rename_all = "kebab-case")]
27pub enum Property {
28    Path(Option<String>),
29    Pause(bool),
30    PlaybackTime(Option<f64>),
31    Duration(Option<f64>),
32    Metadata(Option<HashMap<String, MpvDataType>>),
33    Playlist(Vec<PlaylistEntry>),
34    PlaylistPos(Option<usize>),
35    LoopFile(LoopProperty),
36    LoopPlaylist(LoopProperty),
37    TimePos(Option<f64>),
38    TimeRemaining(Option<f64>),
39    Speed(f64),
40    Volume(f64),
41    Mute(bool),
42    EofReached(bool),
43    Unknown {
44        name: String,
45        data: Option<MpvDataType>,
46    },
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50#[serde(rename_all = "kebab-case")]
51pub enum LoopProperty {
52    N(usize),
53    Inf,
54    No,
55}
56
57/// Parse a highlevel [`Property`] object from mpv data.
58///
59/// This is intended to be used with the `data` field of
60/// `Event::PropertyChange` and the response from `Mpv::get_property_value()`.
61pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property, MpvError> {
62    match name {
63        "path" => {
64            let path = match data {
65                Some(MpvDataType::String(s)) => Some(s),
66                Some(MpvDataType::Null) => None,
67                Some(data) => {
68                    return Err(MpvError::DataContainsUnexpectedType {
69                        expected_type: "String".to_owned(),
70                        received: data,
71                    })
72                }
73                None => {
74                    return Err(MpvError::MissingMpvData);
75                }
76            };
77            Ok(Property::Path(path))
78        }
79        "pause" => {
80            let pause = match data {
81                Some(MpvDataType::Bool(b)) => b,
82                Some(data) => {
83                    return Err(MpvError::DataContainsUnexpectedType {
84                        expected_type: "bool".to_owned(),
85                        received: data,
86                    })
87                }
88                None => {
89                    return Err(MpvError::MissingMpvData);
90                }
91            };
92            Ok(Property::Pause(pause))
93        }
94        "playback-time" => {
95            let playback_time = match data {
96                Some(MpvDataType::Double(d)) => Some(d),
97                None | Some(MpvDataType::Null) => None,
98                Some(data) => {
99                    return Err(MpvError::DataContainsUnexpectedType {
100                        expected_type: "f64".to_owned(),
101                        received: data,
102                    })
103                }
104            };
105            Ok(Property::PlaybackTime(playback_time))
106        }
107        "duration" => {
108            let duration = match data {
109                Some(MpvDataType::Double(d)) => Some(d),
110                None | Some(MpvDataType::Null) => None,
111                Some(data) => {
112                    return Err(MpvError::DataContainsUnexpectedType {
113                        expected_type: "f64".to_owned(),
114                        received: data,
115                    })
116                }
117            };
118            Ok(Property::Duration(duration))
119        }
120        "metadata" => {
121            let metadata = match data {
122                Some(MpvDataType::HashMap(m)) => Some(m),
123                None | Some(MpvDataType::Null) => None,
124                Some(data) => {
125                    return Err(MpvError::DataContainsUnexpectedType {
126                        expected_type: "HashMap".to_owned(),
127                        received: data,
128                    })
129                }
130            };
131            Ok(Property::Metadata(metadata))
132        }
133        "playlist" => {
134            let playlist = match data {
135                Some(MpvDataType::Array(a)) => mpv_array_to_playlist(&a)?,
136                None => Vec::new(),
137                Some(data) => {
138                    return Err(MpvError::DataContainsUnexpectedType {
139                        expected_type: "Array".to_owned(),
140                        received: data,
141                    })
142                }
143            };
144            Ok(Property::Playlist(playlist))
145        }
146        "playlist-pos" => {
147            let playlist_pos = match data {
148                Some(MpvDataType::Usize(u)) => Some(u),
149                Some(MpvDataType::MinusOne) => None,
150                Some(MpvDataType::Null) => None,
151                None => None,
152                Some(data) => {
153                    return Err(MpvError::DataContainsUnexpectedType {
154                        expected_type: "usize or -1".to_owned(),
155                        received: data,
156                    })
157                }
158            };
159            Ok(Property::PlaylistPos(playlist_pos))
160        }
161        "loop-file" => {
162            let loop_file = match data.to_owned() {
163                Some(MpvDataType::Usize(n)) => Some(LoopProperty::N(n)),
164                Some(MpvDataType::Bool(b)) => match b {
165                    true => Some(LoopProperty::Inf),
166                    false => Some(LoopProperty::No),
167                },
168                Some(MpvDataType::String(s)) => match s.as_str() {
169                    "inf" => Some(LoopProperty::Inf),
170                    _ => None,
171                },
172                _ => None,
173            }
174            .ok_or(match data {
175                Some(data) => MpvError::DataContainsUnexpectedType {
176                    expected_type: "'inf', bool, or usize".to_owned(),
177                    received: data,
178                },
179                None => MpvError::MissingMpvData,
180            })?;
181            Ok(Property::LoopFile(loop_file))
182        }
183        "loop-playlist" => {
184            let loop_playlist = match data.to_owned() {
185                Some(MpvDataType::Usize(n)) => Some(LoopProperty::N(n)),
186                Some(MpvDataType::Bool(b)) => match b {
187                    true => Some(LoopProperty::Inf),
188                    false => Some(LoopProperty::No),
189                },
190                Some(MpvDataType::String(s)) => match s.as_str() {
191                    "inf" => Some(LoopProperty::Inf),
192                    _ => None,
193                },
194                _ => None,
195            }
196            .ok_or(match data {
197                Some(data) => MpvError::DataContainsUnexpectedType {
198                    expected_type: "'inf', bool, or usize".to_owned(),
199                    received: data,
200                },
201                None => MpvError::MissingMpvData,
202            })?;
203
204            Ok(Property::LoopPlaylist(loop_playlist))
205        }
206        "time-pos" => {
207            let time_pos = match data {
208                Some(MpvDataType::Double(d)) => Some(d),
209                Some(data) => {
210                    return Err(MpvError::DataContainsUnexpectedType {
211                        expected_type: "f64".to_owned(),
212                        received: data,
213                    })
214                }
215                None => None,
216            };
217
218            Ok(Property::TimePos(time_pos))
219        }
220        "time-remaining" => {
221            let time_remaining = match data {
222                Some(MpvDataType::Double(d)) => Some(d),
223                Some(data) => {
224                    return Err(MpvError::DataContainsUnexpectedType {
225                        expected_type: "f64".to_owned(),
226                        received: data,
227                    })
228                }
229                None => None,
230            };
231            Ok(Property::TimeRemaining(time_remaining))
232        }
233        "speed" => {
234            let speed = match data {
235                Some(MpvDataType::Double(d)) => d,
236                Some(data) => {
237                    return Err(MpvError::DataContainsUnexpectedType {
238                        expected_type: "f64".to_owned(),
239                        received: data,
240                    })
241                }
242                None => {
243                    return Err(MpvError::MissingMpvData);
244                }
245            };
246            Ok(Property::Speed(speed))
247        }
248        "volume" => {
249            let volume = match data {
250                Some(MpvDataType::Double(d)) => d,
251                Some(data) => {
252                    return Err(MpvError::DataContainsUnexpectedType {
253                        expected_type: "f64".to_owned(),
254                        received: data,
255                    })
256                }
257                None => {
258                    return Err(MpvError::MissingMpvData);
259                }
260            };
261            Ok(Property::Volume(volume))
262        }
263        "mute" => {
264            let mute = match data {
265                Some(MpvDataType::Bool(b)) => b,
266                Some(data) => {
267                    return Err(MpvError::DataContainsUnexpectedType {
268                        expected_type: "bool".to_owned(),
269                        received: data,
270                    })
271                }
272                None => {
273                    return Err(MpvError::MissingMpvData);
274                }
275            };
276            Ok(Property::Mute(mute))
277        }
278        "eof-reached" => {
279            let eof_reached = match data {
280                Some(MpvDataType::Bool(b)) => b,
281                Some(data) => {
282                    return Err(MpvError::DataContainsUnexpectedType {
283                        expected_type: "bool".to_owned(),
284                        received: data,
285                    })
286                }
287                None => true,
288            };
289            Ok(Property::EofReached(eof_reached))
290        }
291        // TODO: add missing cases
292        _ => Ok(Property::Unknown {
293            name: name.to_owned(),
294            data,
295        }),
296    }
297}
298
299fn mpv_data_to_playlist_entry(
300    map: &HashMap<String, MpvDataType>,
301) -> Result<PlaylistEntry, MpvError> {
302    let filename = match map.get("filename") {
303        Some(MpvDataType::String(s)) => s.to_string(),
304        Some(data) => {
305            return Err(MpvError::DataContainsUnexpectedType {
306                expected_type: "String".to_owned(),
307                received: data.clone(),
308            })
309        }
310        None => return Err(MpvError::MissingMpvData),
311    };
312    let title = match map.get("title") {
313        Some(MpvDataType::String(s)) => Some(s.to_string()),
314        Some(data) => {
315            return Err(MpvError::DataContainsUnexpectedType {
316                expected_type: "String".to_owned(),
317                received: data.clone(),
318            })
319        }
320        None => None,
321    };
322    let current = match map.get("current") {
323        Some(MpvDataType::Bool(b)) => *b,
324        Some(data) => {
325            return Err(MpvError::DataContainsUnexpectedType {
326                expected_type: "bool".to_owned(),
327                received: data.clone(),
328            })
329        }
330        None => false,
331    };
332    Ok(PlaylistEntry {
333        id: 0,
334        filename,
335        title,
336        current,
337    })
338}
339
340fn mpv_array_to_playlist(array: &[MpvDataType]) -> Result<Vec<PlaylistEntry>, MpvError> {
341    array
342        .iter()
343        .map(|value| match value {
344            MpvDataType::HashMap(map) => mpv_data_to_playlist_entry(map),
345            _ => Err(MpvError::DataContainsUnexpectedType {
346                expected_type: "HashMap".to_owned(),
347                received: value.clone(),
348            }),
349        })
350        .enumerate()
351        .map(|(id, entry)| entry.map(|entry| PlaylistEntry { id, ..entry }))
352        .collect()
353}