use crate::{ApiError, Result};
use serde::de::{DeserializeOwned, Deserializer};
use serde::{Deserialize, Serialize};
pub fn parse<T: DeserializeOwned>(xml: &str) -> Result<T> {
let stripped = strip_namespaces(xml);
quick_xml::de::from_str(&stripped)
.map_err(|e| ApiError::ParseError(format!("XML deserialization failed: {e}")))
}
pub fn strip_namespaces(xml: &str) -> String {
let mut result = String::with_capacity(xml.len());
let mut chars = xml.chars().peekable();
while let Some(c) = chars.next() {
if c == '<' {
result.push(c);
let is_closing = chars.peek() == Some(&'/');
if is_closing {
result.push(chars.next().unwrap());
}
if let Some(&next) = chars.peek() {
if next == '?' || next == '!' {
for ch in chars.by_ref() {
result.push(ch);
if ch == '>' {
break;
}
}
continue;
}
}
let mut tag_name = String::new();
while let Some(&ch) = chars.peek() {
if ch.is_whitespace() || ch == '>' || ch == '/' {
break;
}
tag_name.push(chars.next().unwrap());
}
if let Some(pos) = tag_name.find(':') {
result.push_str(&tag_name[pos + 1..]);
} else {
result.push_str(&tag_name);
}
while let Some(&ch) = chars.peek() {
if ch == '>' {
result.push(chars.next().unwrap());
break;
}
if ch == '/' {
result.push(chars.next().unwrap());
continue;
}
if ch.is_whitespace() {
result.push(chars.next().unwrap());
continue;
}
let mut attr_name = String::new();
while let Some(&ach) = chars.peek() {
if ach == '=' || ach.is_whitespace() || ach == '>' || ach == '/' {
break;
}
attr_name.push(chars.next().unwrap());
}
if attr_name.starts_with("xmlns") {
if chars.peek() == Some(&'=') {
chars.next();
}
if let Some("e) = chars.peek() {
if quote == '"' || quote == '\'' {
chars.next();
for ch in chars.by_ref() {
if ch == quote {
break;
}
}
}
}
} else {
if let Some(pos) = attr_name.find(':') {
result.push_str(&attr_name[pos + 1..]);
} else {
result.push_str(&attr_name);
}
while let Some(&ach) = chars.peek() {
if ach == '>' || ach == '/' {
break;
}
if ach == '"' || ach == '\'' {
let quote = chars.next().unwrap();
result.push(quote);
for ch in chars.by_ref() {
result.push(ch);
if ch == quote {
break;
}
}
break;
}
result.push(chars.next().unwrap());
}
}
}
} else {
result.push(c);
}
}
result
}
pub fn deserialize_nested<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
where
D: Deserializer<'de>,
T: DeserializeOwned,
{
let s = String::deserialize(deserializer)?;
parse::<T>(&s).map_err(serde::de::Error::custom)
}
pub fn deserialize_zone_group_state<'de, D, T>(
deserializer: D,
) -> std::result::Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: DeserializeOwned,
{
let s = String::deserialize(deserializer)?;
if s.trim().is_empty() {
return Ok(None);
}
let parsed = parse::<T>(&s).map_err(serde::de::Error::custom)?;
Ok(Some(parsed))
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct ValueAttribute {
#[serde(rename = "@val", default)]
pub val: String,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct NestedAttribute<T> {
pub val: Option<T>,
}
impl<'de, T: DeserializeOwned> Deserialize<'de> for NestedAttribute<T> {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct RawAttr {
#[serde(rename = "@val", default)]
val: String,
}
let raw = RawAttr::deserialize(deserializer)?;
if raw.val.is_empty() {
return Ok(NestedAttribute { val: None });
}
match parse::<T>(&raw.val) {
Ok(parsed) => Ok(NestedAttribute { val: Some(parsed) }),
Err(_) => Ok(NestedAttribute { val: None }),
}
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(rename = "DIDL-Lite")]
pub struct DidlLite {
#[serde(rename = "item", default)]
pub items: Vec<DidlItem>,
}
impl DidlLite {
pub fn from_xml(xml: &str) -> Result<Self> {
parse(xml)
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct DidlItem {
#[serde(rename = "@id", default)]
pub id: String,
#[serde(rename = "@parentID", default)]
pub parent_id: String,
#[serde(rename = "@restricted", default)]
pub restricted: Option<String>,
#[serde(rename = "res", default)]
pub resources: Vec<DidlResource>,
#[serde(rename = "albumArtURI", default)]
pub album_art_uri: Option<String>,
#[serde(rename = "class", default)]
pub class: Option<String>,
#[serde(rename = "title", default)]
pub title: Option<String>,
#[serde(rename = "creator", default)]
pub creator: Option<String>,
#[serde(rename = "album", default)]
pub album: Option<String>,
#[serde(rename = "streamInfo", default)]
pub stream_info: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
pub struct DidlResource {
#[serde(rename = "@duration", default)]
pub duration: Option<String>,
#[serde(rename = "@protocolInfo", default)]
pub protocol_info: Option<String>,
#[serde(rename = "$value", default)]
pub uri: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_namespaces_basic() {
let input = r#"<e:propertyset><e:property>test</e:property></e:propertyset>"#;
let expected = r#"<propertyset><property>test</property></propertyset>"#;
assert_eq!(strip_namespaces(input), expected);
}
#[test]
fn test_strip_namespaces_with_attributes() {
let input = r#"<dc:title id="1">Song</dc:title>"#;
let expected = r#"<title id="1">Song</title>"#;
assert_eq!(strip_namespaces(input), expected);
}
#[test]
fn test_strip_namespaces_multiple() {
let input = r#"<dc:title>Song</dc:title><upnp:album>Album</upnp:album>"#;
let expected = r#"<title>Song</title><album>Album</album>"#;
assert_eq!(strip_namespaces(input), expected);
}
#[test]
fn test_value_attribute_deserialize() {
let xml = r#"<Root><TransportState val="PLAYING"/></Root>"#;
#[derive(Debug, Deserialize)]
struct Root {
#[serde(rename = "TransportState")]
transport_state: ValueAttribute,
}
let result: Root = parse(xml).unwrap();
assert_eq!(result.transport_state.val, "PLAYING");
}
#[test]
fn test_value_attribute_empty() {
let xml = r#"<Root><TransportState val=""/></Root>"#;
#[derive(Debug, Deserialize)]
struct Root {
#[serde(rename = "TransportState")]
transport_state: ValueAttribute,
}
let result: Root = parse(xml).unwrap();
assert_eq!(result.transport_state.val, "");
}
#[test]
fn test_value_attribute_default() {
let xml = r#"<Root><TransportState/></Root>"#;
#[derive(Debug, Deserialize)]
struct Root {
#[serde(rename = "TransportState")]
transport_state: ValueAttribute,
}
let result: Root = parse(xml).unwrap();
assert_eq!(result.transport_state.val, "");
}
#[test]
fn test_parse_didl_lite_basic() {
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>"#;
let result = DidlLite::from_xml(didl_xml);
assert!(
result.is_ok(),
"Failed to parse DIDL-Lite: {:?}",
result.err()
);
let didl = result.unwrap();
assert_eq!(didl.items.len(), 1);
let item = &didl.items[0];
assert_eq!(item.id, "-1");
assert_eq!(item.parent_id, "-1");
assert_eq!(item.title, Some("Test Song".to_string()));
assert_eq!(item.creator, Some("Test Artist".to_string()));
assert_eq!(item.album, Some("Test Album".to_string()));
}
#[test]
fn test_parse_didl_lite_with_resource() {
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>"#;
let result = DidlLite::from_xml(didl_xml);
assert!(
result.is_ok(),
"Failed to parse DIDL-Lite with resource: {:?}",
result.err()
);
let didl = result.unwrap();
let item = &didl.items[0];
assert_eq!(item.title, Some("Song".to_string()));
assert_eq!(item.creator, Some("Artist".to_string()));
let res = &item.resources[0];
assert_eq!(res.duration, Some("0:03:58".to_string()));
assert_eq!(
res.protocol_info,
Some("http-get:*:audio/mpeg:*".to_string())
);
assert_eq!(res.uri, Some("http://example.com/song.mp3".to_string()));
}
#[test]
fn test_parse_didl_lite_minimal() {
let didl_xml = r#"<DIDL-Lite><item id="1" parentID="0"></item></DIDL-Lite>"#;
let result = DidlLite::from_xml(didl_xml);
assert!(
result.is_ok(),
"Failed to parse minimal DIDL-Lite: {:?}",
result.err()
);
let didl = result.unwrap();
let item = &didl.items[0];
assert_eq!(item.id, "1");
assert_eq!(item.parent_id, "0");
assert_eq!(item.title, None);
assert_eq!(item.creator, None);
assert_eq!(item.album, None);
}
}