1use crate::{ApiError, Result};
8use serde::de::{DeserializeOwned, Deserializer};
9use serde::{Deserialize, Serialize};
10
11pub fn parse<T: DeserializeOwned>(xml: &str) -> Result<T> {
26 quick_xml::de::from_str(xml)
27 .map_err(|e| ApiError::ParseError(format!("XML deserialization failed: {e}")))
28}
29
30pub fn deserialize_nested<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
46where
47 D: Deserializer<'de>,
48 T: DeserializeOwned,
49{
50 let s = String::deserialize(deserializer)?;
51 parse::<T>(&s).map_err(serde::de::Error::custom)
52}
53
54pub fn deserialize_zone_group_state<'de, D, T>(
59 deserializer: D,
60) -> std::result::Result<Option<T>, D::Error>
61where
62 D: Deserializer<'de>,
63 T: DeserializeOwned,
64{
65 let s = String::deserialize(deserializer)?;
66 if s.trim().is_empty() {
67 return Ok(None);
68 }
69 let parsed = parse::<T>(&s).map_err(serde::de::Error::custom)?;
70 Ok(Some(parsed))
71}
72
73#[derive(Debug, Clone, Deserialize, Serialize, Default)]
83pub struct ValueAttribute {
84 #[serde(rename = "@val", default)]
86 pub val: String,
87}
88
89#[derive(Debug, Clone, Default, Serialize)]
98pub struct NestedAttribute<T> {
99 pub val: Option<T>,
101}
102
103impl<'de, T: DeserializeOwned> Deserialize<'de> for NestedAttribute<T> {
104 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
105 where
106 D: Deserializer<'de>,
107 {
108 #[derive(Deserialize)]
109 struct RawAttr {
110 #[serde(rename = "@val", default)]
111 val: String,
112 }
113
114 let raw = RawAttr::deserialize(deserializer)?;
115
116 if raw.val.is_empty() {
117 return Ok(NestedAttribute { val: None });
118 }
119
120 match parse::<T>(&raw.val) {
122 Ok(parsed) => Ok(NestedAttribute { val: Some(parsed) }),
123 Err(_) => Ok(NestedAttribute { val: None }),
124 }
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
142#[serde(rename = "DIDL-Lite")]
143pub struct DidlLite {
144 #[serde(rename = "item", default)]
146 pub items: Vec<DidlItem>,
147}
148
149impl DidlLite {
150 pub fn from_xml(xml: &str) -> Result<Self> {
160 parse(xml)
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
166pub struct DidlItem {
167 #[serde(rename = "@id", default)]
169 pub id: String,
170
171 #[serde(rename = "@parentID", default)]
173 pub parent_id: String,
174
175 #[serde(rename = "@restricted", default)]
177 pub restricted: Option<String>,
178
179 #[serde(rename = "res", default)]
181 pub resources: Vec<DidlResource>,
182
183 #[serde(rename = "albumArtURI", default)]
185 pub album_art_uri: Option<String>,
186
187 #[serde(rename = "class", default)]
189 pub class: Option<String>,
190
191 #[serde(rename = "title", default)]
193 pub title: Option<String>,
194
195 #[serde(rename = "creator", default)]
197 pub creator: Option<String>,
198
199 #[serde(rename = "album", default)]
201 pub album: Option<String>,
202
203 #[serde(rename = "streamInfo", default)]
205 pub stream_info: Option<String>,
206}
207
208#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
210pub struct DidlResource {
211 #[serde(rename = "@duration", default)]
213 pub duration: Option<String>,
214
215 #[serde(rename = "@protocolInfo", default)]
217 pub protocol_info: Option<String>,
218
219 #[serde(rename = "$value", default)]
221 pub uri: Option<String>,
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
231 fn test_parse_survives_cdata_and_comment_containing_gt() {
232 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0"><!-- a > b --><e:property><TransportState val="PLAYING"/><Note><![CDATA[3 > 2 && <ok>]]></Note></e:property></e:propertyset>"#;
233
234 #[derive(Debug, Deserialize)]
235 struct PropertySet {
236 property: Property,
237 }
238 #[derive(Debug, Deserialize)]
239 struct Property {
240 #[serde(rename = "TransportState")]
241 transport_state: ValueAttribute,
242 #[serde(rename = "Note")]
243 note: String,
244 }
245
246 let parsed: PropertySet = parse(xml).unwrap();
247 assert_eq!(parsed.property.transport_state.val, "PLAYING");
248 assert_eq!(parsed.property.note, "3 > 2 && <ok>");
249 }
250
251 #[test]
252 fn test_value_attribute_deserialize() {
253 let xml = r#"<Root><TransportState val="PLAYING"/></Root>"#;
254
255 #[derive(Debug, Deserialize)]
256 struct Root {
257 #[serde(rename = "TransportState")]
258 transport_state: ValueAttribute,
259 }
260
261 let result: Root = parse(xml).unwrap();
262 assert_eq!(result.transport_state.val, "PLAYING");
263 }
264
265 #[test]
266 fn test_value_attribute_empty() {
267 let xml = r#"<Root><TransportState val=""/></Root>"#;
268
269 #[derive(Debug, Deserialize)]
270 struct Root {
271 #[serde(rename = "TransportState")]
272 transport_state: ValueAttribute,
273 }
274
275 let result: Root = parse(xml).unwrap();
276 assert_eq!(result.transport_state.val, "");
277 }
278
279 #[test]
280 fn test_value_attribute_default() {
281 let xml = r#"<Root><TransportState/></Root>"#;
282
283 #[derive(Debug, Deserialize)]
284 struct Root {
285 #[serde(rename = "TransportState")]
286 transport_state: ValueAttribute,
287 }
288
289 let result: Root = parse(xml).unwrap();
290 assert_eq!(result.transport_state.val, "");
291 }
292
293 #[test]
294 fn test_parse_didl_lite_basic() {
295 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>"#;
296
297 let result = DidlLite::from_xml(didl_xml);
298 assert!(
299 result.is_ok(),
300 "Failed to parse DIDL-Lite: {:?}",
301 result.err()
302 );
303
304 let didl = result.unwrap();
305 assert_eq!(didl.items.len(), 1);
306 let item = &didl.items[0];
307 assert_eq!(item.id, "-1");
308 assert_eq!(item.parent_id, "-1");
309 assert_eq!(item.title, Some("Test Song".to_string()));
310 assert_eq!(item.creator, Some("Test Artist".to_string()));
311 assert_eq!(item.album, Some("Test Album".to_string()));
312 }
313
314 #[test]
315 fn test_parse_didl_lite_with_resource() {
316 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>"#;
317
318 let result = DidlLite::from_xml(didl_xml);
319 assert!(
320 result.is_ok(),
321 "Failed to parse DIDL-Lite with resource: {:?}",
322 result.err()
323 );
324
325 let didl = result.unwrap();
326 let item = &didl.items[0];
327 assert_eq!(item.title, Some("Song".to_string()));
328 assert_eq!(item.creator, Some("Artist".to_string()));
329
330 let res = &item.resources[0];
331 assert_eq!(res.duration, Some("0:03:58".to_string()));
332 assert_eq!(
333 res.protocol_info,
334 Some("http-get:*:audio/mpeg:*".to_string())
335 );
336 assert_eq!(res.uri, Some("http://example.com/song.mp3".to_string()));
337 }
338
339 #[test]
340 fn test_parse_didl_lite_minimal() {
341 let didl_xml = r#"<DIDL-Lite><item id="1" parentID="0"></item></DIDL-Lite>"#;
342
343 let result = DidlLite::from_xml(didl_xml);
344 assert!(
345 result.is_ok(),
346 "Failed to parse minimal DIDL-Lite: {:?}",
347 result.err()
348 );
349
350 let didl = result.unwrap();
351 let item = &didl.items[0];
352 assert_eq!(item.id, "1");
353 assert_eq!(item.parent_id, "0");
354 assert_eq!(item.title, None);
355 assert_eq!(item.creator, None);
356 assert_eq!(item.album, None);
357 }
358}