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