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 let clean_xml = xml_utils::strip_namespaces(xml);
237 quick_xml::de::from_str(&clean_xml)
238 .map_err(|e| ApiError::ParseError(format!("Failed to parse AVTransport XML: {e}")))
239 }
240}
241
242pub struct AVTransportEventParser;
244
245impl EventParser for AVTransportEventParser {
246 type EventData = AVTransportEvent;
247
248 fn parse_upnp_event(&self, xml: &str) -> Result<Self::EventData> {
249 AVTransportEvent::from_xml(xml)
250 }
251
252 fn service_type(&self) -> Service {
253 Service::AVTransport
254 }
255}
256
257pub fn create_enriched_event(
259 speaker_ip: IpAddr,
260 event_source: EventSource,
261 event_data: AVTransportEvent,
262) -> EnrichedEvent<AVTransportEvent> {
263 EnrichedEvent::new(speaker_ip, Service::AVTransport, event_source, event_data)
264}
265
266pub fn create_enriched_event_with_registration_id(
268 registration_id: u64,
269 speaker_ip: IpAddr,
270 event_source: EventSource,
271 event_data: AVTransportEvent,
272) -> EnrichedEvent<AVTransportEvent> {
273 EnrichedEvent::with_registration_id(
274 registration_id,
275 speaker_ip,
276 Service::AVTransport,
277 event_source,
278 event_data,
279 )
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_av_transport_parser_service_type() {
288 let parser = AVTransportEventParser;
289 assert_eq!(parser.service_type(), Service::AVTransport);
290 }
291
292 #[test]
293 fn test_av_transport_event_creation() {
294 let event_data = AVTransportEventData {
295 instance: AVTransportInstance {
296 transport_state: Some(xml_utils::ValueAttribute {
297 val: "PLAYING".to_string(),
298 }),
299 transport_status: Some(xml_utils::ValueAttribute {
300 val: "OK".to_string(),
301 }),
302 speed: Some(xml_utils::ValueAttribute {
303 val: "1".to_string(),
304 }),
305 current_track_uri: None,
306 track_duration: None,
307 rel_time: None,
308 abs_time: None,
309 rel_count: None,
310 play_mode: None,
311 track_metadata: None,
312 next_track_uri: None,
313 next_track_metadata: None,
314 queue_length: None,
315 },
316 };
317
318 let event = AVTransportEvent {
319 property: AVTransportProperty {
320 last_change: event_data,
321 },
322 };
323
324 assert_eq!(event.transport_state(), Some("PLAYING".to_string()));
325 assert_eq!(event.transport_status(), Some("OK".to_string()));
326 }
327
328 #[test]
329 fn test_enriched_event_creation() {
330 let ip: IpAddr = "192.168.1.100".parse().unwrap();
331 let source = EventSource::UPnPNotification {
332 subscription_id: "uuid:123".to_string(),
333 };
334 let event_data = AVTransportEvent {
335 property: AVTransportProperty {
336 last_change: AVTransportEventData {
337 instance: AVTransportInstance {
338 transport_state: Some(xml_utils::ValueAttribute {
339 val: "PLAYING".to_string(),
340 }),
341 transport_status: None,
342 speed: None,
343 current_track_uri: None,
344 track_duration: None,
345 rel_time: None,
346 abs_time: None,
347 rel_count: None,
348 play_mode: None,
349 track_metadata: None,
350 next_track_uri: None,
351 next_track_metadata: None,
352 queue_length: None,
353 },
354 },
355 },
356 };
357
358 let enriched = create_enriched_event(ip, source, event_data);
359
360 assert_eq!(enriched.speaker_ip, ip);
361 assert_eq!(enriched.service, Service::AVTransport);
362 assert!(enriched.registration_id.is_none());
363 }
364
365 #[test]
366 fn test_enriched_event_with_registration_id() {
367 let ip: IpAddr = "192.168.1.100".parse().unwrap();
368 let source = EventSource::UPnPNotification {
369 subscription_id: "uuid:123".to_string(),
370 };
371 let event_data = AVTransportEvent {
372 property: AVTransportProperty {
373 last_change: AVTransportEventData {
374 instance: AVTransportInstance {
375 transport_state: Some(xml_utils::ValueAttribute {
376 val: "PLAYING".to_string(),
377 }),
378 transport_status: None,
379 speed: None,
380 current_track_uri: None,
381 track_duration: None,
382 rel_time: None,
383 abs_time: None,
384 rel_count: None,
385 play_mode: None,
386 track_metadata: None,
387 next_track_uri: None,
388 next_track_metadata: None,
389 queue_length: None,
390 },
391 },
392 },
393 };
394
395 let enriched = create_enriched_event_with_registration_id(42, ip, source, event_data);
396
397 assert_eq!(enriched.registration_id, Some(42));
398 }
399
400 #[test]
401 fn test_basic_xml_parsing() {
402 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
403 <e:property>
404 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/AVT/">
405 <InstanceID val="0">
406 <TransportState val="PLAYING"/>
407 <TransportStatus val="OK"/>
408 <CurrentTrack val="1"/>
409 <NumberOfTracks val="5"/>
410 </InstanceID>
411 </Event></LastChange>
412 </e:property>
413 </e:propertyset>"#;
414
415 let event = AVTransportEvent::from_xml(xml).unwrap();
416 assert_eq!(event.transport_state(), Some("PLAYING".to_string()));
417 assert_eq!(event.transport_status(), Some("OK".to_string()));
418 assert_eq!(event.rel_count(), Some(1));
419 assert_eq!(event.queue_length(), Some(5));
420 }
421
422 #[test]
423 fn test_into_state_maps_all_fields() {
424 let event = AVTransportEvent {
425 property: AVTransportProperty {
426 last_change: AVTransportEventData {
427 instance: AVTransportInstance {
428 transport_state: Some(xml_utils::ValueAttribute {
429 val: "PLAYING".to_string(),
430 }),
431 transport_status: Some(xml_utils::ValueAttribute {
432 val: "OK".to_string(),
433 }),
434 speed: Some(xml_utils::ValueAttribute {
435 val: "1".to_string(),
436 }),
437 current_track_uri: Some(xml_utils::ValueAttribute {
438 val: "x-sonos-spotify:track123".to_string(),
439 }),
440 track_duration: Some(xml_utils::ValueAttribute {
441 val: "0:03:45".to_string(),
442 }),
443 rel_time: Some(xml_utils::ValueAttribute {
444 val: "0:01:30".to_string(),
445 }),
446 abs_time: None,
447 rel_count: Some(xml_utils::ValueAttribute {
448 val: "1".to_string(),
449 }),
450 play_mode: Some(xml_utils::ValueAttribute {
451 val: "NORMAL".to_string(),
452 }),
453 track_metadata: None,
454 next_track_uri: None,
455 next_track_metadata: None,
456 queue_length: Some(xml_utils::ValueAttribute {
457 val: "5".to_string(),
458 }),
459 },
460 },
461 },
462 };
463
464 let state = event.into_state();
465
466 assert_eq!(state.transport_state, Some("PLAYING".to_string()));
467 assert_eq!(state.transport_status, Some("OK".to_string()));
468 assert_eq!(state.speed, Some("1".to_string()));
469 assert_eq!(
470 state.current_track_uri,
471 Some("x-sonos-spotify:track123".to_string())
472 );
473 assert_eq!(state.track_duration, Some("0:03:45".to_string()));
474 assert_eq!(state.rel_time, Some("0:01:30".to_string()));
475 assert_eq!(state.abs_time, None);
476 assert_eq!(state.rel_count, Some(1));
477 assert_eq!(state.play_mode, Some("NORMAL".to_string()));
478 assert_eq!(state.queue_length, Some(5));
479 }
480
481 #[test]
482 fn test_into_state_from_xml_round_trip() {
483 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
484 <e:property>
485 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/AVT/">
486 <InstanceID val="0">
487 <TransportState val="PLAYING"/>
488 <TransportStatus val="OK"/>
489 <CurrentTrack val="1"/>
490 <NumberOfTracks val="5"/>
491 </InstanceID>
492 </Event></LastChange>
493 </e:property>
494 </e:propertyset>"#;
495
496 let state = AVTransportEvent::from_xml(xml).unwrap().into_state();
497
498 assert_eq!(state.transport_state, Some("PLAYING".to_string()));
499 assert_eq!(state.transport_status, Some("OK".to_string()));
500 assert_eq!(state.rel_count, Some(1));
501 assert_eq!(state.queue_length, Some(5));
502 }
503}