]> git.basschouten.com Git - openhab-addons.git/commitdiff
[sonos] Added support of RadioApp music service (#13235)
authorlolodomo <lg.hc@free.fr>
Thu, 11 Aug 2022 06:18:56 +0000 (08:18 +0200)
committerGitHub <noreply@github.com>
Thu, 11 Aug 2022 06:18:56 +0000 (08:18 +0200)
* [sonos] Added support of RadioApp music service
* Extract artist and song title from TITLE
* Extract code in methods to reduce the size of the method updateMediaInformation
* Create new class and add tests

Fix #13208

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/handler/SonosMediaInformation.java [new file with mode: 0644]
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/SonosMediaInformationTest.java [new file with mode: 0644]

diff --git a/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/handler/SonosMediaInformation.java b/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/handler/SonosMediaInformation.java
new file mode 100644 (file)
index 0000000..264e867
--- /dev/null
@@ -0,0 +1,202 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sonos.internal.handler;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.sonos.internal.SonosMetaData;
+import org.openhab.binding.sonos.internal.SonosXMLParser;
+import org.openhab.core.io.net.http.HttpUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link SonosMediaInformation} is responsible for extracting media information from XML metadata
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class SonosMediaInformation {
+
+    private static final int HTTP_TIMEOUT = 5000;
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(SonosMediaInformation.class);
+
+    private @Nullable String artist;
+    private @Nullable String album;
+    private @Nullable String title;
+    private @Nullable String combinedInfo;
+    private boolean needsUpdate;
+
+    public SonosMediaInformation() {
+        this(false);
+    }
+
+    public SonosMediaInformation(boolean needsUpdate) {
+        this(null, null, null, null, needsUpdate);
+    }
+
+    public SonosMediaInformation(@Nullable String artist, @Nullable String album, @Nullable String title,
+            @Nullable String combinedInfo, boolean needsUpdate) {
+        this.artist = artist;
+        this.album = album;
+        this.title = title;
+        this.combinedInfo = combinedInfo;
+        this.needsUpdate = needsUpdate;
+    }
+
+    public @Nullable String getArtist() {
+        return artist;
+    }
+
+    public @Nullable String getAlbum() {
+        return album;
+    }
+
+    public @Nullable String getTitle() {
+        return title;
+    }
+
+    public @Nullable String getCombinedInfo() {
+        return combinedInfo;
+    }
+
+    public boolean needsUpdate() {
+        return needsUpdate;
+    }
+
+    public static SonosMediaInformation parseTuneInMediaInfo(@Nullable String opmlUrl, @Nullable String radioTitle,
+            @Nullable SonosMetaData trackMetaData) {
+        String title = null;
+        String combinedInfo = null;
+        if (opmlUrl != null) {
+            String response = null;
+            try {
+                response = HttpUtil.executeUrl("GET", opmlUrl, HTTP_TIMEOUT);
+            } catch (IOException e) {
+                LOGGER.debug("Request to device failed", e);
+            }
+
+            if (response != null) {
+                List<String> fields = SonosXMLParser.getRadioTimeFromXML(response);
+
+                if (!fields.isEmpty()) {
+                    combinedInfo = "";
+                    for (String field : fields) {
+                        if (combinedInfo.isEmpty()) {
+                            // radio name should be first field
+                            title = field;
+                        } else {
+                            combinedInfo += " - ";
+                        }
+                        combinedInfo += field;
+                    }
+                    return new SonosMediaInformation(null, null, title, combinedInfo, true);
+                }
+            }
+        }
+        if (radioTitle != null && !radioTitle.isEmpty()) {
+            title = radioTitle;
+            combinedInfo = title;
+            if (trackMetaData != null && !trackMetaData.getStreamContent().isEmpty()) {
+                combinedInfo += " - " + trackMetaData.getStreamContent();
+            }
+            return new SonosMediaInformation(null, null, title, combinedInfo, true);
+        }
+        return new SonosMediaInformation(false);
+    }
+
+    public static SonosMediaInformation parseRadioAppMediaInfo(@Nullable String radioTitle,
+            @Nullable SonosMetaData trackMetaData) {
+        if (radioTitle != null && !radioTitle.isEmpty()) {
+            String artist = null;
+            String album = null;
+            String title = radioTitle;
+            String combinedInfo = title;
+            if (trackMetaData != null) {
+                String[] contents = trackMetaData.getStreamContent().split("\\|");
+                String contentTitle = null;
+                for (int i = 0; i < contents.length; i++) {
+                    if (contents[i].startsWith("TITLE ")) {
+                        contentTitle = contents[i].substring(6).trim();
+                    }
+                    if (contents[i].startsWith("ARTIST ")) {
+                        artist = contents[i].substring(7).trim();
+                    }
+                    if (contents[i].startsWith("ALBUM ")) {
+                        album = contents[i].substring(6).trim();
+                    }
+                }
+                if ((artist == null || artist.isEmpty()) && contentTitle != null && !contentTitle.isEmpty()
+                        && !contentTitle.startsWith("Advertisement_")) {
+                    // Try to extract artist and song title from contentTitle
+                    int idx = contentTitle.indexOf(" - ");
+                    if (idx > 0) {
+                        artist = contentTitle.substring(0, idx);
+                        title = contentTitle.substring(idx + 3);
+                    }
+                } else if (artist != null && !artist.isEmpty() && album != null && !album.isEmpty()
+                        && contentTitle != null && !contentTitle.isEmpty()) {
+                    title = contentTitle;
+                }
+                if (artist != null && !artist.isEmpty()) {
+                    combinedInfo += " - " + artist;
+                }
+                if (album != null && !album.isEmpty()) {
+                    combinedInfo += " - " + album;
+                }
+                if (!radioTitle.equals(title)) {
+                    combinedInfo += " - " + title;
+                } else if (contentTitle != null && !contentTitle.isEmpty()
+                        && !contentTitle.startsWith("Advertisement_")) {
+                    combinedInfo += " - " + contentTitle;
+                }
+            }
+            return new SonosMediaInformation(artist, album, title, combinedInfo, true);
+        }
+        return new SonosMediaInformation(false);
+    }
+
+    public static SonosMediaInformation parseTrack(@Nullable SonosMetaData trackMetaData) {
+        if (trackMetaData != null) {
+            List<String> infos = new ArrayList<>();
+            String artist = !trackMetaData.getAlbumArtist().isEmpty() ? trackMetaData.getAlbumArtist()
+                    : trackMetaData.getCreator();
+            if (!artist.isEmpty()) {
+                infos.add(artist);
+            }
+            String album = trackMetaData.getAlbum();
+            if (!album.isEmpty()) {
+                infos.add(album);
+            }
+            String title = trackMetaData.getTitle();
+            if (!title.isEmpty()) {
+                infos.add(title);
+            }
+            return new SonosMediaInformation(artist, album, title, String.join(" - ", infos), true);
+        }
+        return new SonosMediaInformation(false);
+    }
+
+    public static SonosMediaInformation parseTrackTitle(@Nullable SonosMetaData trackMetaData) {
+        if (trackMetaData != null) {
+            String title = trackMetaData.getTitle();
+            return new SonosMediaInformation(null, null, title, title, true);
+        }
+        return new SonosMediaInformation(false);
+    }
+}
index e0784cce5b372e2d57f815164bb49f4d89b6dc97..b55b9f4d7aa70bedbae0039f37c9f67ed962c72a 100644 (file)
@@ -14,7 +14,6 @@ package org.openhab.binding.sonos.internal.handler;
 
 import static org.openhab.binding.sonos.internal.SonosBindingConstants.*;
 
-import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.text.ParseException;
@@ -92,6 +91,7 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
     private static final String STREAM_URI = "x-sonosapi-stream:";
     private static final String RADIO_URI = "x-sonosapi-radio:";
     private static final String RADIO_MP3_URI = "x-rincon-mp3radio:";
+    private static final String RADIOAPP_URI = "x-sonosapi-hls:radioapp_";
     private static final String OPML_TUNE = "http://opml.radiotime.com/Tune.ashx";
     private static final String FILE_URI = "x-file-cifs:";
     private static final String SPDIF = ":spdif";
@@ -150,8 +150,6 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
     private static final String ACTION_SET_LOUDNESS = "SetLoudness";
     private static final String ACTION_SET_EQ = "SetEQ";
 
-    private static final int SOCKET_TIMEOUT = 5000;
-
     private static final int TUNEIN_DEFAULT_SERVICE_TYPE = 65031;
 
     private static final int MIN_BASS = -10;
@@ -1228,18 +1226,14 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
         SonosMetaData currentTrack = getTrackMetadata();
         SonosMetaData currentUriMetaData = getCurrentURIMetadata();
 
-        String artist = null;
-        String album = null;
-        String title = null;
-        String resultString = null;
         String stationID = null;
-        boolean needsUpdating = false;
+        SonosMediaInformation mediaInfo = new SonosMediaInformation();
 
         // if currentURI == null, we do nothing
         if (currentURI != null) {
             if (currentURI.isEmpty()) {
                 // Reset data
-                needsUpdating = true;
+                mediaInfo = new SonosMediaInformation(true);
             }
 
             // if (currentURI.contains(GROUP_URI)) we do nothing, because
@@ -1249,76 +1243,23 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
 
             else if (isPlayingStream(currentURI) || isPlayingRadioStartedByAmazonEcho(currentURI)) {
                 // Radio stream (tune-in)
-                boolean opmlUrlSucceeded = false;
                 stationID = extractStationId(currentURI);
-                String url = opmlUrl;
-                if (url != null) {
-                    String mac = getMACAddress();
-                    if (stationID != null && !stationID.isEmpty() && mac != null && !mac.isEmpty()) {
-                        url = url.replace("%id", stationID);
-                        url = url.replace("%serial", mac);
-
-                        String response = null;
-                        try {
-                            response = HttpUtil.executeUrl("GET", url, SOCKET_TIMEOUT);
-                        } catch (IOException e) {
-                            logger.debug("Request to device failed", e);
-                        }
-
-                        if (response != null) {
-                            List<String> fields = SonosXMLParser.getRadioTimeFromXML(response);
-
-                            if (!fields.isEmpty()) {
-                                opmlUrlSucceeded = true;
-
-                                resultString = "";
-                                for (String field : fields) {
-                                    if (resultString.isEmpty()) {
-                                        // radio name should be first field
-                                        title = field;
-                                    } else {
-                                        resultString += " - ";
-                                    }
-                                    resultString += field;
-                                }
+                mediaInfo = SonosMediaInformation.parseTuneInMediaInfo(buildOpmlUrl(stationID),
+                        currentUriMetaData != null ? currentUriMetaData.getTitle() : null, currentTrack);
+            }
 
-                                needsUpdating = true;
-                            }
-                        }
-                    }
-                }
-                if (!opmlUrlSucceeded) {
-                    if (currentUriMetaData != null) {
-                        title = currentUriMetaData.getTitle();
-                        if (currentTrack == null || currentTrack.getStreamContent().isEmpty()) {
-                            resultString = title;
-                        } else {
-                            resultString = title + " - " + currentTrack.getStreamContent();
-                        }
-                        needsUpdating = true;
-                    }
-                }
+            else if (isPlayingRadioApp(currentURI)) {
+                mediaInfo = SonosMediaInformation.parseRadioAppMediaInfo(
+                        currentUriMetaData != null ? currentUriMetaData.getTitle() : null, currentTrack);
             }
 
             else if (isPlayingLineIn(currentURI)) {
-                if (currentTrack != null) {
-                    title = currentTrack.getTitle();
-                    resultString = title;
-                    needsUpdating = true;
-                }
+                mediaInfo = SonosMediaInformation.parseTrackTitle(currentTrack);
             }
 
             else if (isPlayingRadio(currentURI)
                     || (!currentURI.contains("x-rincon-mp3") && !currentURI.contains("x-sonosapi"))) {
-                // isPlayingRadio(currentURI) is true for Google Play Music radio or Apple Music radio
-                if (currentTrack != null) {
-                    artist = !currentTrack.getAlbumArtist().isEmpty() ? currentTrack.getAlbumArtist()
-                            : currentTrack.getCreator();
-                    album = currentTrack.getAlbum();
-                    title = currentTrack.getTitle();
-                    resultString = artist + " - " + album + " - " + title;
-                    needsUpdating = true;
-                }
+                mediaInfo = SonosMediaInformation.parseTrack(currentTrack);
             }
         }
 
@@ -1337,14 +1278,18 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
                     }
                     memberHandler.onValueReceived("CurrentTuneInStationId", (stationID != null) ? stationID : "",
                             SERVICE_AV_TRANSPORT);
-                    if (needsUpdating) {
+                    if (mediaInfo.needsUpdate()) {
+                        String artist = mediaInfo.getArtist();
+                        String album = mediaInfo.getAlbum();
+                        String title = mediaInfo.getTitle();
+                        String combinedInfo = mediaInfo.getCombinedInfo();
                         memberHandler.onValueReceived("CurrentArtist", (artist != null) ? artist : "",
                                 SERVICE_AV_TRANSPORT);
                         memberHandler.onValueReceived("CurrentAlbum", (album != null) ? album : "",
                                 SERVICE_AV_TRANSPORT);
                         memberHandler.onValueReceived("CurrentTitle", (title != null) ? title : "",
                                 SERVICE_AV_TRANSPORT);
-                        memberHandler.onValueReceived("CurrentURIFormatted", (resultString != null) ? resultString : "",
+                        memberHandler.onValueReceived("CurrentURIFormatted", (combinedInfo != null) ? combinedInfo : "",
                                 SERVICE_AV_TRANSPORT);
                         memberHandler.onValueReceived("CurrentAlbumArtURI", albumArtURI, SERVICE_AV_TRANSPORT);
                     }
@@ -1353,11 +1298,24 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
                 logger.debug("Cannot update media data for group member ({})", e.getMessage());
             }
         }
-        if (needsUpdating && handlerForImageUpdate != null) {
+        if (mediaInfo.needsUpdate() && handlerForImageUpdate != null) {
             handlerForImageUpdate.updateAlbumArtChannel(true);
         }
     }
 
+    private @Nullable String buildOpmlUrl(@Nullable String stationId) {
+        String url = opmlUrl;
+        if (url != null && stationId != null && !stationId.isEmpty()) {
+            String mac = getMACAddress();
+            if (mac != null && !mac.isEmpty()) {
+                url = url.replace("%id", stationId);
+                url = url.replace("%serial", mac);
+                return url;
+            }
+        }
+        return null;
+    }
+
     private @Nullable String extractStationId(String uri) {
         String stationID = null;
         if (isPlayingStream(uri)) {
@@ -1711,8 +1669,7 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
             savedState.volume = getVolume();
 
             if (currentURI != null) {
-                if (isPlayingStream(currentURI) || isPlayingRadioStartedByAmazonEcho(currentURI)
-                        || isPlayingRadio(currentURI)) {
+                if (isPlayingStreamOrRadio(currentURI)) {
                     // we are streaming music, like tune-in radio or Google Play Music radio
                     SonosMetaData track = getTrackMetadata();
                     SonosMetaData current = getCurrentURIMetadata();
@@ -2653,8 +2610,7 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
                 logger.debug("playNotificationSoundURI: currentURI {} metadata {}", currentURI,
                         coordinator.getCurrentURIMetadataAsString());
 
-                if (isPlayingStream(currentURI) || isPlayingRadioStartedByAmazonEcho(currentURI)
-                        || isPlayingRadio(currentURI)) {
+                if (isPlayingStreamOrRadio(currentURI)) {
                     handleNotifForRadioStream(currentURI, notificationURL, coordinator);
                 } else if (isPlayingLineIn(currentURI)) {
                     handleNotifForLineIn(currentURI, notificationURL, coordinator);
@@ -2692,13 +2648,24 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
     }
 
     private boolean isPlayingRadio(@Nullable String currentURI) {
+        // Google Play Music radio or Apple Music radio
         return currentURI != null && currentURI.contains(RADIO_URI);
     }
 
+    private boolean isPlayingRadioApp(@Nullable String currentURI) {
+        // RadioApp music service
+        return currentURI != null && currentURI.contains(RADIOAPP_URI);
+    }
+
     private boolean isPlayingRadioStartedByAmazonEcho(@Nullable String currentURI) {
         return currentURI != null && currentURI.contains(RADIO_MP3_URI) && currentURI.contains(OPML_TUNE);
     }
 
+    private boolean isPlayingStreamOrRadio(@Nullable String currentURI) {
+        return isPlayingStream(currentURI) || isPlayingRadioStartedByAmazonEcho(currentURI)
+                || isPlayingRadio(currentURI) || isPlayingRadioApp(currentURI);
+    }
+
     private boolean isPlayingLineIn(@Nullable String currentURI) {
         return currentURI != null && (isPlayingAnalogLineIn(currentURI) || isPlayingOpticalLineIn(currentURI));
     }
diff --git a/bundles/org.openhab.binding.sonos/src/test/java/org/openhab/binding/sonos/internal/SonosMediaInformationTest.java b/bundles/org.openhab.binding.sonos/src/test/java/org/openhab/binding/sonos/internal/SonosMediaInformationTest.java
new file mode 100644 (file)
index 0000000..a9e4868
--- /dev/null
@@ -0,0 +1,250 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sonos.internal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.sonos.internal.handler.SonosMediaInformation;
+
+/**
+ * Test for class SonosMediaInformation
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class SonosMediaInformationTest {
+
+    @Test
+    public void parseTuneInMediaInfoWithStreamContent() {
+        SonosMetaData trackMetaData = new SonosMetaData("yyy", "yyy", "yyy", "Mroning Live", "yyy", "yyy", "yyy", "yyy",
+                "yyy", "yyy");
+        SonosMediaInformation result = SonosMediaInformation.parseTuneInMediaInfo(null, "Radio One", trackMetaData);
+        assertNull(result.getArtist());
+        assertNull(result.getAlbum());
+        assertEquals("Radio One", result.getTitle());
+        assertEquals("Radio One - Mroning Live", result.getCombinedInfo());
+        assertEquals(true, result.needsUpdate());
+    }
+
+    @Test
+    public void parseTuneInMediaInfoWithoutStreamContent() {
+        SonosMetaData trackMetaData = new SonosMetaData("yyy", "yyy", "yyy", "", "yyy", "yyy", "yyy", "yyy", "yyy",
+                "yyy");
+        SonosMediaInformation result = SonosMediaInformation.parseTuneInMediaInfo(null, "Radio One", trackMetaData);
+        assertNull(result.getArtist());
+        assertNull(result.getAlbum());
+        assertEquals("Radio One", result.getTitle());
+        assertEquals("Radio One", result.getCombinedInfo());
+        assertEquals(true, result.needsUpdate());
+    }
+
+    @Test
+    public void parseTuneInMediaInfoWithoutTitle() {
+        SonosMetaData trackMetaData = new SonosMetaData("yyy", "yyy", "yyy", "Mroning Live", "yyy", "yyy", "yyy", "yyy",
+                "yyy", "yyy");
+        SonosMediaInformation result = SonosMediaInformation.parseTuneInMediaInfo(null, "", trackMetaData);
+        assertNull(result.getArtist());
+        assertNull(result.getAlbum());
+        assertNull(result.getTitle());
+        assertNull(result.getCombinedInfo());
+        assertEquals(false, result.needsUpdate());
+    }
+
+    @Test
+    public void parseTuneInMediaInfoWithNullParams() {
+        SonosMediaInformation result = SonosMediaInformation.parseTuneInMediaInfo(null, null, null);
+        assertNull(result.getArtist());
+        assertNull(result.getAlbum());
+        assertNull(result.getTitle());
+        assertNull(result.getCombinedInfo());
+        assertEquals(false, result.needsUpdate());
+    }
+
+    @Test
+    public void parseRadioAppMediaInfoWithSongTitle() {
+        SonosMetaData trackMetaData = new SonosMetaData("yyy", "yyy", "yyy",
+                "TYPE=SNG|TITLE Green Day - Time Of Your Life (Good Riddance)|ARTIST |ALBUM ", "yyy", "yyy", "yyy",
+                "yyy", "yyy", "yyy");
+        SonosMediaInformation result = SonosMediaInformation.parseRadioAppMediaInfo("Radio Two", trackMetaData);
+        assertEquals("Green Day", result.getArtist());
+        assertEquals("", result.getAlbum());
+        assertEquals("Time Of Your Life (Good Riddance)", result.getTitle());
+        assertEquals("Radio Two - Green Day - Time Of Your Life (Good Riddance)", result.getCombinedInfo());
+        assertEquals(true, result.needsUpdate());
+    }
+
+    @Test
+    public void parseRadioAppMediaInfoWithSongTitleArtistAlbum() {
+        SonosMetaData trackMetaData = new SonosMetaData("yyy", "yyy", "yyy",
+                "TYPE=SNG|TITLE Time Of Your Life (Good Riddance)|ARTIST Green Day|ALBUM Nimrod", "yyy", "yyy", "yyy",
+                "yyy", "yyy", "yyy");
+        SonosMediaInformation result = SonosMediaInformation.parseRadioAppMediaInfo("Radio Two", trackMetaData);
+        assertEquals("Green Day", result.getArtist());
+        assertEquals("Nimrod", result.getAlbum());
+        assertEquals("Time Of Your Life (Good Riddance)", result.getTitle());
+        assertEquals("Radio Two - Green Day - Nimrod - Time Of Your Life (Good Riddance)", result.getCombinedInfo());
+        assertEquals(true, result.needsUpdate());
+    }
+
+    @Test
+    public void parseRadioAppMediaInfoWithdvertisement() {
+        SonosMetaData trackMetaData = new SonosMetaData("yyy", "yyy", "yyy",
+                "TYPE=SNG|TITLE Advertisement_Stop|ARTIST |ALBUM ", "yyy", "yyy", "yyy", "yyy", "yyy", "yyy");
+        SonosMediaInformation result = SonosMediaInformation.parseRadioAppMediaInfo("Radio Two", trackMetaData);
+        assertEquals("", result.getArtist());
+        assertEquals("", result.getAlbum());
+        assertEquals("Radio Two", result.getTitle());
+        assertEquals("Radio Two", result.getCombinedInfo());
+        assertEquals(true, result.needsUpdate());
+    }
+
+    @Test
+    public void parseRadioAppMediaInfoWithoutStreamContent() {
+        SonosMetaData trackMetaData = new SonosMetaData("yyy", "yyy", "yyy", "", "yyy", "yyy", "yyy", "yyy", "yyy",
+                "yyy");
+        SonosMediaInformation result = SonosMediaInformation.parseRadioAppMediaInfo("Radio Two", trackMetaData);
+        assertNull(result.getArtist());
+        assertNull(result.getAlbum());
+        assertEquals("Radio Two", result.getTitle());
+        assertEquals("Radio Two", result.getCombinedInfo());
+        assertEquals(true, result.needsUpdate());
+    }
+
+    @Test
+    public void parseRadioAppMediaInfoWithoutTitle() {
+        SonosMetaData trackMetaData = new SonosMetaData("yyy", "yyy", "yyy",
+                "TYPE=SNG|TITLE Green Day - Time Of Your Life (Good Riddance)|ARTIST |ALBUM ", "yyy", "yyy", "yyy",
+                "yyy", "yyy", "yyy");
+        SonosMediaInformation result = SonosMediaInformation.parseRadioAppMediaInfo("", trackMetaData);
+        assertNull(result.getArtist());
+        assertNull(result.getAlbum());
+        assertNull(result.getTitle());
+        assertNull(result.getCombinedInfo());
+        assertEquals(false, result.needsUpdate());
+    }
+
+    @Test
+    public void parseRadioAppMediaInfoWithNullParams() {
+        SonosMediaInformation result = SonosMediaInformation.parseRadioAppMediaInfo(null, null);
+        assertNull(result.getArtist());
+        assertNull(result.getAlbum());
+        assertNull(result.getTitle());
+        assertNull(result.getCombinedInfo());
+        assertEquals(false, result.needsUpdate());
+    }
+
+    @Test
+    public void parseTrack() {
+        SonosMetaData trackMetaData = new SonosMetaData("xxx", "xxx", "xxx", "xxx", "xxx",
+                "Time Of Your Life (Good Riddance)", "xxx", "xxx", "Nimrod", "Green Day");
+        SonosMediaInformation result = SonosMediaInformation.parseTrack(trackMetaData);
+        assertEquals("Green Day", result.getArtist());
+        assertEquals("Nimrod", result.getAlbum());
+        assertEquals("Time Of Your Life (Good Riddance)", result.getTitle());
+        assertEquals("Green Day - Nimrod - Time Of Your Life (Good Riddance)", result.getCombinedInfo());
+        assertEquals(true, result.needsUpdate());
+    }
+
+    @Test
+    public void parseTrackWithoutArtist() {
+        SonosMetaData trackMetaData = new SonosMetaData("xxx", "xxx", "xxx", "xxx", "xxx",
+                "Time Of Your Life (Good Riddance)", "xxx", "", "Nimrod", "");
+        SonosMediaInformation result = SonosMediaInformation.parseTrack(trackMetaData);
+        assertEquals("", result.getArtist());
+        assertEquals("Nimrod", result.getAlbum());
+        assertEquals("Time Of Your Life (Good Riddance)", result.getTitle());
+        assertEquals("Nimrod - Time Of Your Life (Good Riddance)", result.getCombinedInfo());
+        assertEquals(true, result.needsUpdate());
+    }
+
+    @Test
+    public void parseTrackWithoutAlbum() {
+        SonosMetaData trackMetaData = new SonosMetaData("xxx", "xxx", "xxx", "xxx", "xxx",
+                "Time Of Your Life (Good Riddance)", "xxx", "Green Day", "", "");
+        SonosMediaInformation result = SonosMediaInformation.parseTrack(trackMetaData);
+        assertEquals("Green Day", result.getArtist());
+        assertEquals("", result.getAlbum());
+        assertEquals("Time Of Your Life (Good Riddance)", result.getTitle());
+        assertEquals("Green Day - Time Of Your Life (Good Riddance)", result.getCombinedInfo());
+        assertEquals(true, result.needsUpdate());
+    }
+
+    @Test
+    public void parseTrackWithoutTitle() {
+        SonosMetaData trackMetaData = new SonosMetaData("xxx", "xxx", "xxx", "xxx", "xxx", "", "xxx", "xxx", "Nimrod",
+                "Green Day");
+        SonosMediaInformation result = SonosMediaInformation.parseTrack(trackMetaData);
+        assertEquals("Green Day", result.getArtist());
+        assertEquals("Nimrod", result.getAlbum());
+        assertEquals("", result.getTitle());
+        assertEquals("Green Day - Nimrod", result.getCombinedInfo());
+        assertEquals(true, result.needsUpdate());
+    }
+
+    @Test
+    public void parseTrackWithOnlyTitle() {
+        SonosMetaData trackMetaData = new SonosMetaData("", "", "", "", "", "Time Of Your Life (Good Riddance)", "", "",
+                "", "");
+        SonosMediaInformation result = SonosMediaInformation.parseTrack(trackMetaData);
+        assertEquals("", result.getArtist());
+        assertEquals("", result.getAlbum());
+        assertEquals("Time Of Your Life (Good Riddance)", result.getTitle());
+        assertEquals("Time Of Your Life (Good Riddance)", result.getCombinedInfo());
+        assertEquals(true, result.needsUpdate());
+    }
+
+    @Test
+    public void parseTrackWithEmptyMetaData() {
+        SonosMetaData trackMetaData = new SonosMetaData("", "", "", "", "", "", "", "", "", "");
+        SonosMediaInformation result = SonosMediaInformation.parseTrack(trackMetaData);
+        assertEquals("", result.getArtist());
+        assertEquals("", result.getAlbum());
+        assertEquals("", result.getTitle());
+        assertEquals("", result.getCombinedInfo());
+        assertEquals(true, result.needsUpdate());
+    }
+
+    @Test
+    public void parseTrackWithNullParam() {
+        SonosMediaInformation result = SonosMediaInformation.parseTrack(null);
+        assertNull(result.getArtist());
+        assertNull(result.getAlbum());
+        assertNull(result.getTitle());
+        assertNull(result.getCombinedInfo());
+        assertEquals(false, result.needsUpdate());
+    }
+
+    @Test
+    public void parseTrackTitle() {
+        SonosMetaData trackMetaData = new SonosMetaData("xxx", "xxx", "xxx", "xxx", "xxx",
+                "Time Of Your Life (Good Riddance)", "xxx", "xxx", "Nimrod", "Green Day");
+        SonosMediaInformation result = SonosMediaInformation.parseTrackTitle(trackMetaData);
+        assertNull(result.getArtist());
+        assertNull(result.getAlbum());
+        assertEquals("Time Of Your Life (Good Riddance)", result.getTitle());
+        assertEquals("Time Of Your Life (Good Riddance)", result.getCombinedInfo());
+        assertEquals(true, result.needsUpdate());
+    }
+
+    @Test
+    public void parseTrackTitleWithNullParam() {
+        SonosMediaInformation result = SonosMediaInformation.parseTrackTitle(null);
+        assertNull(result.getArtist());
+        assertNull(result.getAlbum());
+        assertNull(result.getTitle());
+        assertNull(result.getCombinedInfo());
+        assertEquals(false, result.needsUpdate());
+    }
+}