]> git.basschouten.com Git - openhab-addons.git/commitdiff
[sonos] Fix warnings (#15708)
authorJacob Laursen <jacob-github@vindvejr.dk>
Sun, 9 Jun 2024 13:10:49 +0000 (15:10 +0200)
committerGitHub <noreply@github.com>
Sun, 9 Jun 2024 13:10:49 +0000 (15:10 +0200)
* Fix warnings
* Fix discovery error logging for offline devices

Fixes #6793

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosAudioSink.java
bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosXMLParser.java
bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/discovery/ZonePlayerDiscoveryParticipant.java
bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/handler/ZonePlayerHandler.java
bundles/org.openhab.binding.sonos/src/test/java/org/openhab/binding/sonos/internal/SonosXMLParserTest.java
bundles/org.openhab.binding.sonos/src/test/resources/MetaData.xml [new file with mode: 0644]

index 93146411ffe6483790ec5b96b8460f37853e6b70..1d9abbfc4faf5d58f7a8f1555f5acd1febca7a86 100644 (file)
@@ -22,7 +22,6 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.sonos.internal.handler.ZonePlayerHandler;
 import org.openhab.core.audio.AudioFormat;
 import org.openhab.core.audio.AudioHTTPServer;
-import org.openhab.core.audio.AudioSink;
 import org.openhab.core.audio.AudioSinkSync;
 import org.openhab.core.audio.AudioStream;
 import org.openhab.core.audio.FileAudioStream;
@@ -39,7 +38,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * This makes a Sonos speaker to serve as an {@link AudioSink}-
+ * This makes a Sonos speaker to serve as an {@link org.openhab.core.audio.AudioSink}-
  *
  * @author Kai Kreuzer - Initial contribution and API
  * @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException
index a4bdef75c8bc8fe0c16fe7e3e096917fc80b5772..d1939f0987b9ee4b35532cbad11e9452ef715c56 100644 (file)
@@ -25,6 +25,10 @@ import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.core.util.StringUtils;
@@ -33,9 +37,7 @@ import org.slf4j.LoggerFactory;
 import org.xml.sax.Attributes;
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
-import org.xml.sax.XMLReader;
 import org.xml.sax.helpers.DefaultHandler;
-import org.xml.sax.helpers.XMLReaderFactory;
 
 /**
  * The {@link SonosXMLParser} is a class of helper functions
@@ -48,15 +50,18 @@ public class SonosXMLParser {
 
     static final Logger LOGGER = LoggerFactory.getLogger(SonosXMLParser.class);
 
-    private static final MessageFormat METADATA_FORMAT = new MessageFormat(
-            "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" "
-                    + "xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" "
-                    + "xmlns:r=\"urn:schemas-rinconnetworks-com:metadata-1-0/\" "
-                    + "xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\">"
-                    + "<item id=\"{0}\" parentID=\"{1}\" restricted=\"true\">" + "<dc:title>{2}</dc:title>"
-                    + "<upnp:class>{3}</upnp:class>"
-                    + "<desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">" + "{4}</desc>"
-                    + "</item></DIDL-Lite>");
+    private static final String METADATA_FORMAT_PATTERN = """
+            <DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" \
+            xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" \
+            xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" \
+            xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">\
+            <item id="{0}" parentID="{1}" restricted="true">\
+            <dc:title>{2}</dc:title>\
+            <upnp:class>{3}</upnp:class>\
+            <desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">{4}</desc>\
+            </item>\
+            </DIDL-Lite>\
+            """;
 
     private enum Element {
         TITLE,
@@ -90,13 +95,11 @@ public class SonosXMLParser {
     public static List<SonosAlarm> getAlarmsFromStringResult(String xml) {
         AlarmHandler handler = new AlarmHandler();
         try {
-            XMLReader reader = XMLReaderFactory.createXMLReader();
-            reader.setContentHandler(handler);
-            reader.parse(new InputSource(new StringReader(xml)));
-        } catch (IOException e) {
-            LOGGER.error("Could not parse Alarms from string '{}'", xml);
-        } catch (SAXException s) {
-            LOGGER.error("Could not parse Alarms from string '{}'", xml);
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            SAXParser saxParser = factory.newSAXParser();
+            saxParser.parse(new InputSource(new StringReader(xml)), handler);
+        } catch (IOException | SAXException | ParserConfigurationException e) {
+            LOGGER.warn("Could not parse Alarms from string '{}'", xml);
         }
         return handler.getAlarms();
     }
@@ -108,13 +111,11 @@ public class SonosXMLParser {
     public static List<SonosEntry> getEntriesFromString(String xml) {
         EntryHandler handler = new EntryHandler();
         try {
-            XMLReader reader = XMLReaderFactory.createXMLReader();
-            reader.setContentHandler(handler);
-            reader.parse(new InputSource(new StringReader(xml)));
-        } catch (IOException e) {
-            LOGGER.error("Could not parse Entries from string '{}'", xml);
-        } catch (SAXException s) {
-            LOGGER.error("Could not parse Entries from string '{}'", xml);
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            SAXParser saxParser = factory.newSAXParser();
+            saxParser.parse(new InputSource(new StringReader(xml)), handler);
+        } catch (IOException | SAXException | ParserConfigurationException e) {
+            LOGGER.warn("Could not parse Entries from string '{}'", xml);
         }
 
         return handler.getArtists();
@@ -127,18 +128,18 @@ public class SonosXMLParser {
      * @param xml
      * @return The value of the desc xml tag
      * @throws SAXException
+     * @throws ParserConfigurationException
      */
-    public static @Nullable SonosResourceMetaData getResourceMetaData(String xml) throws SAXException {
-        XMLReader reader = XMLReaderFactory.createXMLReader();
-        reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+    public static @Nullable SonosResourceMetaData getResourceMetaData(String xml)
+            throws SAXException, ParserConfigurationException {
+        SAXParserFactory factory = SAXParserFactory.newInstance();
+        factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+        SAXParser saxParser = factory.newSAXParser();
         ResourceMetaDataHandler handler = new ResourceMetaDataHandler();
-        reader.setContentHandler(handler);
         try {
-            reader.parse(new InputSource(new StringReader(xml)));
-        } catch (IOException e) {
-            LOGGER.error("Could not parse Resource MetaData from String '{}'", xml);
-        } catch (SAXException s) {
-            LOGGER.error("Could not parse Resource MetaData from string '{}'", xml);
+            saxParser.parse(new InputSource(new StringReader(xml)), handler);
+        } catch (IOException | SAXException e) {
+            LOGGER.warn("Could not parse Resource MetaData from string '{}'", xml);
         }
         return handler.getMetaData();
     }
@@ -150,14 +151,11 @@ public class SonosXMLParser {
     public static List<SonosZoneGroup> getZoneGroupFromXML(String xml) {
         ZoneGroupHandler handler = new ZoneGroupHandler();
         try {
-            XMLReader reader = XMLReaderFactory.createXMLReader();
-            reader.setContentHandler(handler);
-            reader.parse(new InputSource(new StringReader(xml)));
-        } catch (IOException e) {
-            // This should never happen - we're not performing I/O!
-            LOGGER.error("Could not parse ZoneGroup from string '{}'", xml);
-        } catch (SAXException s) {
-            LOGGER.error("Could not parse ZoneGroup from string '{}'", xml);
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            SAXParser saxParser = factory.newSAXParser();
+            saxParser.parse(new InputSource(new StringReader(xml)), handler);
+        } catch (IOException | SAXException | ParserConfigurationException e) {
+            LOGGER.warn("Could not parse ZoneGroup from string '{}'", xml);
         }
 
         return handler.getGroups();
@@ -166,14 +164,11 @@ public class SonosXMLParser {
     public static List<String> getRadioTimeFromXML(String xml) {
         OpmlHandler handler = new OpmlHandler();
         try {
-            XMLReader reader = XMLReaderFactory.createXMLReader();
-            reader.setContentHandler(handler);
-            reader.parse(new InputSource(new StringReader(xml)));
-        } catch (IOException e) {
-            // This should never happen - we're not performing I/O!
-            LOGGER.error("Could not parse RadioTime from string '{}'", xml);
-        } catch (SAXException s) {
-            LOGGER.error("Could not parse RadioTime from string '{}'", xml);
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            SAXParser saxParser = factory.newSAXParser();
+            saxParser.parse(new InputSource(new StringReader(xml)), handler);
+        } catch (IOException | SAXException | ParserConfigurationException e) {
+            LOGGER.warn("Could not parse RadioTime from string '{}'", xml);
         }
 
         return handler.getTextFields();
@@ -182,14 +177,11 @@ public class SonosXMLParser {
     public static Map<String, String> getRenderingControlFromXML(String xml) {
         RenderingControlEventHandler handler = new RenderingControlEventHandler();
         try {
-            XMLReader reader = XMLReaderFactory.createXMLReader();
-            reader.setContentHandler(handler);
-            reader.parse(new InputSource(new StringReader(xml)));
-        } catch (IOException e) {
-            // This should never happen - we're not performing I/O!
-            LOGGER.error("Could not parse Rendering Control from string '{}'", xml);
-        } catch (SAXException s) {
-            LOGGER.error("Could not parse Rendering Control from string '{}'", xml);
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            SAXParser saxParser = factory.newSAXParser();
+            saxParser.parse(new InputSource(new StringReader(xml)), handler);
+        } catch (IOException | SAXException | ParserConfigurationException e) {
+            LOGGER.warn("Could not parse Rendering Control from string '{}'", xml);
         }
         return handler.getChanges();
     }
@@ -197,14 +189,11 @@ public class SonosXMLParser {
     public static Map<String, String> getAVTransportFromXML(String xml) {
         AVTransportEventHandler handler = new AVTransportEventHandler();
         try {
-            XMLReader reader = XMLReaderFactory.createXMLReader();
-            reader.setContentHandler(handler);
-            reader.parse(new InputSource(new StringReader(xml)));
-        } catch (IOException e) {
-            // This should never happen - we're not performing I/O!
-            LOGGER.error("Could not parse AV Transport from string '{}'", xml);
-        } catch (SAXException s) {
-            LOGGER.error("Could not parse AV Transport from string '{}'", xml);
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            SAXParser saxParser = factory.newSAXParser();
+            saxParser.parse(new InputSource(new StringReader(xml)), handler);
+        } catch (IOException | SAXException | ParserConfigurationException e) {
+            LOGGER.warn("Could not parse AV Transport from string '{}'", xml);
         }
         return handler.getChanges();
     }
@@ -212,14 +201,11 @@ public class SonosXMLParser {
     public static SonosMetaData getMetaDataFromXML(String xml) {
         MetaDataHandler handler = new MetaDataHandler();
         try {
-            XMLReader reader = XMLReaderFactory.createXMLReader();
-            reader.setContentHandler(handler);
-            reader.parse(new InputSource(new StringReader(xml)));
-        } catch (IOException e) {
-            // This should never happen - we're not performing I/O!
-            LOGGER.error("Could not parse MetaData from string '{}'", xml);
-        } catch (SAXException s) {
-            LOGGER.error("Could not parse MetaData from string '{}'", xml);
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            SAXParser saxParser = factory.newSAXParser();
+            saxParser.parse(new InputSource(new StringReader(xml)), handler);
+        } catch (IOException | SAXException | ParserConfigurationException e) {
+            LOGGER.warn("Could not parse MetaData from string '{}'", xml);
         }
 
         return handler.getMetaData();
@@ -228,14 +214,11 @@ public class SonosXMLParser {
     public static List<SonosMusicService> getMusicServicesFromXML(String xml) {
         MusicServiceHandler handler = new MusicServiceHandler();
         try {
-            XMLReader reader = XMLReaderFactory.createXMLReader();
-            reader.setContentHandler(handler);
-            reader.parse(new InputSource(new StringReader(xml)));
-        } catch (IOException e) {
-            // This should never happen - we're not performing I/O!
-            LOGGER.error("Could not parse music services from string '{}'", xml);
-        } catch (SAXException s) {
-            LOGGER.error("Could not parse music services from string '{}'", xml);
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            SAXParser saxParser = factory.newSAXParser();
+            saxParser.parse(new InputSource(new StringReader(xml)), handler);
+        } catch (IOException | SAXException | ParserConfigurationException e) {
+            LOGGER.warn("Could not parse music services from string '{}'", xml);
         }
         return handler.getServices();
     }
@@ -305,14 +288,14 @@ public class SonosXMLParser {
                     if (curIgnore == null) {
                         curIgnore = new ArrayList<>();
                         curIgnore.add("DIDL-Lite");
-                        curIgnore.add("type");
-                        curIgnore.add("ordinal");
-                        curIgnore.add("description");
+                        curIgnore.add("r:type");
+                        curIgnore.add("r:ordinal");
+                        curIgnore.add("r:description");
                         ignore = curIgnore;
                     }
 
-                    if (!curIgnore.contains(localName)) {
-                        LOGGER.debug("Did not recognise element named {}", localName);
+                    if (!curIgnore.contains(qName)) {
+                        LOGGER.debug("Did not recognise element named {}", qName);
                     }
                     element = null;
                     break;
@@ -373,7 +356,7 @@ public class SonosXMLParser {
                 if (!desc.toString().isEmpty()) {
                     try {
                         md = getResourceMetaData(desc.toString());
-                    } catch (SAXException ignore) {
+                    } catch (SAXException | ParserConfigurationException ignore) {
                         LOGGER.debug("Failed to parse embeded", ignore);
                     }
                 }
@@ -715,13 +698,14 @@ public class SonosXMLParser {
              * The events are all of the form <qName val="value"/> so we can get all
              * the info we need from here.
              */
-            if (localName == null) {
-                // this means that localName isn't defined in EventType, which is expected for some elements
-                LOGGER.info("{} is not defined in EventType. ", localName);
+            if (qName == null) {
+                // this means that qName isn't defined in EventType, which is expected for some elements
+                LOGGER.info("{} is not defined in EventType. ", qName);
             } else {
                 String val = attributes == null ? null : attributes.getValue("val");
                 if (val != null) {
-                    changes.put(localName, val);
+                    String key = qName.contains(":") ? qName.split(":")[1] : qName;
+                    changes.put(key, val);
                 }
             }
         }
@@ -749,7 +733,7 @@ public class SonosXMLParser {
         @Override
         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
                 @Nullable Attributes attributes) throws SAXException {
-            String name = localName == null ? "" : localName;
+            String name = qName == null ? "" : qName;
             switch (name) {
                 case "item":
                     currentElement = CurrentElement.item;
@@ -761,25 +745,25 @@ public class SonosXMLParser {
                 case "res":
                     currentElement = CurrentElement.res;
                     break;
-                case "streamContent":
+                case "r:streamContent":
                     currentElement = CurrentElement.streamContent;
                     break;
-                case "albumArtURI":
+                case "upnp:albumArtURI":
                     currentElement = CurrentElement.albumArtURI;
                     break;
-                case "title":
+                case "dc:title":
                     currentElement = CurrentElement.title;
                     break;
-                case "class":
+                case "upnp:class":
                     currentElement = CurrentElement.upnpClass;
                     break;
-                case "creator":
+                case "dc:creator":
                     currentElement = CurrentElement.creator;
                     break;
-                case "album":
+                case "upnp:album":
                     currentElement = CurrentElement.album;
                     break;
-                case "albumArtist":
+                case "r:albumArtist":
                     currentElement = CurrentElement.albumArtist;
                     break;
                 default:
@@ -928,15 +912,16 @@ public class SonosXMLParser {
         }
     }
 
-    public static @Nullable String getRoomName(String descriptorXML) {
+    public static @Nullable String getRoomName(URL descriptorURL) {
         RoomNameHandler roomNameHandler = new RoomNameHandler();
         try {
-            XMLReader reader = XMLReaderFactory.createXMLReader();
-            reader.setContentHandler(roomNameHandler);
-            URL url = new URL(descriptorXML);
-            reader.parse(new InputSource(url.openStream()));
-        } catch (IOException | SAXException e) {
-            LOGGER.error("Could not parse Sonos room name from string '{}'", descriptorXML);
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            SAXParser saxParser = factory.newSAXParser();
+            saxParser.parse(new InputSource(descriptorURL.openStream()), roomNameHandler);
+        } catch (SAXException | ParserConfigurationException e) {
+            LOGGER.warn("Could not parse Sonos room name from URL '{}'", descriptorURL);
+        } catch (IOException e) {
+            LOGGER.debug("Could not fetch descriptor XML from URL '{}': {}", descriptorURL, e.getMessage());
         }
         return roomNameHandler.getRoomName();
     }
@@ -949,7 +934,7 @@ public class SonosXMLParser {
         @Override
         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
                 @Nullable Attributes attributes) throws SAXException {
-            if ("roomName".equalsIgnoreCase(localName)) {
+            if ("roomName".equalsIgnoreCase(qName)) {
                 roomNameTag = true;
             }
         }
@@ -970,12 +955,13 @@ public class SonosXMLParser {
     public static @Nullable String parseModelDescription(URL descriptorURL) {
         ModelNameHandler modelNameHandler = new ModelNameHandler();
         try {
-            XMLReader reader = XMLReaderFactory.createXMLReader();
-            reader.setContentHandler(modelNameHandler);
-            URL url = new URL(descriptorURL.toString());
-            reader.parse(new InputSource(url.openStream()));
-        } catch (IOException | SAXException e) {
-            LOGGER.error("Could not parse Sonos model name from string '{}'", descriptorURL.toString());
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            SAXParser saxParser = factory.newSAXParser();
+            saxParser.parse(new InputSource(descriptorURL.openStream()), modelNameHandler);
+        } catch (SAXException | ParserConfigurationException e) {
+            LOGGER.warn("Could not parse Sonos model name from URL '{}'", descriptorURL);
+        } catch (IOException e) {
+            LOGGER.debug("Could not fetch descriptor XML from URL '{}': {}", descriptorURL, e.getMessage());
         }
         return modelNameHandler.getModelName();
     }
@@ -988,7 +974,7 @@ public class SonosXMLParser {
         @Override
         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
                 @Nullable Attributes attributes) throws SAXException {
-            if ("modelName".equalsIgnoreCase(localName)) {
+            if ("modelName".equalsIgnoreCase(qName)) {
                 modelNameTag = true;
             }
         }
@@ -1079,8 +1065,6 @@ public class SonosXMLParser {
 
         title = StringUtils.escapeXml(title);
 
-        String metadata = METADATA_FORMAT.format(new Object[] { id, parentId, title, upnpClass, desc });
-
-        return metadata;
+        return new MessageFormat(METADATA_FORMAT_PATTERN).format(new Object[] { id, parentId, title, upnpClass, desc });
     }
 }
index 2c632d9a6e6f4e57a07e2dfde24919f6e5a305d2..59fd2549e07a55008c8c52fda9b4fcba60f20be2 100644 (file)
@@ -108,6 +108,6 @@ public class ZonePlayerDiscoveryParticipant implements UpnpDiscoveryParticipant
     }
 
     private @Nullable String getSonosRoomName(RemoteDevice device) {
-        return SonosXMLParser.getRoomName(device.getIdentity().getDescriptorURL().toString());
+        return SonosXMLParser.getRoomName(device.getIdentity().getDescriptorURL());
     }
 }
index 154a536cfe8b7d4ff8050f54c3c69d05fccd7399..a6b68398c95539b8b836e5b6cc049bdfb186d209 100644 (file)
@@ -1689,11 +1689,12 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
      */
     protected void saveState() {
         synchronized (stateLock) {
-            savedState = new SonosZonePlayerState();
-            String currentURI = getCurrentURI();
-
+            SonosZonePlayerState savedState = new SonosZonePlayerState();
             savedState.transportState = getTransportState();
             savedState.volume = getVolume();
+            this.savedState = savedState;
+
+            String currentURI = getCurrentURI();
 
             if (currentURI != null) {
                 if (isPlayingStreamOrRadio(currentURI)) {
@@ -2523,7 +2524,6 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
         }
     }
 
-    @SuppressWarnings("PMD.CompareObjectsWithEquals")
     public boolean publicAddress(LineInType lineInType) {
         // check if sourcePlayer has a line-in connected
         if ((lineInType != LineInType.DIGITAL && isAnalogLineInConnected())
@@ -2536,7 +2536,7 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
                 for (String player : group.getMembers()) {
                     try {
                         ZonePlayerHandler somePlayer = getHandlerByName(player);
-                        if (somePlayer != this) {
+                        if (!somePlayer.equals(this)) {
                             somePlayer.becomeStandAlonePlayer();
                             somePlayer.stop();
                             addMember(StringType.valueOf(somePlayer.getUDN()));
index 61949785a2bfc49ebfa92600f39c0b069a2a1472..65857dd5a0a7a0b1b575529b87852c4098a7e182 100644 (file)
@@ -79,4 +79,43 @@ public class SonosXMLParserTest {
             assertEquals("Paris, France", result.get(2));
         }
     }
+
+    @Test
+    public void getMetaDataFromXML() throws IOException {
+        InputStream resourceStream = getClass().getResourceAsStream("/MetaData.xml");
+        assertNotNull(resourceStream);
+        final String xml = new String(resourceStream.readAllBytes(), StandardCharsets.UTF_8);
+        SonosMetaData sonosMetaData = SonosXMLParser.getMetaDataFromXML(xml);
+        assertEquals("-1", sonosMetaData.getId());
+        assertEquals("-1", sonosMetaData.getParentId());
+        assertEquals("Turn Down for What - Single", sonosMetaData.getAlbum());
+        assertEquals("DJ Snake & Lil Jon", sonosMetaData.getCreator());
+        assertEquals("Turn Down for What", sonosMetaData.getTitle());
+        assertEquals("object.item.audioItem.musicTrack", sonosMetaData.getUpnpClass());
+        assertEquals("x-sonosapi-hls-static:librarytrack%3ai.eoD8VQ5SZOB8QX7?sid=204&flags=8232&sn=9",
+                sonosMetaData.getResource());
+        assertEquals(
+                "/getaa?s=1&u=x-sonosapi-hls-static%3alibrarytrack%253ai.eoD8VQ5SZOB8QX7%3fsid%3d204%26flags%3d8232%26sn%3d9",
+                sonosMetaData.getAlbumArtUri());
+    }
+
+    @Test
+    public void compileMetadataString() {
+        SonosEntry sonosEntry = new SonosEntry("1", "Can't Buy Me Love", "0", "A Hard Day's Night", "", "",
+                "object.item.audioItem.musicTrack", "");
+        String expected = """
+                <DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" \
+                xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" \
+                xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" \
+                xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">\
+                <item id="1" parentID="0" restricted="true">\
+                <dc:title>Can&apos;t Buy Me Love</dc:title>\
+                <upnp:class>object.item.audioItem.musicTrack</upnp:class>\
+                <desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">RINCON_AssociatedZPUDN</desc>\
+                </item>\
+                </DIDL-Lite>\
+                """;
+        String actual = SonosXMLParser.compileMetadataString(sonosEntry);
+        assertEquals(expected, actual);
+    }
 }
diff --git a/bundles/org.openhab.binding.sonos/src/test/resources/MetaData.xml b/bundles/org.openhab.binding.sonos/src/test/resources/MetaData.xml
new file mode 100644 (file)
index 0000000..865dbe8
--- /dev/null
@@ -0,0 +1 @@
+<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="-1" parentID="-1" restricted="true"><res protocolInfo="sonos.com-http:*:application/x-mpegURL:*" duration="0:03:33">x-sonosapi-hls-static:librarytrack%3ai.eoD8VQ5SZOB8QX7?sid=204&amp;flags=8232&amp;sn=9</res><r:streamContent></r:streamContent><r:radioShowMd></r:radioShowMd><r:streamInfo>bd:16,sr:22050,c:3,l:0,d:0</r:streamInfo><upnp:albumArtURI>/getaa?s=1&amp;u=x-sonosapi-hls-static%3alibrarytrack%253ai.eoD8VQ5SZOB8QX7%3fsid%3d204%26flags%3d8232%26sn%3d9</upnp:albumArtURI><dc:title>Turn Down for What</dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><dc:creator>DJ Snake &amp; Lil Jon</dc:creator><upnp:album>Turn Down for What - Single</upnp:album></item></DIDL-Lite>