mpvipc_async/
event_parser.rs

1//! JSON parsing logic for events from [`MpvIpc`](crate::ipc::MpvIpc).
2
3use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6use serde_json::{Map, Value};
7
8use crate::{MpvDataType, MpvError, ipc::MpvIpcEvent, message_parser::json_to_value};
9
10/// Reason behind the `MPV_EVENT_END_FILE` event.
11///
12/// Ref: <https://mpv.io/manual/stable/#command-interface-mpv-event-end-file>
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "kebab-case")]
15pub enum EventEndFileReason {
16    /// The file has ended. This can (but doesn't have to) include
17    /// incomplete files or broken network connections under circumstances.
18    Eof,
19
20    /// Playback was ended by a command.
21    Stop,
22
23    /// Playback was ended by sending the quit command.
24    Quit,
25
26    /// An error happened. In this case, an `error` field is present with the error string.
27    Error,
28
29    /// Happens with playlists and similar. For details, see
30    /// [`MPV_END_FILE_REASON_REDIRECT`](https://github.com/mpv-player/mpv/blob/72efbfd009a2b3259055133d74b88c81b1115ae1/include/mpv/client.h#L1493)
31    /// in the C API.
32    Redirect,
33
34    /// Unknown. Normally doesn't happen, unless the Lua API is out of sync
35    /// with the C API. (Likewise, it could happen that your script gets reason
36    /// strings that did not exist yet at the time your script was written.)
37    Unknown,
38
39    /// A catch-all enum variant in case `mpvipc-async` has not implemented the
40    /// returned error yet.
41    Unimplemented(String),
42}
43
44impl FromStr for EventEndFileReason {
45    type Err = ();
46
47    fn from_str(s: &str) -> Result<Self, Self::Err> {
48        match s {
49            "eof" => Ok(EventEndFileReason::Eof),
50            "stop" => Ok(EventEndFileReason::Stop),
51            "quit" => Ok(EventEndFileReason::Quit),
52            "error" => Ok(EventEndFileReason::Error),
53            "redirect" => Ok(EventEndFileReason::Redirect),
54            reason => Ok(EventEndFileReason::Unimplemented(reason.to_string())),
55        }
56    }
57}
58
59/// The log level of a log message event.
60///
61/// Ref:
62/// - <https://mpv.io/manual/stable/#command-interface-mpv-event-log-message>
63/// - <https://mpv.io/manual/stable/#mp-msg-functions>
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65#[serde(rename_all = "kebab-case")]
66pub enum EventLogMessageLevel {
67    Info,
68    Warn,
69    Error,
70    Fatal,
71    Verbose,
72    Debug,
73    Trace,
74
75    /// A catch-all enum variant in case `mpvipc-async` has not implemented the
76    /// returned log-level yet.
77    Unimplemented(String),
78}
79
80impl FromStr for EventLogMessageLevel {
81    type Err = ();
82
83    fn from_str(s: &str) -> Result<Self, Self::Err> {
84        match s {
85            "info" => Ok(EventLogMessageLevel::Info),
86            "warn" => Ok(EventLogMessageLevel::Warn),
87            "error" => Ok(EventLogMessageLevel::Error),
88            "fatal" => Ok(EventLogMessageLevel::Fatal),
89            "verbose" => Ok(EventLogMessageLevel::Verbose),
90            "debug" => Ok(EventLogMessageLevel::Debug),
91            "trace" => Ok(EventLogMessageLevel::Trace),
92            level => Ok(EventLogMessageLevel::Unimplemented(level.to_string())),
93        }
94    }
95}
96
97/// All possible events that can be sent by mpv.
98///
99/// Not all event types are guaranteed to be implemented.
100/// If something is missing, please open an issue.
101///
102/// Otherwise, the event will be returned as an `Event::Unimplemented` variant.
103///
104/// See <https://mpv.io/manual/master/#list-of-events> for
105/// the upstream list of events.
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107#[serde(rename_all = "kebab-case")]
108pub enum Event {
109    StartFile {
110        playlist_entry_id: usize,
111    },
112    EndFile {
113        reason: EventEndFileReason,
114        playlist_entry_id: usize,
115        file_error: Option<String>,
116        playlist_insert_id: Option<usize>,
117        playlist_insert_num_entries: Option<usize>,
118    },
119    FileLoaded,
120    Seek,
121    PlaybackRestart,
122    Shutdown,
123    LogMessage {
124        prefix: String,
125        level: EventLogMessageLevel,
126        text: String,
127    },
128    Hook {
129        hook_id: usize,
130    },
131    GetPropertyReply,
132    SetPropertyReply,
133    CommandReply {
134        result: String,
135    },
136    ClientMessage {
137        args: Vec<String>,
138    },
139    VideoReconfig,
140    AudioReconfig,
141    PropertyChange {
142        id: Option<u64>,
143        name: String,
144        data: Option<MpvDataType>,
145    },
146    EventQueueOverflow,
147    None,
148
149    /// Deprecated since mpv v0.33.0
150    Idle,
151
152    /// Deprecated since mpv v0.31.0
153    Tick,
154
155    /// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
156    TracksChanged,
157
158    /// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
159    TrackSwitched,
160
161    /// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
162    Pause,
163
164    /// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
165    Unpause,
166
167    /// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
168    MetadataUpdate,
169
170    /// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
171    ChapterChange,
172
173    /// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
174    ScriptInputDispatch,
175
176    /// Catch-all for unimplemented events
177    Unimplemented(Map<String, Value>),
178}
179
180macro_rules! get_key_as {
181    ($as_type:ident, $key:expr, $event:ident) => {{
182        let tmp = $event.get($key).ok_or(MpvError::MissingKeyInObject {
183            key: $key.to_owned(),
184            map: $event.clone(),
185        })?;
186
187        tmp.$as_type()
188            .ok_or(MpvError::ValueContainsUnexpectedType {
189                expected_type: stringify!($as_type).strip_prefix("as_").unwrap().to_owned(),
190                received: tmp.clone(),
191            })?
192    }};
193}
194
195macro_rules! get_optional_key_as {
196    ($as_type:ident, $key:expr, $event:ident) => {{
197        match $event.get($key) {
198            Some(Value::Null) => None,
199            Some(tmp) => Some(
200                tmp.$as_type()
201                    .ok_or(MpvError::ValueContainsUnexpectedType {
202                        expected_type: stringify!($as_type).strip_prefix("as_").unwrap().to_owned(),
203                        received: tmp.clone(),
204                    })?,
205            ),
206            None => None,
207        }
208    }};
209}
210
211// NOTE: I have not been able to test all of these events,
212//       so some of the parsing logic might be incorrect.
213//       In particular, I have not been able to make mpv
214//       produce any of the commented out events, and since
215//       the documentation for the most part just says
216//       "See C API", I have not pursued this further.
217//
218//       If you need this, please open an issue or a PR.
219
220/// Parse a highlevel [`Event`] objects from json.
221pub(crate) fn parse_event(raw_event: MpvIpcEvent) -> Result<Event, MpvError> {
222    let MpvIpcEvent(event) = raw_event;
223
224    event
225        .as_object()
226        .ok_or(MpvError::ValueContainsUnexpectedType {
227            expected_type: "object".to_owned(),
228            received: event.clone(),
229        })
230        .and_then(|event| {
231            let event_name = get_key_as!(as_str, "event", event);
232
233            match event_name {
234                "start-file" => parse_start_file(event),
235                "end-file" => parse_end_file(event),
236                "file-loaded" => Ok(Event::FileLoaded),
237                "seek" => Ok(Event::Seek),
238                "playback-restart" => Ok(Event::PlaybackRestart),
239                "shutdown" => Ok(Event::Shutdown),
240                "log-message" => parse_log_message(event),
241                "hook" => parse_hook(event),
242
243                // TODO: fix these. They are asynchronous responses to different requests.
244                //       see:
245                //         - https://github.com/mpv-player/mpv/blob/5f768a688b706cf94041adf5bed7c7004af2ec5a/libmpv/client.h#L1158-L1160
246                //         - https://github.com/mpv-player/mpv/blob/5f768a688b706cf94041adf5bed7c7004af2ec5a/libmpv/client.h#L1095-L1098
247                //         - https://github.com/mpv-player/mpv/blob/5f768a688b706cf94041adf5bed7c7004af2ec5a/libmpv/client.h#L972-L982
248                // "get-property-reply" =>
249                // "set-property-reply" =>
250                // "command-reply" =>
251                "client-message" => parse_client_message(event),
252                "video-reconfig" => Ok(Event::VideoReconfig),
253                "audio-reconfig" => Ok(Event::AudioReconfig),
254                "property-change" => parse_property_change(event),
255                "tick" => Ok(Event::Tick),
256                "idle" => Ok(Event::Idle),
257                "tracks-changed" => Ok(Event::TracksChanged),
258                "track-switched" => Ok(Event::TrackSwitched),
259                "pause" => Ok(Event::Pause),
260                "unpause" => Ok(Event::Unpause),
261                "metadata-update" => Ok(Event::MetadataUpdate),
262                "chapter-change" => Ok(Event::ChapterChange),
263                _ => Ok(Event::Unimplemented(event.to_owned())),
264            }
265        })
266}
267
268fn parse_start_file(event: &Map<String, Value>) -> Result<Event, MpvError> {
269    let playlist_entry_id = get_key_as!(as_u64, "playlist_entry_id", event) as usize;
270
271    Ok(Event::StartFile { playlist_entry_id })
272}
273
274fn parse_end_file(event: &Map<String, Value>) -> Result<Event, MpvError> {
275    let reason = get_key_as!(as_str, "reason", event);
276    let playlist_entry_id = get_key_as!(as_u64, "playlist_entry_id", event) as usize;
277    let file_error = get_optional_key_as!(as_str, "file_error", event).map(|s| s.to_string());
278    let playlist_insert_id =
279        get_optional_key_as!(as_u64, "playlist_insert_id", event).map(|i| i as usize);
280    let playlist_insert_num_entries =
281        get_optional_key_as!(as_u64, "playlist_insert_num_entries", event).map(|i| i as usize);
282
283    Ok(Event::EndFile {
284        reason: reason
285            .parse()
286            .unwrap_or(EventEndFileReason::Unimplemented(reason.to_string())),
287        playlist_entry_id,
288        file_error,
289        playlist_insert_id,
290        playlist_insert_num_entries,
291    })
292}
293
294fn parse_log_message(event: &Map<String, Value>) -> Result<Event, MpvError> {
295    let prefix = get_key_as!(as_str, "prefix", event).to_owned();
296    let level = get_key_as!(as_str, "level", event);
297    let text = get_key_as!(as_str, "text", event).to_owned();
298
299    Ok(Event::LogMessage {
300        prefix,
301        level: level
302            .parse()
303            .unwrap_or(EventLogMessageLevel::Unimplemented(level.to_string())),
304        text,
305    })
306}
307
308fn parse_hook(event: &Map<String, Value>) -> Result<Event, MpvError> {
309    let hook_id = get_key_as!(as_u64, "hook_id", event) as usize;
310    Ok(Event::Hook { hook_id })
311}
312
313fn parse_client_message(event: &Map<String, Value>) -> Result<Event, MpvError> {
314    let args = get_key_as!(as_array, "args", event)
315        .iter()
316        .map(|arg| {
317            arg.as_str()
318                .ok_or(MpvError::ValueContainsUnexpectedType {
319                    expected_type: "string".to_owned(),
320                    received: arg.clone(),
321                })
322                .map(|s| s.to_string())
323        })
324        .collect::<Result<Vec<String>, MpvError>>()?;
325    Ok(Event::ClientMessage { args })
326}
327
328fn parse_property_change(event: &Map<String, Value>) -> Result<Event, MpvError> {
329    let id = get_optional_key_as!(as_u64, "id", event);
330    let property_name = get_key_as!(as_str, "name", event);
331    let data = event.get("data").map(json_to_value).transpose()?;
332
333    Ok(Event::PropertyChange {
334        id,
335        name: property_name.to_string(),
336        data,
337    })
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use crate::ipc::MpvIpcEvent;
344    use serde_json::json;
345
346    #[test]
347    fn test_parse_simple_events() {
348        let simple_events = vec![
349            (json!({"event": "file-loaded"}), Event::FileLoaded),
350            (json!({"event": "seek"}), Event::Seek),
351            (json!({"event": "playback-restart"}), Event::PlaybackRestart),
352            (json!({"event": "shutdown"}), Event::Shutdown),
353            (json!({"event": "video-reconfig"}), Event::VideoReconfig),
354            (json!({"event": "audio-reconfig"}), Event::AudioReconfig),
355            (json!({"event": "tick"}), Event::Tick),
356            (json!({"event": "idle"}), Event::Idle),
357            (json!({"event": "tracks-changed"}), Event::TracksChanged),
358            (json!({"event": "track-switched"}), Event::TrackSwitched),
359            (json!({"event": "pause"}), Event::Pause),
360            (json!({"event": "unpause"}), Event::Unpause),
361            (json!({"event": "metadata-update"}), Event::MetadataUpdate),
362            (json!({"event": "chapter-change"}), Event::ChapterChange),
363        ];
364
365        for (raw_event_json, expected_event) in simple_events {
366            let raw_event = MpvIpcEvent(raw_event_json);
367            let event = parse_event(raw_event).unwrap();
368            assert_eq!(event, expected_event);
369        }
370    }
371
372    #[test]
373    fn test_parse_start_file_event() {
374        let raw_event = MpvIpcEvent(json!({
375            "event": "start-file",
376            "playlist_entry_id": 1
377        }));
378
379        let event = parse_event(raw_event).unwrap();
380
381        assert_eq!(
382            event,
383            Event::StartFile {
384                playlist_entry_id: 1
385            }
386        );
387    }
388
389    #[test]
390    fn test_parse_end_file_event() {
391        let raw_event = MpvIpcEvent(json!({
392            "event": "end-file",
393            "reason": "eof",
394            "playlist_entry_id": 2,
395            "file_error": null,
396            "playlist_insert_id": 3,
397            "playlist_insert_num_entries": 5
398        }));
399        let event = parse_event(raw_event).unwrap();
400        assert_eq!(
401            event,
402            Event::EndFile {
403                reason: EventEndFileReason::Eof,
404                playlist_entry_id: 2,
405                file_error: None,
406                playlist_insert_id: Some(3),
407                playlist_insert_num_entries: Some(5)
408            }
409        );
410
411        let raw_event_with_error = MpvIpcEvent(json!({
412            "event": "end-file",
413            "reason": "error",
414            "playlist_entry_id": 4,
415            "file_error": "File not found",
416        }));
417        let event_with_error = parse_event(raw_event_with_error).unwrap();
418        assert_eq!(
419            event_with_error,
420            Event::EndFile {
421                reason: EventEndFileReason::Error,
422                playlist_entry_id: 4,
423                file_error: Some("File not found".to_string()),
424                playlist_insert_id: None,
425                playlist_insert_num_entries: None,
426            }
427        );
428
429        let raw_event_unimplemented = MpvIpcEvent(json!({
430            "event": "end-file",
431            "reason": "unknown-reason",
432            "playlist_entry_id": 5
433        }));
434        let event_unimplemented = parse_event(raw_event_unimplemented).unwrap();
435        assert_eq!(
436            event_unimplemented,
437            Event::EndFile {
438                reason: EventEndFileReason::Unimplemented("unknown-reason".to_string()),
439                playlist_entry_id: 5,
440                file_error: None,
441                playlist_insert_id: None,
442                playlist_insert_num_entries: None,
443            }
444        );
445    }
446
447    #[test]
448    fn test_parse_log_message_event() {
449        let raw_event = MpvIpcEvent(json!({
450            "event": "log-message",
451            "prefix": "mpv",
452            "level": "info",
453            "text": "This is a log message"
454        }));
455        let event = parse_event(raw_event).unwrap();
456        assert_eq!(
457            event,
458            Event::LogMessage {
459                prefix: "mpv".to_string(),
460                level: EventLogMessageLevel::Info,
461                text: "This is a log message".to_string(),
462            }
463        );
464    }
465
466    #[test]
467    fn test_parse_hook_event() {
468        let raw_event = MpvIpcEvent(json!({
469            "event": "hook",
470            "hook_id": 42
471        }));
472        let event = parse_event(raw_event).unwrap();
473        assert_eq!(event, Event::Hook { hook_id: 42 });
474    }
475
476    #[test]
477    fn test_parse_client_message_event() {
478        let raw_event = MpvIpcEvent(json!({
479            "event": "client-message",
480            "args": ["arg1", "arg2", "arg3"]
481        }));
482        let event = parse_event(raw_event).unwrap();
483        assert_eq!(
484            event,
485            Event::ClientMessage {
486                args: vec!["arg1".to_string(), "arg2".to_string(), "arg3".to_string()]
487            }
488        );
489    }
490
491    #[test]
492    fn test_parse_property_change_event() {
493        let raw_event = MpvIpcEvent(json!({
494            "event": "property-change",
495            "id": 1,
496            "name": "pause",
497            "data": true
498        }));
499        let event = parse_event(raw_event).unwrap();
500        assert_eq!(
501            event,
502            Event::PropertyChange {
503                id: Some(1),
504                name: "pause".to_string(),
505                data: Some(MpvDataType::Bool(true)),
506            }
507        );
508    }
509
510    #[test]
511    fn test_parse_unimplemented_event() {
512        let raw_event = MpvIpcEvent(json!({
513            "event": "some-unimplemented-event",
514            "some_key": "some_value"
515        }));
516        let event = parse_event(raw_event).unwrap();
517        assert_eq!(
518            event,
519            Event::Unimplemented(
520                json!({
521                    "event": "some-unimplemented-event",
522                    "some_key": "some_value"
523                })
524                .as_object()
525                .unwrap()
526                .to_owned()
527            )
528        );
529    }
530}