import xml.etree.ElementTree as ET import json from typing import Any, Dict import logging def parse_last_change_event(xml_text: str, logger: logging.Logger) -> Dict[str, Any]: """ Parses the LastChange event XML from UPnP services. AVTransport and RenderingControl typically use this. The XML structure is or for AVTransport: """ changed_variables: Dict[str, Any] = {} try: # Remove default namespace for easier parsing if present xml_text = xml_text.replace( ' xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/"', "" ) xml_text = xml_text.replace( ' xmlns="urn:schemas-upnp-org:metadata-1-0/AVT/"', "" ) xml_text = xml_text.replace( ' xmlns="urn:schemas-wiimu-com:metadata-1-0/PlayQueue/"', "" ) root = ET.fromstring(xml_text) instance_id_node = root.find("InstanceID") or root.find("QueueID") if instance_id_node is None: logger.warning( "No InstanceID found in LastChange event XML: %s", xml_text[:200] ) return changed_variables for child in instance_id_node: variable_name = child.tag value = child.get("val") channel = child.get("channel") if variable_name == "Volume" or variable_name == "Mute": if variable_name not in changed_variables: changed_variables[variable_name] = [] channel_data = {"val": value} if channel: channel_data["channel"] = channel changed_variables[variable_name].append(channel_data) elif ( variable_name == "CurrentTrackMetaData" or variable_name == "AVTransportURIMetaData" or variable_name == "NextAVTransportURIMetaData" ): # Metadata can arrive as JSON or DIDL-Lite XML depending on source/app. try: if value is None: continue raw_value = value.strip() if raw_value.startswith("{") or raw_value.startswith("["): changed_variables[variable_name] = json.loads(raw_value) continue didl_root = ET.fromstring(value) item_node = didl_root.find( ".//{urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/}item" ) if item_node is not None: meta: Dict[str, Any] = {} title_node = item_node.find( "{http://purl.org/dc/elements/1.1/}title" ) if title_node is not None: meta["title"] = title_node.text artist_node = item_node.find( "{urn:schemas-upnp-org:metadata-1-0/upnp/}artist" ) if artist_node is not None: meta["artist"] = artist_node.text album_node = item_node.find( "{urn:schemas-upnp-org:metadata-1-0/upnp/}album" ) if album_node is not None: meta["album"] = album_node.text art_node = item_node.find( "{urn:schemas-upnp-org:metadata-1-0/upnp/}albumArtURI" ) if art_node is not None: meta["albumArtURI"] = art_node.text res_node = item_node.find( ".//{urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/}res" ) if res_node is not None: meta["res"] = res_node.text meta["duration"] = res_node.get("duration") changed_variables[variable_name] = meta else: changed_variables[variable_name] = ( value # Store raw if not parsable as DIDL ) except json.JSONDecodeError: if value: logger.debug( "Failed to parse metadata JSON for %s: %s", variable_name, value[:100], ) changed_variables[variable_name] = value except ET.ParseError: if value: logger.debug( "Failed to parse metadata XML for %s: %s", variable_name, value[:100], ) changed_variables[variable_name] = value # Store raw XML elif variable_name == "LoopMpde" or variable_name == "LoopMode": # interpret as integer if possible try: changed_variables[variable_name] = ( int(value) if value is not None else 4 ) except ValueError: changed_variables[variable_name] = 4 else: changed_variables[variable_name] = value logger.debug("Parsed LastChange event: %s", changed_variables) except ET.ParseError as e: logger.error( "Error parsing LastChange event XML: %s\nXML: %s", e, xml_text[:500] ) # Log more XML on error return changed_variables