From ed9fbf07316bf4bc50e5ecd46485b4b1b1cfa029 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sun, 9 Jun 2024 15:10:49 +0200 Subject: [PATCH] [sonos] Fix warnings (#15708) * Fix warnings * Fix discovery error logging for offline devices Fixes #6793 Signed-off-by: Jacob Laursen --- .../sonos/internal/SonosAudioSink.java | 3 +- .../sonos/internal/SonosXMLParser.java | 220 ++++++++---------- .../ZonePlayerDiscoveryParticipant.java | 2 +- .../internal/handler/ZonePlayerHandler.java | 10 +- .../sonos/internal/SonosXMLParserTest.java | 39 ++++ .../src/test/resources/MetaData.xml | 1 + 6 files changed, 149 insertions(+), 126 deletions(-) create mode 100644 bundles/org.openhab.binding.sonos/src/test/resources/MetaData.xml diff --git a/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosAudioSink.java b/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosAudioSink.java index 93146411ffe..1d9abbfc4fa 100644 --- a/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosAudioSink.java +++ b/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosAudioSink.java @@ -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 diff --git a/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosXMLParser.java b/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosXMLParser.java index a4bdef75c8b..d1939f0987b 100644 --- a/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosXMLParser.java +++ b/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosXMLParser.java @@ -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( - "" - + "" + "{2}" - + "{3}" - + "" + "{4}" - + ""); + private static final String METADATA_FORMAT_PATTERN = """ + \ + \ + {2}\ + {3}\ + {4}\ + \ + \ + """; private enum Element { TITLE, @@ -90,13 +95,11 @@ public class SonosXMLParser { public static List 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 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 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 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 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 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 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 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 }); } } diff --git a/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/discovery/ZonePlayerDiscoveryParticipant.java b/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/discovery/ZonePlayerDiscoveryParticipant.java index 2c632d9a6e6..59fd2549e07 100644 --- a/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/discovery/ZonePlayerDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/discovery/ZonePlayerDiscoveryParticipant.java @@ -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()); } } diff --git a/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/handler/ZonePlayerHandler.java b/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/handler/ZonePlayerHandler.java index 154a536cfe8..a6b68398c95 100644 --- a/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/handler/ZonePlayerHandler.java +++ b/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/handler/ZonePlayerHandler.java @@ -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())); diff --git a/bundles/org.openhab.binding.sonos/src/test/java/org/openhab/binding/sonos/internal/SonosXMLParserTest.java b/bundles/org.openhab.binding.sonos/src/test/java/org/openhab/binding/sonos/internal/SonosXMLParserTest.java index 61949785a2b..65857dd5a0a 100644 --- a/bundles/org.openhab.binding.sonos/src/test/java/org/openhab/binding/sonos/internal/SonosXMLParserTest.java +++ b/bundles/org.openhab.binding.sonos/src/test/java/org/openhab/binding/sonos/internal/SonosXMLParserTest.java @@ -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 = """ + \ + \ + Can't Buy Me Love\ + object.item.audioItem.musicTrack\ + RINCON_AssociatedZPUDN\ + \ + \ + """; + 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 index 00000000000..865dbe89eea --- /dev/null +++ b/bundles/org.openhab.binding.sonos/src/test/resources/MetaData.xml @@ -0,0 +1 @@ +x-sonosapi-hls-static:librarytrack%3ai.eoD8VQ5SZOB8QX7?sid=204&flags=8232&sn=9bd:16,sr:22050,c:3,l:0,d:0/getaa?s=1&u=x-sonosapi-hls-static%3alibrarytrack%253ai.eoD8VQ5SZOB8QX7%3fsid%3d204%26flags%3d8232%26sn%3d9Turn Down for Whatobject.item.audioItem.musicTrackDJ Snake & Lil JonTurn Down for What - Single