1use 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "kebab-case")]
15pub enum EventEndFileReason {
16 Eof,
19
20 Stop,
22
23 Quit,
25
26 Error,
28
29 Redirect,
33
34 Unknown,
38
39 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#[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 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#[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 Idle,
151
152 Tick,
154
155 TracksChanged,
157
158 TrackSwitched,
160
161 Pause,
163
164 Unpause,
166
167 MetadataUpdate,
169
170 ChapterChange,
172
173 ScriptInputDispatch,
175
176 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
211pub(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 "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}