fix non-ascii characters in didl_lite metadata (#2256)
authorFabian Munkes <105975993+fmunkes@users.noreply.github.com>
Wed, 2 Jul 2025 10:10:02 +0000 (12:10 +0200)
committerGitHub <noreply@github.com>
Wed, 2 Jul 2025 10:10:02 +0000 (12:10 +0200)
music_assistant/helpers/upnp.py

index 47041a8cb0c783eb75b2f46c7d07c554518b9f43..de289156510a26b8745519d3f0a12af01993bc3e 100644 (file)
@@ -111,6 +111,21 @@ def get_xml_soap_set_next_url(player_media: PlayerMedia) -> tuple[str, str]:
 # DIDL-LITE
 def create_didl_metadata(media: PlayerMedia) -> str:
     """Create DIDL metadata string from url and PlayerMedia."""
+
+    def escape_metadata(data: str) -> str:
+        """Escape didl metadata."""
+        data = xmlescape(data)
+        # Escape non-ascii to decimal code.
+        result = ""
+        for char in data:
+            unicode_code = ord(char)
+            if unicode_code < 128:
+                # ascii
+                result += char
+            else:
+                result += f"&#{unicode_code};"
+        return result
+
     ext = media.uri.split(".")[-1].split("?")[0]
     image_url = media.image_url or MASS_LOGO_ONLINE
     if media.media_type in (MediaType.FLOW_STREAM, MediaType.RADIO) or not media.duration:
@@ -119,12 +134,12 @@ def create_didl_metadata(media: PlayerMedia) -> str:
         return (
             '<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">'
             f'<item id="flowmode" parentID="0" restricted="1">'
-            f"<dc:title>{xmlescape(title)}</dc:title>"
-            f"<upnp:albumArtURI>{xmlescape(image_url)}</upnp:albumArtURI>"
-            f"<dc:queueItemId>{media.uri}</dc:queueItemId>"
+            f"<dc:title>{escape_metadata(title)}</dc:title>"
+            f"<upnp:albumArtURI>{escape_metadata(image_url)}</upnp:albumArtURI>"
+            f"<dc:queueItemId>{escape_metadata(media.uri)}</dc:queueItemId>"
             "<upnp:class>object.item.audioItem.audioBroadcast</upnp:class>"
             f"<upnp:mimeType>audio/{ext}</upnp:mimeType>"
-            f'<res protocolInfo="http-get:*:audio/{ext}:DLNA.ORG_PN={ext.upper()};DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0d500000000000000000000000000000">{xmlescape(media.uri)}</res>'
+            f'<res protocolInfo="http-get:*:audio/{ext}:DLNA.ORG_PN={ext.upper()};DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0d500000000000000000000000000000">{escape_metadata(media.uri)}</res>'
             "</item>"
             "</DIDL-Lite>"
         )
@@ -135,17 +150,17 @@ def create_didl_metadata(media: PlayerMedia) -> str:
     return (
         '<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/">'
         f'<item id="{media.queue_item_id or xmlescape(media.uri)}" restricted="true" parentID="{media.queue_id or ""}">'
-        f"<dc:title>{xmlescape(media.title or media.uri)}</dc:title>"
-        f"<dc:creator>{xmlescape(media.artist or '')}</dc:creator>"
-        f"<upnp:album>{xmlescape(media.album or '')}</upnp:album>"
-        f"<upnp:artist>{xmlescape(media.artist or '')}</upnp:artist>"
+        f"<dc:title>{escape_metadata(media.title or media.uri)}</dc:title>"
+        f"<dc:creator>{escape_metadata(media.artist or '')}</dc:creator>"
+        f"<upnp:album>{escape_metadata(media.album or '')}</upnp:album>"
+        f"<upnp:artist>{escape_metadata(media.artist or '')}</upnp:artist>"
         f"<upnp:duration>{int(media.duration or 0)}</upnp:duration>"
-        f"<dc:queueItemId>{xmlescape(media.queue_item_id)}</dc:queueItemId>"
+        f"<dc:queueItemId>{escape_metadata(media.queue_item_id)}</dc:queueItemId>"
         f"<dc:description>Music Assistant</dc:description>"
-        f"<upnp:albumArtURI>{xmlescape(image_url)}</upnp:albumArtURI>"
+        f"<upnp:albumArtURI>{escape_metadata(image_url)}</upnp:albumArtURI>"
         "<upnp:class>object.item.audioItem.musicTrack</upnp:class>"
         f"<upnp:mimeType>audio/{ext}</upnp:mimeType>"
-        f'<res duration="{duration_str}" protocolInfo="http-get:*:audio/{ext}:DLNA.ORG_PN={ext.upper()};DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0d500000000000000000000000000000">{xmlescape(media.uri)}</res>'
+        f'<res duration="{duration_str}" protocolInfo="http-get:*:audio/{ext}:DLNA.ORG_PN={ext.upper()};DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0d500000000000000000000000000000">{escape_metadata(media.uri)}</res>'
         '<desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">RINCON_AssociatedZPUDN</desc>'
         "</item>"
         "</DIDL-Lite>"