1use crate::{ApiError, Result};
8use serde::de::{DeserializeOwned, Deserializer};
9use serde::{Deserialize, Serialize};
10
11pub fn parse<T: DeserializeOwned>(xml: &str) -> Result<T> {
25 let stripped = strip_namespaces(xml);
26 quick_xml::de::from_str(&stripped)
27 .map_err(|e| ApiError::ParseError(format!("XML deserialization failed: {e}")))
28}
29
30pub fn strip_namespaces(xml: &str) -> String {
40 let mut result = String::with_capacity(xml.len());
41 let mut chars = xml.chars().peekable();
42
43 while let Some(c) = chars.next() {
44 if c == '<' {
45 result.push(c);
46
47 let is_closing = chars.peek() == Some(&'/');
49 if is_closing {
50 result.push(chars.next().unwrap());
51 }
52
53 if let Some(&next) = chars.peek() {
55 if next == '?' || next == '!' {
56 for ch in chars.by_ref() {
58 result.push(ch);
59 if ch == '>' {
60 break;
61 }
62 }
63 continue;
64 }
65 }
66
67 let mut tag_name = String::new();
69 while let Some(&ch) = chars.peek() {
70 if ch.is_whitespace() || ch == '>' || ch == '/' {
71 break;
72 }
73 tag_name.push(chars.next().unwrap());
74 }
75
76 if let Some(pos) = tag_name.find(':') {
78 result.push_str(&tag_name[pos + 1..]);
79 } else {
80 result.push_str(&tag_name);
81 }
82
83 while let Some(&ch) = chars.peek() {
85 if ch == '>' {
86 result.push(chars.next().unwrap());
87 break;
88 }
89 if ch == '/' {
90 result.push(chars.next().unwrap());
91 continue;
92 }
93 if ch.is_whitespace() {
94 result.push(chars.next().unwrap());
95 continue;
96 }
97
98 let mut attr_name = String::new();
100 while let Some(&ach) = chars.peek() {
101 if ach == '=' || ach.is_whitespace() || ach == '>' || ach == '/' {
102 break;
103 }
104 attr_name.push(chars.next().unwrap());
105 }
106
107 if attr_name.starts_with("xmlns") {
109 if chars.peek() == Some(&'=') {
112 chars.next();
113 }
114 if let Some("e) = chars.peek() {
116 if quote == '"' || quote == '\'' {
117 chars.next();
118 for ch in chars.by_ref() {
119 if ch == quote {
120 break;
121 }
122 }
123 }
124 }
125 } else {
126 if let Some(pos) = attr_name.find(':') {
128 result.push_str(&attr_name[pos + 1..]);
129 } else {
130 result.push_str(&attr_name);
131 }
132
133 while let Some(&ach) = chars.peek() {
135 if ach == '>' || ach == '/' {
136 break;
137 }
138 if ach == '"' || ach == '\'' {
139 let quote = chars.next().unwrap();
140 result.push(quote);
141 for ch in chars.by_ref() {
142 result.push(ch);
143 if ch == quote {
144 break;
145 }
146 }
147 break;
148 }
149 result.push(chars.next().unwrap());
150 }
151 }
152 }
153 } else {
154 result.push(c);
155 }
156 }
157
158 result
159}
160
161pub fn deserialize_nested<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
177where
178 D: Deserializer<'de>,
179 T: DeserializeOwned,
180{
181 let s = String::deserialize(deserializer)?;
182 parse::<T>(&s).map_err(serde::de::Error::custom)
183}
184
185pub fn deserialize_zone_group_state<'de, D, T>(
190 deserializer: D,
191) -> std::result::Result<Option<T>, D::Error>
192where
193 D: Deserializer<'de>,
194 T: DeserializeOwned,
195{
196 let s = String::deserialize(deserializer)?;
197 if s.trim().is_empty() {
198 return Ok(None);
199 }
200 let parsed = parse::<T>(&s).map_err(serde::de::Error::custom)?;
201 Ok(Some(parsed))
202}
203
204#[derive(Debug, Clone, Deserialize, Serialize, Default)]
214pub struct ValueAttribute {
215 #[serde(rename = "@val", default)]
217 pub val: String,
218}
219
220#[derive(Debug, Clone, Default, Serialize)]
229pub struct NestedAttribute<T> {
230 pub val: Option<T>,
232}
233
234impl<'de, T: DeserializeOwned> Deserialize<'de> for NestedAttribute<T> {
235 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
236 where
237 D: Deserializer<'de>,
238 {
239 #[derive(Deserialize)]
240 struct RawAttr {
241 #[serde(rename = "@val", default)]
242 val: String,
243 }
244
245 let raw = RawAttr::deserialize(deserializer)?;
246
247 if raw.val.is_empty() {
248 return Ok(NestedAttribute { val: None });
249 }
250
251 match parse::<T>(&raw.val) {
253 Ok(parsed) => Ok(NestedAttribute { val: Some(parsed) }),
254 Err(_) => Ok(NestedAttribute { val: None }),
255 }
256 }
257}
258
259#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
273#[serde(rename = "DIDL-Lite")]
274pub struct DidlLite {
275 #[serde(rename = "item", default)]
277 pub items: Vec<DidlItem>,
278}
279
280impl DidlLite {
281 pub fn from_xml(xml: &str) -> Result<Self> {
291 parse(xml)
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
297pub struct DidlItem {
298 #[serde(rename = "@id", default)]
300 pub id: String,
301
302 #[serde(rename = "@parentID", default)]
304 pub parent_id: String,
305
306 #[serde(rename = "@restricted", default)]
308 pub restricted: Option<String>,
309
310 #[serde(rename = "res", default)]
312 pub resources: Vec<DidlResource>,
313
314 #[serde(rename = "albumArtURI", default)]
316 pub album_art_uri: Option<String>,
317
318 #[serde(rename = "class", default)]
320 pub class: Option<String>,
321
322 #[serde(rename = "title", default)]
324 pub title: Option<String>,
325
326 #[serde(rename = "creator", default)]
328 pub creator: Option<String>,
329
330 #[serde(rename = "album", default)]
332 pub album: Option<String>,
333
334 #[serde(rename = "streamInfo", default)]
336 pub stream_info: Option<String>,
337}
338
339#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
341pub struct DidlResource {
342 #[serde(rename = "@duration", default)]
344 pub duration: Option<String>,
345
346 #[serde(rename = "@protocolInfo", default)]
348 pub protocol_info: Option<String>,
349
350 #[serde(rename = "$value", default)]
352 pub uri: Option<String>,
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn test_strip_namespaces_basic() {
361 let input = r#"<e:propertyset><e:property>test</e:property></e:propertyset>"#;
362 let expected = r#"<propertyset><property>test</property></propertyset>"#;
363 assert_eq!(strip_namespaces(input), expected);
364 }
365
366 #[test]
367 fn test_strip_namespaces_with_attributes() {
368 let input = r#"<dc:title id="1">Song</dc:title>"#;
369 let expected = r#"<title id="1">Song</title>"#;
370 assert_eq!(strip_namespaces(input), expected);
371 }
372
373 #[test]
374 fn test_strip_namespaces_multiple() {
375 let input = r#"<dc:title>Song</dc:title><upnp:album>Album</upnp:album>"#;
376 let expected = r#"<title>Song</title><album>Album</album>"#;
377 assert_eq!(strip_namespaces(input), expected);
378 }
379
380 #[test]
381 fn test_value_attribute_deserialize() {
382 let xml = r#"<Root><TransportState val="PLAYING"/></Root>"#;
383
384 #[derive(Debug, Deserialize)]
385 struct Root {
386 #[serde(rename = "TransportState")]
387 transport_state: ValueAttribute,
388 }
389
390 let result: Root = parse(xml).unwrap();
391 assert_eq!(result.transport_state.val, "PLAYING");
392 }
393
394 #[test]
395 fn test_value_attribute_empty() {
396 let xml = r#"<Root><TransportState val=""/></Root>"#;
397
398 #[derive(Debug, Deserialize)]
399 struct Root {
400 #[serde(rename = "TransportState")]
401 transport_state: ValueAttribute,
402 }
403
404 let result: Root = parse(xml).unwrap();
405 assert_eq!(result.transport_state.val, "");
406 }
407
408 #[test]
409 fn test_value_attribute_default() {
410 let xml = r#"<Root><TransportState/></Root>"#;
411
412 #[derive(Debug, Deserialize)]
413 struct Root {
414 #[serde(rename = "TransportState")]
415 transport_state: ValueAttribute,
416 }
417
418 let result: Root = parse(xml).unwrap();
419 assert_eq!(result.transport_state.val, "");
420 }
421
422 #[test]
423 fn test_parse_didl_lite_basic() {
424 let didl_xml = r#"<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"><item id="-1" parentID="-1"><dc:title>Test Song</dc:title><dc:creator>Test Artist</dc:creator><upnp:album>Test Album</upnp:album></item></DIDL-Lite>"#;
425
426 let result = DidlLite::from_xml(didl_xml);
427 assert!(
428 result.is_ok(),
429 "Failed to parse DIDL-Lite: {:?}",
430 result.err()
431 );
432
433 let didl = result.unwrap();
434 assert_eq!(didl.items.len(), 1);
435 let item = &didl.items[0];
436 assert_eq!(item.id, "-1");
437 assert_eq!(item.parent_id, "-1");
438 assert_eq!(item.title, Some("Test Song".to_string()));
439 assert_eq!(item.creator, Some("Test Artist".to_string()));
440 assert_eq!(item.album, Some("Test Album".to_string()));
441 }
442
443 #[test]
444 fn test_parse_didl_lite_with_resource() {
445 let didl_xml = r#"<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/"><item id="-1" parentID="-1"><dc:title>Song</dc:title><dc:creator>Artist</dc:creator><res duration="0:03:58" protocolInfo="http-get:*:audio/mpeg:*">http://example.com/song.mp3</res></item></DIDL-Lite>"#;
446
447 let result = DidlLite::from_xml(didl_xml);
448 assert!(
449 result.is_ok(),
450 "Failed to parse DIDL-Lite with resource: {:?}",
451 result.err()
452 );
453
454 let didl = result.unwrap();
455 let item = &didl.items[0];
456 assert_eq!(item.title, Some("Song".to_string()));
457 assert_eq!(item.creator, Some("Artist".to_string()));
458
459 let res = &item.resources[0];
460 assert_eq!(res.duration, Some("0:03:58".to_string()));
461 assert_eq!(
462 res.protocol_info,
463 Some("http-get:*:audio/mpeg:*".to_string())
464 );
465 assert_eq!(res.uri, Some("http://example.com/song.mp3".to_string()));
466 }
467
468 #[test]
469 fn test_parse_didl_lite_minimal() {
470 let didl_xml = r#"<DIDL-Lite><item id="1" parentID="0"></item></DIDL-Lite>"#;
471
472 let result = DidlLite::from_xml(didl_xml);
473 assert!(
474 result.is_ok(),
475 "Failed to parse minimal DIDL-Lite: {:?}",
476 result.err()
477 );
478
479 let didl = result.unwrap();
480 let item = &didl.items[0];
481 assert_eq!(item.id, "1");
482 assert_eq!(item.parent_id, "0");
483 assert_eq!(item.title, None);
484 assert_eq!(item.creator, None);
485 assert_eq!(item.album, None);
486 }
487}