1use serde::{Deserialize, Serialize};
7use std::net::IpAddr;
8
9use crate::events::{xml_utils, EnrichedEvent, EventParser, EventSource};
10use crate::{ApiError, Result, Service};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(rename = "propertyset")]
15pub struct AVTransportEvent {
16 #[serde(rename = "property")]
17 property: AVTransportProperty,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21struct AVTransportProperty {
22 #[serde(
23 rename = "LastChange",
24 deserialize_with = "xml_utils::deserialize_nested"
25 )]
26 last_change: AVTransportEventData,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(rename = "Event")]
31pub struct AVTransportEventData {
32 #[serde(rename = "InstanceID")]
33 instance: AVTransportInstance,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37struct AVTransportInstance {
38 #[serde(rename = "TransportState", default)]
39 pub transport_state: Option<xml_utils::ValueAttribute>,
40
41 #[serde(rename = "TransportStatus", default)]
42 pub transport_status: Option<xml_utils::ValueAttribute>,
43
44 #[serde(rename = "TransportPlaySpeed", default)]
45 pub speed: Option<xml_utils::ValueAttribute>,
46
47 #[serde(rename = "CurrentTrackURI", default)]
48 pub current_track_uri: Option<xml_utils::ValueAttribute>,
49
50 #[serde(rename = "CurrentTrackDuration", default)]
51 pub track_duration: Option<xml_utils::ValueAttribute>,
52
53 #[serde(rename = "RelativeTimePosition", default)]
54 pub rel_time: Option<xml_utils::ValueAttribute>,
55
56 #[serde(rename = "AbsoluteTimePosition", default)]
57 pub abs_time: Option<xml_utils::ValueAttribute>,
58
59 #[serde(rename = "CurrentTrack", default)]
60 pub rel_count: Option<xml_utils::ValueAttribute>,
61
62 #[serde(rename = "CurrentPlayMode", default)]
63 pub play_mode: Option<xml_utils::ValueAttribute>,
64
65 #[serde(rename = "CurrentTrackMetaData", default)]
66 pub track_metadata: Option<xml_utils::ValueAttribute>,
67
68 #[serde(rename = "NextTrackURI", default)]
69 pub next_track_uri: Option<xml_utils::ValueAttribute>,
70
71 #[serde(rename = "NextTrackMetaData", default)]
72 pub next_track_metadata: Option<xml_utils::ValueAttribute>,
73
74 #[serde(rename = "NumberOfTracks", default)]
75 pub queue_length: Option<xml_utils::ValueAttribute>,
76}
77
78impl AVTransportEvent {
79 pub fn transport_state(&self) -> Option<String> {
81 self.property
82 .last_change
83 .instance
84 .transport_state
85 .as_ref()
86 .map(|v| v.val.clone())
87 }
88
89 pub fn transport_status(&self) -> Option<String> {
91 self.property
92 .last_change
93 .instance
94 .transport_status
95 .as_ref()
96 .map(|v| v.val.clone())
97 }
98
99 pub fn speed(&self) -> Option<String> {
101 self.property
102 .last_change
103 .instance
104 .speed
105 .as_ref()
106 .map(|v| v.val.clone())
107 }
108
109 pub fn current_track_uri(&self) -> Option<String> {
111 self.property
112 .last_change
113 .instance
114 .current_track_uri
115 .as_ref()
116 .map(|v| v.val.clone())
117 }
118
119 pub fn track_duration(&self) -> Option<String> {
121 self.property
122 .last_change
123 .instance
124 .track_duration
125 .as_ref()
126 .map(|v| v.val.clone())
127 }
128
129 pub fn rel_time(&self) -> Option<String> {
131 self.property
132 .last_change
133 .instance
134 .rel_time
135 .as_ref()
136 .map(|v| v.val.clone())
137 }
138
139 pub fn abs_time(&self) -> Option<String> {
141 self.property
142 .last_change
143 .instance
144 .abs_time
145 .as_ref()
146 .map(|v| v.val.clone())
147 }
148
149 pub fn rel_count(&self) -> Option<u32> {
151 self.property
152 .last_change
153 .instance
154 .rel_count
155 .as_ref()
156 .and_then(|v| v.val.parse().ok())
157 }
158
159 pub fn abs_count(&self) -> Option<u32> {
161 None
162 }
163
164 pub fn play_mode(&self) -> Option<String> {
166 self.property
167 .last_change
168 .instance
169 .play_mode
170 .as_ref()
171 .map(|v| v.val.clone())
172 }
173
174 pub fn track_metadata(&self) -> Option<String> {
176 self.property
177 .last_change
178 .instance
179 .track_metadata
180 .as_ref()
181 .map(|v| v.val.clone())
182 }
183
184 pub fn next_track_uri(&self) -> Option<String> {
186 self.property
187 .last_change
188 .instance
189 .next_track_uri
190 .as_ref()
191 .map(|v| v.val.clone())
192 }
193
194 pub fn next_track_metadata(&self) -> Option<String> {
196 self.property
197 .last_change
198 .instance
199 .next_track_metadata
200 .as_ref()
201 .map(|v| v.val.clone())
202 }
203
204 pub fn queue_length(&self) -> Option<u32> {
206 self.property
207 .last_change
208 .instance
209 .queue_length
210 .as_ref()
211 .and_then(|v| v.val.parse().ok())
212 }
213
214 pub fn into_state(&self) -> super::state::AVTransportState {
216 super::state::AVTransportState {
217 transport_state: self.transport_state(),
218 transport_status: self.transport_status(),
219 speed: self.speed(),
220 current_track_uri: self.current_track_uri(),
221 track_duration: self.track_duration(),
222 track_metadata: self.track_metadata(),
223 rel_time: self.rel_time(),
224 abs_time: self.abs_time(),
225 rel_count: self.rel_count(),
226 abs_count: self.abs_count(),
227 play_mode: self.play_mode(),
228 next_track_uri: self.next_track_uri(),
229 next_track_metadata: self.next_track_metadata(),
230 queue_length: self.queue_length(),
231 }
232 }
233
234 pub fn from_xml(xml: &str) -> Result<Self> {
236 quick_xml::de::from_str(xml)
237 .map_err(|e| ApiError::ParseError(format!("Failed to parse AVTransport XML: {e}")))
238 }
239}
240
241pub struct AVTransportEventParser;
243
244impl EventParser for AVTransportEventParser {
245 type EventData = AVTransportEvent;
246
247 fn parse_upnp_event(&self, xml: &str) -> Result<Self::EventData> {
248 AVTransportEvent::from_xml(xml)
249 }
250
251 fn service_type(&self) -> Service {
252 Service::AVTransport
253 }
254}
255
256pub fn create_enriched_event(
258 speaker_ip: IpAddr,
259 event_source: EventSource,
260 event_data: AVTransportEvent,
261) -> EnrichedEvent<AVTransportEvent> {
262 EnrichedEvent::new(speaker_ip, Service::AVTransport, event_source, event_data)
263}
264
265pub fn create_enriched_event_with_registration_id(
267 registration_id: u64,
268 speaker_ip: IpAddr,
269 event_source: EventSource,
270 event_data: AVTransportEvent,
271) -> EnrichedEvent<AVTransportEvent> {
272 EnrichedEvent::with_registration_id(
273 registration_id,
274 speaker_ip,
275 Service::AVTransport,
276 event_source,
277 event_data,
278 )
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_av_transport_parser_service_type() {
287 let parser = AVTransportEventParser;
288 assert_eq!(parser.service_type(), Service::AVTransport);
289 }
290
291 #[test]
292 fn test_av_transport_event_creation() {
293 let event_data = AVTransportEventData {
294 instance: AVTransportInstance {
295 transport_state: Some(xml_utils::ValueAttribute {
296 val: "PLAYING".to_string(),
297 }),
298 transport_status: Some(xml_utils::ValueAttribute {
299 val: "OK".to_string(),
300 }),
301 speed: Some(xml_utils::ValueAttribute {
302 val: "1".to_string(),
303 }),
304 current_track_uri: None,
305 track_duration: None,
306 rel_time: None,
307 abs_time: None,
308 rel_count: None,
309 play_mode: None,
310 track_metadata: None,
311 next_track_uri: None,
312 next_track_metadata: None,
313 queue_length: None,
314 },
315 };
316
317 let event = AVTransportEvent {
318 property: AVTransportProperty {
319 last_change: event_data,
320 },
321 };
322
323 assert_eq!(event.transport_state(), Some("PLAYING".to_string()));
324 assert_eq!(event.transport_status(), Some("OK".to_string()));
325 }
326
327 #[test]
328 fn test_enriched_event_creation() {
329 let ip: IpAddr = "192.168.1.100".parse().unwrap();
330 let source = EventSource::UPnPNotification {
331 subscription_id: "uuid:123".to_string(),
332 };
333 let event_data = AVTransportEvent {
334 property: AVTransportProperty {
335 last_change: AVTransportEventData {
336 instance: AVTransportInstance {
337 transport_state: Some(xml_utils::ValueAttribute {
338 val: "PLAYING".to_string(),
339 }),
340 transport_status: None,
341 speed: None,
342 current_track_uri: None,
343 track_duration: None,
344 rel_time: None,
345 abs_time: None,
346 rel_count: None,
347 play_mode: None,
348 track_metadata: None,
349 next_track_uri: None,
350 next_track_metadata: None,
351 queue_length: None,
352 },
353 },
354 },
355 };
356
357 let enriched = create_enriched_event(ip, source, event_data);
358
359 assert_eq!(enriched.speaker_ip, ip);
360 assert_eq!(enriched.service, Service::AVTransport);
361 assert!(enriched.registration_id.is_none());
362 }
363
364 #[test]
365 fn test_enriched_event_with_registration_id() {
366 let ip: IpAddr = "192.168.1.100".parse().unwrap();
367 let source = EventSource::UPnPNotification {
368 subscription_id: "uuid:123".to_string(),
369 };
370 let event_data = AVTransportEvent {
371 property: AVTransportProperty {
372 last_change: AVTransportEventData {
373 instance: AVTransportInstance {
374 transport_state: Some(xml_utils::ValueAttribute {
375 val: "PLAYING".to_string(),
376 }),
377 transport_status: None,
378 speed: None,
379 current_track_uri: None,
380 track_duration: None,
381 rel_time: None,
382 abs_time: None,
383 rel_count: None,
384 play_mode: None,
385 track_metadata: None,
386 next_track_uri: None,
387 next_track_metadata: None,
388 queue_length: None,
389 },
390 },
391 },
392 };
393
394 let enriched = create_enriched_event_with_registration_id(42, ip, source, event_data);
395
396 assert_eq!(enriched.registration_id, Some(42));
397 }
398
399 #[test]
400 fn test_basic_xml_parsing() {
401 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
402 <e:property>
403 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/AVT/">
404 <InstanceID val="0">
405 <TransportState val="PLAYING"/>
406 <TransportStatus val="OK"/>
407 <CurrentTrack val="1"/>
408 <NumberOfTracks val="5"/>
409 </InstanceID>
410 </Event></LastChange>
411 </e:property>
412 </e:propertyset>"#;
413
414 let event = AVTransportEvent::from_xml(xml).unwrap();
415 assert_eq!(event.transport_state(), Some("PLAYING".to_string()));
416 assert_eq!(event.transport_status(), Some("OK".to_string()));
417 assert_eq!(event.rel_count(), Some(1));
418 assert_eq!(event.queue_length(), Some(5));
419 }
420
421 #[test]
422 fn test_into_state_maps_all_fields() {
423 let event = AVTransportEvent {
424 property: AVTransportProperty {
425 last_change: AVTransportEventData {
426 instance: AVTransportInstance {
427 transport_state: Some(xml_utils::ValueAttribute {
428 val: "PLAYING".to_string(),
429 }),
430 transport_status: Some(xml_utils::ValueAttribute {
431 val: "OK".to_string(),
432 }),
433 speed: Some(xml_utils::ValueAttribute {
434 val: "1".to_string(),
435 }),
436 current_track_uri: Some(xml_utils::ValueAttribute {
437 val: "x-sonos-spotify:track123".to_string(),
438 }),
439 track_duration: Some(xml_utils::ValueAttribute {
440 val: "0:03:45".to_string(),
441 }),
442 rel_time: Some(xml_utils::ValueAttribute {
443 val: "0:01:30".to_string(),
444 }),
445 abs_time: None,
446 rel_count: Some(xml_utils::ValueAttribute {
447 val: "1".to_string(),
448 }),
449 play_mode: Some(xml_utils::ValueAttribute {
450 val: "NORMAL".to_string(),
451 }),
452 track_metadata: None,
453 next_track_uri: None,
454 next_track_metadata: None,
455 queue_length: Some(xml_utils::ValueAttribute {
456 val: "5".to_string(),
457 }),
458 },
459 },
460 },
461 };
462
463 let state = event.into_state();
464
465 assert_eq!(state.transport_state, Some("PLAYING".to_string()));
466 assert_eq!(state.transport_status, Some("OK".to_string()));
467 assert_eq!(state.speed, Some("1".to_string()));
468 assert_eq!(
469 state.current_track_uri,
470 Some("x-sonos-spotify:track123".to_string())
471 );
472 assert_eq!(state.track_duration, Some("0:03:45".to_string()));
473 assert_eq!(state.rel_time, Some("0:01:30".to_string()));
474 assert_eq!(state.abs_time, None);
475 assert_eq!(state.rel_count, Some(1));
476 assert_eq!(state.play_mode, Some("NORMAL".to_string()));
477 assert_eq!(state.queue_length, Some(5));
478 }
479
480 #[test]
481 fn test_into_state_from_xml_round_trip() {
482 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
483 <e:property>
484 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/AVT/">
485 <InstanceID val="0">
486 <TransportState val="PLAYING"/>
487 <TransportStatus val="OK"/>
488 <CurrentTrack val="1"/>
489 <NumberOfTracks val="5"/>
490 </InstanceID>
491 </Event></LastChange>
492 </e:property>
493 </e:propertyset>"#;
494
495 let state = AVTransportEvent::from_xml(xml).unwrap().into_state();
496
497 assert_eq!(state.transport_state, Some("PLAYING".to_string()));
498 assert_eq!(state.transport_status, Some("OK".to_string()));
499 assert_eq!(state.rel_count, Some(1));
500 assert_eq!(state.queue_length, Some(5));
501 }
502}