The branch, frodo has been updated
       via  cd74c08b88c6174a4963ee6671db1ec299e88667 (commit)
      from  50a117274492d2b7e4dc4fa9156a0b482c6df508 (commit)

- Log -----------------------------------------------------------------
http://xbmc.git.sourceforge.net/git/gitweb.cgi?p=xbmc/scripts;a=commit;h=cd74c08b88c6174a4963ee6671db1ec299e88667

commit cd74c08b88c6174a4963ee6671db1ec299e88667
Author: Martijn Kaijser <[email protected]>
Date:   Tue Aug 26 21:43:11 2014 +0200

    [script.sonos] 1.1.0

diff --git a/script.sonos/addon.xml b/script.sonos/addon.xml
index 544ee44..e41030b 100644
--- a/script.sonos/addon.xml
+++ b/script.sonos/addon.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<addon id="script.sonos" name="Sonos" version="1.0.10" 
provider-name="robwebset">
+<addon id="script.sonos" name="Sonos" version="1.1.0" 
provider-name="robwebset">
        <requires>
                <import addon="xbmc.python" version="2.1.0"/>
                <import addon="script.module.requests" version="1.1.0"/>
diff --git a/script.sonos/changelog.txt b/script.sonos/changelog.txt
index feab768..e5e4a40 100644
--- a/script.sonos/changelog.txt
+++ b/script.sonos/changelog.txt
@@ -1,3 +1,10 @@
+v1.1.0
+- Prevent script error if Sonos detect fails
+- Add additional Artist Slideshow with controller screen (No Bio or Albums)
+- Add additional Artist Slideshow Only screen (no controller, bio or Albums)
+- Update images to look more like the latest ipad controls
+- Add support for viewing and playing Sonos Playlists
+
 v1.0.10
 - Highlight which speakers are group coordinators
 
diff --git a/script.sonos/default.py b/script.sonos/default.py
index b14244d..6d77582 100644
--- a/script.sonos/default.py
+++ b/script.sonos/default.py
@@ -661,7 +661,7 @@ class SonosArtistSlideshow(SonosControllerWindow):
         except:
             log("SonosArtistSlideshow: Exception Details: %s" % 
traceback.format_exc())
 
-        return SonosArtistSlideshow("script-sonos-artist-slideshow.xml", 
__cwd__, sonosDevice=sonosDevice)
+        return SonosArtistSlideshow(Settings.getArtistInfoLayout(), __cwd__, 
sonosDevice=sonosDevice)
 
     # Launch ArtistSlideshow
     def runArtistSlideshow(self):
diff --git a/script.sonos/discovery.py b/script.sonos/discovery.py
index ffc6ec8..cdf10e8 100644
--- a/script.sonos/discovery.py
+++ b/script.sonos/discovery.py
@@ -48,42 +48,43 @@ if __name__ == '__main__':
 
     speakers = {}
 
-    for device in sonos_devices:
-        ip = device.ip_address
-        log("SonosDiscovery: Getting info for IP address %s" % ip)
-
-        playerInfo = None
-
-        # Try and get the player info, if it fails then it is not a valid
-        # player and we should continue to the next
-        try:
-            playerInfo = device.get_speaker_info()
-        except:
-            log("SonosDiscovery: IP address %s is not a valid player" % ip)
-            log("SonosDiscovery: %s" % traceback.format_exc())
-            continue
-
-        # If player  info was found, then print it out
-        if playerInfo is not None:
-            # What is the name of the zone that this speaker is in?
-            zone_name = playerInfo['zone_name']
-            displayName = ip
-            if (zone_name is not None) and (zone_name != ""):
-                log("SonosDiscovery: Zone of %s is \"%s\"" % (ip, zone_name))
-                displayName = "%s     [%s]" % (ip, zone_name)
-            else:
-                log("SonosDiscovery: No zone for IP address %s" % ip)
-            # Record if this is the group coordinator, as when there are 
several
-            # speakers in the group, we need to send messages to the group
-            # coordinator for things to work correctly
-            isCoordinator = device.is_coordinator
-            if isCoordinator:
-                log("SonosDiscovery: %s is the group coordinator" % ip)
-                displayName = "%s - %s" % (displayName, 
__addon__.getLocalizedString(32031))
-            else:
-                log("SonosDiscovery: %s is not the group coordinator" % ip)
-
-            speakers[displayName] = (ip, zone_name, isCoordinator)
+    if sonos_devices is not None:
+        for device in sonos_devices:
+            ip = device.ip_address
+            log("SonosDiscovery: Getting info for IP address %s" % ip)
+
+            playerInfo = None
+
+            # Try and get the player info, if it fails then it is not a valid
+            # player and we should continue to the next
+            try:
+                playerInfo = device.get_speaker_info()
+            except:
+                log("SonosDiscovery: IP address %s is not a valid player" % ip)
+                log("SonosDiscovery: %s" % traceback.format_exc())
+                continue
+
+            # If player  info was found, then print it out
+            if playerInfo is not None:
+                # What is the name of the zone that this speaker is in?
+                zone_name = playerInfo['zone_name']
+                displayName = ip
+                if (zone_name is not None) and (zone_name != ""):
+                    log("SonosDiscovery: Zone of %s is \"%s\"" % (ip, 
zone_name))
+                    displayName = "%s     [%s]" % (ip, zone_name)
+                else:
+                    log("SonosDiscovery: No zone for IP address %s" % ip)
+                # Record if this is the group coordinator, as when there are 
several
+                # speakers in the group, we need to send messages to the group
+                # coordinator for things to work correctly
+                isCoordinator = device.is_coordinator
+                if isCoordinator:
+                    log("SonosDiscovery: %s is the group coordinator" % ip)
+                    displayName = "%s - %s" % (displayName, 
__addon__.getLocalizedString(32031))
+                else:
+                    log("SonosDiscovery: %s is not the group coordinator" % ip)
+
+                speakers[displayName] = (ip, zone_name, isCoordinator)
 
     # Remove the busy dialog
     xbmc.executebuiltin("Dialog.Close(busydialog)")
diff --git a/script.sonos/plugin.py b/script.sonos/plugin.py
index 47229d3..225986b 100644
--- a/script.sonos/plugin.py
+++ b/script.sonos/plugin.py
@@ -1,6 +1,4 @@
 # -*- coding: utf-8 -*-
-# Reference:
-# http://wiki.xbmc.org/index.php?title=Audio/Video_plugin_tutorial
 import sys
 import os
 import traceback
@@ -33,9 +31,17 @@ import soco
 # Media files used by the plugin
 ###################################################################
 class MediaFiles():
-    RadioIcon = os.path.join(__media__, '[email protected]')
-    MusicLibraryIcon = os.path.join(__media__, '[email protected]')
-    QueueIcon = os.path.join(__media__, '[email protected]')
+    RadioIcon = 'DefaultAudio.png' if Settings.useSkinIcons() else 
os.path.join(__media__, 'radio.png')
+    MusicLibraryIcon = 'DefaultAudio.png' if Settings.useSkinIcons() else 
os.path.join(__media__, 'library.png')
+    QueueIcon = 'DefaultMusicPlaylists.png' if Settings.useSkinIcons() else 
os.path.join(__media__, 'playlist.png')
+
+    AlbumsIcon = 'DefaultMusicAlbums.png' if Settings.useSkinIcons() else 
os.path.join(__media__, 'albums.png')
+    ArtistsIcon = 'DefaultMusicArtists.png' if Settings.useSkinIcons() else 
os.path.join(__media__, 'artists.png')
+    ComposersIcon = 'DefaultArtist.png' if Settings.useSkinIcons() else 
os.path.join(__media__, 'composers.png')
+    GenresIcon = 'DefaultMusicGenres.png' if Settings.useSkinIcons() else 
os.path.join(__media__, 'genres.png')
+    TracksIcon = 'DefaultMusicSongs.png' if Settings.useSkinIcons() else 
os.path.join(__media__, 'tracks.png')
+    RadioStationIcon = 'DefaultAudio.png' if Settings.useSkinIcons() else 
os.path.join(__media__, 'radiostation.png')
+    SonosPlaylistIcon = 'DefaultMusicPlaylists.png' if Settings.useSkinIcons() 
else os.path.join(__media__, 'sonosplaylist.png')
 
 
 ###################################################################
@@ -50,12 +56,15 @@ class MenuNavigator():
     COMPOSERS = 'composers'
     TRACKS = 'tracks'
     FOLDERS = 'folders'
+    SONOS_PLAYLISTS = 'sonos_playlists'
 
     # Menu items manually set at the root
     ROOT_MENU_MUSIC_LIBRARY = 'Music-Library'
     ROOT_MENU_QUEUE = 'QueueIcon'
     ROOT_MENU_RADIO_STATIONS = 'Radio-Stations'
     ROOT_MENU_RADIO_SHOWS = 'Radio-Shows'
+    ROOT_MENU_RADIO_SHOWS = 'Radio-Shows'
+    ROOT_MENU_SONOS_PLAYLISTS = 'Sonos-Playlists'
 
     def __init__(self, base_url, addon_handle):
         self.base_url = base_url
@@ -101,19 +110,12 @@ class MenuNavigator():
 #        self._addPlayerToContextMenu(li) # Add the Sonos player to the menu
 #        xbmcplugin.addDirectoryItem(handle=self.addon_handle, url=url, 
listitem=li, isFolder=True)
 
-#        url = self._build_url({'mode': 'folder', 'foldername': 
'Sonos-Playlists'})
-#        li = xbmcgui.ListItem('Sonos Playlists (Not Supported Yet)', 
iconImage='DefaultFolder.png')
-#        li.addContextMenuItems([], replaceItems=True) # Clear the Context Menu
-#        self._addPlayerToContextMenu(li) # Add the Sonos player to the menu
-#        xbmcplugin.addDirectoryItem(handle=self.addon_handle, url=url, 
listitem=li, isFolder=True)
-
-#        url = self._build_url({'mode': 'folder', 'foldername': 'Line-In'})
-#        li = xbmcgui.ListItem('Line-In (Not Supported Yet)', 
iconImage='DefaultFolder.png')
-#        li.addContextMenuItems([], replaceItems=True) # Clear the Context Menu
-#        self._addPlayerToContextMenu(li) # Add the Sonos player to the menu
-#        xbmcplugin.addDirectoryItem(handle=self.addon_handle, url=url, 
listitem=li, isFolder=True)
+        url = self._build_url({'mode': 'folder', 'foldername': 
MenuNavigator.SONOS_PLAYLISTS})
+        li = xbmcgui.ListItem(__addon__.getLocalizedString(32104), 
iconImage=MediaFiles.QueueIcon)
+        li.addContextMenuItems([], replaceItems=True)  # Clear the Context Menu
+        self._addPlayerToContextMenu(li)  # Add the Sonos player to the menu
+        xbmcplugin.addDirectoryItem(handle=self.addon_handle, url=url, 
listitem=li, isFolder=True)
 
-        # QueueIcon
         url = self._build_url({'mode': 'folder', 'foldername': 
MenuNavigator.ROOT_MENU_QUEUE})
         li = xbmcgui.ListItem(__addon__.getLocalizedString(32101), 
iconImage=MediaFiles.QueueIcon)
         li.addContextMenuItems([], replaceItems=True)  # Clear the Context Menu
@@ -127,35 +129,35 @@ class MenuNavigator():
         # Artists
         # Note: For artists, the sonos system actually calls "Album Artists"
         url = self._build_url({'mode': 'folder', 'foldername': 
MenuNavigator.ALBUMARTISTS})
-        li = xbmcgui.ListItem(__addon__.getLocalizedString(32110), 
iconImage='DefaultMusicArtists.png')
+        li = xbmcgui.ListItem(__addon__.getLocalizedString(32110), 
iconImage=MediaFiles.ArtistsIcon)
         li.addContextMenuItems([], replaceItems=True)  # Clear the Context Menu
         self._addPlayerToContextMenu(li)  # Add the Sonos player to the menu
         xbmcplugin.addDirectoryItem(handle=self.addon_handle, url=url, 
listitem=li, isFolder=True)
 
         # Albums
         url = self._build_url({'mode': 'folder', 'foldername': 
MenuNavigator.ALBUMS})
-        li = xbmcgui.ListItem(__addon__.getLocalizedString(32111), 
iconImage='DefaultMusicAlbums.png')
+        li = xbmcgui.ListItem(__addon__.getLocalizedString(32111), 
iconImage=MediaFiles.AlbumsIcon)
         li.addContextMenuItems([], replaceItems=True)  # Clear the Context Menu
         self._addPlayerToContextMenu(li)  # Add the Sonos player to the menu
         xbmcplugin.addDirectoryItem(handle=self.addon_handle, url=url, 
listitem=li, isFolder=True)
 
         # Composers
         url = self._build_url({'mode': 'folder', 'foldername': 
MenuNavigator.COMPOSERS})
-        li = xbmcgui.ListItem(__addon__.getLocalizedString(32112), 
iconImage='DefaultArtist.png')
+        li = xbmcgui.ListItem(__addon__.getLocalizedString(32112), 
iconImage=MediaFiles.ComposersIcon)
         li.addContextMenuItems([], replaceItems=True)  # Clear the Context Menu
         self._addPlayerToContextMenu(li)  # Add the Sonos player to the menu
         xbmcplugin.addDirectoryItem(handle=self.addon_handle, url=url, 
listitem=li, isFolder=True)
 
         # Genres
         url = self._build_url({'mode': 'folder', 'foldername': 
MenuNavigator.GENRES})
-        li = xbmcgui.ListItem(__addon__.getLocalizedString(32113), 
iconImage='DefaultMusicGenres.png')
+        li = xbmcgui.ListItem(__addon__.getLocalizedString(32113), 
iconImage=MediaFiles.GenresIcon)
         li.addContextMenuItems([], replaceItems=True)  # Clear the Context Menu
         self._addPlayerToContextMenu(li)  # Add the Sonos player to the menu
         xbmcplugin.addDirectoryItem(handle=self.addon_handle, url=url, 
listitem=li, isFolder=True)
 
         # Tracks
         url = self._build_url({'mode': 'folder', 'foldername': 
MenuNavigator.TRACKS})
-        li = xbmcgui.ListItem(__addon__.getLocalizedString(32114), 
iconImage='DefaultMusicSongs.png')
+        li = xbmcgui.ListItem(__addon__.getLocalizedString(32114), 
iconImage=MediaFiles.TracksIcon)
         li.addContextMenuItems([], replaceItems=True)  # Clear the Context Menu
         self._addPlayerToContextMenu(li)  # Add the Sonos player to the menu
         xbmcplugin.addDirectoryItem(handle=self.addon_handle, url=url, 
listitem=li, isFolder=True)
@@ -201,7 +203,7 @@ class MenuNavigator():
                     if hasattr(item, 'album_art_uri') and (item.album_art_uri 
is not None) and (item.album_art_uri != ""):
                         li = xbmcgui.ListItem(displayTitle, 
iconImage=item.album_art_uri, thumbnailImage=item.album_art_uri)
                     else:
-                        li = xbmcgui.ListItem(displayTitle, 
iconImage='DefaultMusicSongs.png')
+                        li = xbmcgui.ListItem(displayTitle, 
iconImage=MediaFiles.TracksIcon)
                     # Set addition information about the track - will be seen 
in info view
                     li.setInfo('music', {'title': item.title, 'artist': 
item.creator, 'album': item.album})
 
@@ -272,7 +274,13 @@ class MenuNavigator():
                             isFirstItem = False
                             continue
 
-                        self._addDirectory(item, folderName, totalEntries, 
subCategory)
+                        # Check to see if we are dealing with a sonos playlist
+                        if isinstance(item, 
soco.data_structures.MLSonosPlaylist):
+                            # Will need to do the search by ID for playlists 
as the text method
+                            # does not work
+                            self._addDirectory(item, folderName, totalEntries, 
subCategory, item.item_id)
+                        else:
+                            self._addDirectory(item, folderName, totalEntries, 
subCategory)
                     # No longer the first item
                     isFirstItem = False  # noqa PEP8
 
@@ -318,7 +326,7 @@ class MenuNavigator():
                     # Add the radio station to the list
                     url = self._build_url({'mode': 'action', 'action': 
ActionManager.ACTION_RADIO_PLAY, 'itemId': item['uri'], 'title': item['title']})
 
-                    li = xbmcgui.ListItem(item['title'], path=url, 
iconImage='DefaultMusicSongs.png')
+                    li = xbmcgui.ListItem(item['title'], path=url, 
iconImage=MediaFiles.RadioStationIcon)
                     # Set the right click context menu for the ratio station
                     li.addContextMenuItems([], replaceItems=True)  # Clear the 
Context Menu
                     self._addPlayerToContextMenu(li)
@@ -367,7 +375,7 @@ class MenuNavigator():
                     # Add the radio station to the list
                     url = self._build_url({'mode': 'action', 'action': 
ActionManager.ACTION_RADIO_PLAY, 'itemId': item['uri'], 'title': item['title']})
 
-                    li = xbmcgui.ListItem(item['title'], path=url, 
iconImage='DefaultMusicSongs.png')
+                    li = xbmcgui.ListItem(item['title'], path=url, 
iconImage=MediaFiles.RadioStationIcon)
                     # Set the right click context menu for the ratio station
                     li.addContextMenuItems([], replaceItems=True)  # Clear the 
Context Menu
                     self._addPlayerToContextMenu(li)
@@ -388,13 +396,16 @@ class MenuNavigator():
         return currentEntries >= queueLimit
 
     # Adds a sub-directory to the display
-    def _addDirectory(self, item, folderName, totalEntries=None, 
subCategory=None):
+    def _addDirectory(self, item, folderName, totalEntries=None, 
subCategory=None, item_id=None):
         if (item is not None) and (folderName is not None):
             # Escape special characters from the title
             # Useful site: http://www.ascii.cl/htmlcodes.htm
             title = item.title.replace('/', "%2F").replace(':', "%3A")
+            # If the item ID is set we use that instead of the subcatagory name
+            if item_id is not None:
+                subCategory = str(item_id)
             # Update the category
-            if subCategory is not None:
+            elif subCategory is not None:
                 log("SonosPlugin: Adding to existing category %s" % 
subCategory)
                 subCategory += '/' + title.encode("utf-8")
             else:
@@ -418,15 +429,17 @@ class MenuNavigator():
                 li = xbmcgui.ListItem(displayTitle, 
iconImage=item.album_art_uri, thumbnailImage=item.album_art_uri)
             else:
                 # Use one of the default icons
-                defaultIcon = 'DefaultAudio.png'
-                if folderName == MenuNavigator.ARTISTS:
-                    defaultIcon = 'DefaultMusicArtists.png'
-                elif (folderName == MenuNavigator.ALBUMS) or (folderName == 
MenuNavigator.ALBUMARTISTS):
-                    defaultIcon = 'DefaultMusicAlbums.png'
+                defaultIcon = MediaFiles.TracksIcon
+                if (folderName == MenuNavigator.ARTISTS) or (folderName == 
MenuNavigator.ALBUMARTISTS):
+                    defaultIcon = MediaFiles.ArtistsIcon
+                elif folderName == MenuNavigator.ALBUMS:
+                    defaultIcon = MediaFiles.AlbumsIcon
                 elif folderName == MenuNavigator.GENRES:
-                    defaultIcon = 'DefaultMusicGenres.png'
+                    defaultIcon = MediaFiles.GenresIcon
                 elif folderName == MenuNavigator.COMPOSERS:
-                    defaultIcon = 'DefaultArtist.png'
+                    defaultIcon = MediaFiles.ComposersIcon
+                elif folderName == MenuNavigator.SONOS_PLAYLISTS:
+                    defaultIcon = MediaFiles.SonosPlaylistIcon
 
                 li = xbmcgui.ListItem(displayTitle, iconImage=defaultIcon)
 
@@ -449,7 +462,7 @@ class MenuNavigator():
                 # Get the display title, adding the track number if available
                 if (item.original_track_number is not None) and 
(item.original_track_number != ""):
                     displayTitle = "%02d. %s" % (item.original_track_number, 
item.title)
-            elif folderName == MenuNavigator.TRACKS:
+            elif (folderName == MenuNavigator.TRACKS) or (folderName == 
MenuNavigator.SONOS_PLAYLISTS):
                 if (item.creator is not None) and (item.creator != ""):
                     displayTitle = "%s - %s" % (item.title, item.creator)
 
@@ -462,7 +475,7 @@ class MenuNavigator():
             if hasattr(item, 'album_art_uri') and (item.album_art_uri is not 
None) and (item.album_art_uri != ""):
                 li = xbmcgui.ListItem(displayTitle, 
iconImage=item.album_art_uri, thumbnailImage=item.album_art_uri, path=url)
             else:
-                li = xbmcgui.ListItem(displayTitle, path=url, 
iconImage='DefaultMusicSongs.png')
+                li = xbmcgui.ListItem(displayTitle, path=url, 
iconImage=MediaFiles.TracksIcon)
             # Set addition information about the track - will be seen in info 
view
             li.setInfo('music', {'tracknumber': item.original_track_number, 
'title': item.title, 'artist': item.creator, 'album': item.album})
             # li.setProperty("IsPlayable","true");
diff --git a/script.sonos/resources/language/English/strings.po 
b/script.sonos/resources/language/English/strings.po
index 345063c..330da46 100644
--- a/script.sonos/resources/language/English/strings.po
+++ b/script.sonos/resources/language/English/strings.po
@@ -156,7 +156,35 @@ msgctxt "#32033"
 msgid "Some functions may not work correctly if the speaker is not a 
coordinator"
 msgstr ""
 
-#empty strings from id 32034 to 32059
+msgctxt "#32034"
+msgid "Automation"
+msgstr ""
+
+msgctxt "#32035"
+msgid "Layout"
+msgstr ""
+
+msgctxt "#32036"
+msgid "Controller,Slideshow,Bio,Albums"
+msgstr ""
+
+msgctxt "#32037"
+msgid "Controller,Slideshow"
+msgstr ""
+
+msgctxt "#32038"
+msgid "Use Skin Icons Instead Of Sonos Icons"
+msgstr ""
+
+msgctxt "#32039"
+msgid "Slideshow Only"
+msgstr ""
+
+msgctxt "#32040"
+msgid "Do Not Display Notification If Controller Is Showing"
+msgstr ""
+
+#empty strings from id 32041 to 32059
 
 msgctxt "#32060"
 msgid "The following setting in ArtistSlideshow must be enabled"
@@ -188,7 +216,11 @@ msgctxt "#32103"
 msgid "Launch Sonos Controller"
 msgstr ""
 
-#empty strings from id 32104 to 32109
+msgctxt "#32104"
+msgid "Sonos Playlists"
+msgstr ""
+
+#empty strings from id 32105 to 32109
 
 msgctxt "#32110"
 msgid "Artists"
diff --git a/script.sonos/resources/lib/settings.py 
b/script.sonos/resources/lib/settings.py
index 17a5d0d..ff8e6ff 100644
--- a/script.sonos/resources/lib/settings.py
+++ b/script.sonos/resources/lib/settings.py
@@ -114,6 +114,10 @@ class Settings():
         return __addon__.getSetting("notifNotIfVideoPlaying") == 'true'
 
     @staticmethod
+    def stopNotifIfControllerShowing():
+        return __addon__.getSetting("notifNotIfControllerShowing") == 'true'
+
+    @staticmethod
     def useXbmcNotifDialog():
         return __addon__.getSetting("xbmcNotifDialog") == 'true'
 
@@ -142,10 +146,21 @@ class Settings():
         return int(float(__addon__.getSetting("maxListEntries")))
 
     @staticmethod
+    def useSkinIcons():
+        return __addon__.getSetting("useSkinIcons") == 'true'
+
+    @staticmethod
     def displayArtistInfo():
         return __addon__.getSetting("displayArtistInfo") == 'true'
 
     @staticmethod
+    def getArtistInfoLayout():
+        layoutId = int(float(__addon__.getSetting("artistInfoLayout")))
+        # Settings are indexed at zero, so add one to match the Window XML
+        layoutId = layoutId + 1
+        return "script-sonos-artist-slideshow-%s.xml" % layoutId
+
+    @staticmethod
     def linkAudioWithSonos():
         return __addon__.getSetting("linkAudioWithSonos") == 'true'
 
diff --git a/script.sonos/resources/lib/soco/__init__.py 
b/script.sonos/resources/lib/soco/__init__.py
index 73d95b1..e7c55e4 100644
--- a/script.sonos/resources/lib/soco/__init__.py
+++ b/script.sonos/resources/lib/soco/__init__.py
@@ -9,7 +9,7 @@
 
 # Will be parsed by setup.py to determine package metadata
 __author__ = 'The SoCo-Team <[email protected]>'
-__version__ = '0.8'
+__version__ = '0.8.1'
 __website__ = 'https://github.com/SoCo/SoCo'
 __license__ = 'MIT License'
 
diff --git a/script.sonos/resources/lib/soco/compat.py 
b/script.sonos/resources/lib/soco/compat.py
index b1f204a..fb7ad2e 100644
--- a/script.sonos/resources/lib/soco/compat.py
+++ b/script.sonos/resources/lib/soco/compat.py
@@ -22,7 +22,7 @@ except ImportError:  # python 2.7
     from Queue import Queue  # nopep8
     from types import StringType, UnicodeType  # nopep8
 
-try:  # python 2.7 - this has to be done the other way rund
+try:  # python 2.7 - this has to be done the other way round
     from cPickle import dumps  # nopep8
 except ImportError:  # python 3
     from pickle import dumps  # nopep8
diff --git a/script.sonos/resources/lib/soco/core.py 
b/script.sonos/resources/lib/soco/core.py
index 688cb9d..ddfce37 100644
--- a/script.sonos/resources/lib/soco/core.py
+++ b/script.sonos/resources/lib/soco/core.py
@@ -16,11 +16,14 @@ import requests
 
 from .services import DeviceProperties, ContentDirectory
 from .services import RenderingControl, AVTransport, ZoneGroupTopology
+from .services import AlarmClock
 from .groups import ZoneGroup
 from .exceptions import CannotCreateDIDLMetadata
-from .data_structures import get_ml_item, QueueItem, URI
+from .data_structures import get_ml_item, QueueItem, URI, MLSonosPlaylist,\
+    MLShare
 from .utils import really_utf8, camel_to_underscore
 from .xml import XML
+from soco import config
 
 LOGGER = logging.getLogger(__name__)
 
@@ -65,7 +68,7 @@ def discover(timeout=1, include_invisible=False):
         # for the topology to find the other players. It is much more efficient
         # to rely upon the Zone Player's ability to find the others, than to
         # wait for query responses from them ourselves.
-        zone = SoCo(addr[0])
+        zone = config.SOCO_CLASS(addr[0])
         if include_invisible:
             return zone.all_zones
         else:
@@ -97,10 +100,12 @@ class SonosDiscovery(object):  # pylint: disable=R0903
 
 class _ArgsSingleton(type):
     """ A metaclass which permits only a single instance of each derived class
-    to exist for any given set of positional arguments.
+    sharing the same `_class_group` class attribute to exist for any given set
+    of positional arguments.
 
-    Attempts to instantiate a second instance of a derived class will return
-    the existing instance.
+    Attempts to instantiate a second instance of a derived class, or another
+    class with the same `_class_group`, with the same args will return the
+    existing instance.
 
     For example:
 
@@ -108,23 +113,30 @@ class _ArgsSingleton(type):
     ...     __metaclass__ = _ArgsSingleton
     ...
     >>> class First(ArgsSingletonBase):
+    ...     _class_group = "greeting"
     ...     def __init__(self, param):
     ...         pass
     ...
+    >>> class Second(ArgsSingletonBase):
+    ...     _class_group = "greeting"
+    ...     def __init__(self, param):
+    ...         pass
     >>> assert First('hi') is First('hi')
     >>> assert First('hi') is First('bye')
     AssertionError
+    >>> assert First('hi') is Second('hi')
 
      """
     _instances = {}
 
     def __call__(cls, *args, **kwargs):
-        if cls not in cls._instances:
-            cls._instances[cls] = {}
-        if args not in cls._instances[cls]:
-            cls._instances[cls][args] = super(_ArgsSingleton, cls).__call__(
+        key = cls._class_group if hasattr(cls, '_class_group') else cls
+        if key not in cls._instances:
+            cls._instances[key] = {}
+        if args not in cls._instances[key]:
+            cls._instances[key][args] = super(_ArgsSingleton, cls).__call__(
                 *args, **kwargs)
-        return cls._instances[cls][args]
+        return cls._instances[key][args]
 
 
 class _SocoSingletonBase(  # pylint: disable=too-few-public-methods
@@ -206,6 +218,8 @@ class SoCo(_SocoSingletonBase):
 
     """
 
+    _class_group = 'SoCo'
+
     def __init__(self, ip_address):
         # Note: Creation of a SoCo instance should be as cheap and quick as
         # possible. Do not make any network calls here
@@ -227,6 +241,7 @@ class SoCo(_SocoSingletonBase):
         self.deviceProperties = DeviceProperties(self)
         self.renderingControl = RenderingControl(self)
         self.zoneGroupTopology = ZoneGroupTopology(self)
+        self.alarmClock = AlarmClock(self)
 
         # Some private attributes
         self._all_zones = set()
@@ -239,7 +254,8 @@ class SoCo(_SocoSingletonBase):
         self._zgs_cache = None
 
     def __str__(self):
-        return "<SoCo object at ip {0}>".format(self.ip_address)
+        return "<{0} object at ip {1}>".format(
+            self.__class__.__name__, self.ip_address)
 
     def __repr__(self):
         return '{0}("{1}")'.format(self.__class__.__name__, self.ip_address)
@@ -258,7 +274,7 @@ class SoCo(_SocoSingletonBase):
     @player_name.setter
     def player_name(self, playername):
         """ Set the speaker's name """
-        self.deviceProperties.SetZoneAtrributes([
+        self.deviceProperties.SetZoneAttributes([
             ('DesiredZoneName', playername),
             ('DesiredIcon', ''),
             ('DesiredConfiguration', '')
@@ -347,10 +363,9 @@ class SoCo(_SocoSingletonBase):
     @play_mode.setter
     def play_mode(self, playmode):
         """ Set the speaker's mode """
-        modes = ('NORMAL', 'SHUFFLE_NOREPEAT', 'SHUFFLE', 'REPEAT_ALL')
         playmode = playmode.upper()
-        if playmode not in modes:
-            raise KeyError('invalid play mode')
+        if playmode not in PLAY_MODES:
+            raise KeyError("'%s' is not a valid play mode" % playmode)
 
         self.avTransport.SetPlayMode([
             ('InstanceID', 0),
@@ -730,17 +745,14 @@ class SoCo(_SocoSingletonBase):
                 member_attribs = member_element.attrib
                 ip_addr = member_attribs['Location'].\
                     split('//')[1].split(':')[0]
-                zone = SoCo(ip_addr)
+                zone = config.SOCO_CLASS(ip_addr)
                 zone._uid = member_attribs['UUID']
                 # If this element has the same UUID as the coordinator, it is
                 # the coordinator
+                group_coordinator = None
                 if zone._uid == coordinator_uid:
                     group_coordinator = zone
                     zone._is_coordinator = True
-                    # If this is the coordinator, and the same IP address
-                    # then set it as the default UID
-                    if ip_addr == self.ip_address:
-                        self._uid = zone._uid
                 else:
                     zone._is_coordinator = False
                 zone._player_name = member_attribs['ZoneName']
@@ -1193,7 +1205,7 @@ class SoCo(_SocoSingletonBase):
         return out
 
     def get_music_library_information(self, search_type, start=0,
-                                      max_items=100, sub_category=''):
+                                      max_items=100):
         """ Retrieve information about the music library
 
         :param search_type: The kind of information to retrieve. Can be one of:
@@ -1206,9 +1218,6 @@ class SoCo(_SocoSingletonBase):
             may be restricted by the unit, presumably due to transfer
             size consideration, so check the returned number against the
             requested.
-        :param sub_category: Sub category to allow you to refine your search
-            under the given search type, allowing you to look for things like
-            all the artists under a given genre (e.g. A:GENRE/Pop)
         :returns: A dictionary with metadata for the search, with the
             keys 'number_returned', 'update_id', 'total_matches' and an
             'item_list' list with the search results. The search results
@@ -1237,18 +1246,97 @@ class SoCo(_SocoSingletonBase):
         """
         search_translation = {'artists': 'A:ARTIST',
                               'album_artists': 'A:ALBUMARTIST',
-                              'albums': 'A:ALBUM', 'genres': 'A:GENRE',
-                              'composers': 'A:COMPOSER', 'tracks': 'A:TRACKS',
-                              'playlists': 'A:PLAYLISTS', 'share': 'S:',
-                              'sonos_playlists': 'SQ:'}
+                              'albums': 'A:ALBUM',
+                              'genres': 'A:GENRE',
+                              'composers': 'A:COMPOSER',
+                              'tracks': 'A:TRACKS',
+                              'playlists': 'A:PLAYLISTS',
+                              'share': 'S:',
+                              'sonos_playlists': 'SQ:',
+                              'categories': 'A:'}
         search = search_translation[search_type]
+        response, out = self._music_lib_search(search, start, max_items)
+        out['search_type'] = search_type
+        out['item_list'] = []
+
+        # Parse the results
+        dom = XML.fromstring(really_utf8(response['Result']))
+        for container in dom:
+            if search_type == 'sonos_playlists':
+                item = MLSonosPlaylist.from_xml(container)
+            elif search_type == 'share':
+                item = MLShare.from_xml(container)
+            else:
+                item = get_ml_item(container)
+            # Append the item to the list
+            out['item_list'].append(item)
+
+        return out
+
+    def browse(self, ml_item=None, start=0, max_items=100):
+        """Browse (get sub-elements) a music library item
+
+        Keyword arguments:
+            ml_item (MusicLibraryItem): The MusicLibraryItem to browse, if left
+                out or passed None, the items at the base level will be
+                returned
+            start (int): The starting index of the results
+            max_items (int): The maximum number of items to return
+
+        Returns:
+            dict: A dictionary with metadata for the search, with the
+                keys 'number_returned', 'update_id', 'total_matches' and an
+                'item_list' list with the search results.
+
+        Raises:
+            AttributeError: If ``ml_item`` has no ``item_id`` attribute
+            SoCoUPnPException: With ``error_code='701'`` if the item cannot be
+                browsed
+        """
+        if ml_item is None:
+            search = 'A:'
+        else:
+            search = ml_item.item_id
+
+        response, out = self._music_lib_search(search, start, max_items)
+        out['search_type'] = 'browse'
+        out['item_list'] = []
+
+        # Parse the results
+        dom = XML.fromstring(really_utf8(response['Result']))
+        for container in dom:
+            item = get_ml_item(container)
+            out['item_list'].append(item)
+
+        return out
+
+    def _music_lib_search(self, search, start, max_items):
+        """Perform a music library search and extract search numbers
 
-        # Added sub-category suport, extend the search into a subcategory
-        if sub_category != '':
-            if not sub_category.startswith('/'):
-                sub_category = '/' + sub_category
-            search = search + sub_category
+        You can get an overview of all the relevant search prefixes (like
+        'A:') and their meaning with the request:
 
+        .. code ::
+
+         response = device.contentDirectory.Browse([
+             ('ObjectID', '0'),
+             ('BrowseFlag', 'BrowseDirectChildren'),
+             ('Filter', '*'),
+             ('StartingIndex', 0),
+             ('RequestedCount', 100),
+             ('SortCriteria', '')
+         ])
+
+        Args:
+            search (str): The ID to search
+            start: The index of the forst item to return
+            max_items: The maximum number of items to return
+
+        Returns:
+            tuple: (response, metadata) where response is the returned metadata
+                and metadata is a dict with the 'number_returned',
+                'total_matches' and 'update_id' integers
+        """
         response = self.contentDirectory.Browse([
             ('ObjectID', search),
             ('BrowseFlag', 'BrowseDirectChildren'),
@@ -1258,20 +1346,11 @@ class SoCo(_SocoSingletonBase):
             ('SortCriteria', '')
             ])
 
-        dom = XML.fromstring(really_utf8(response['Result']))
-
         # Get result information
-        out = {'item_list': [], 'search_type': search_type}
+        metadata = {}
         for tag in ['NumberReturned', 'TotalMatches', 'UpdateID']:
-            out[camel_to_underscore(tag)] = int(response[tag])
-
-        # Parse the results
-        for container in dom:
-            item = get_ml_item(container)
-            # Append the item to the list
-            out['item_list'].append(item)
-
-        return out
+            metadata[camel_to_underscore(tag)] = int(response[tag])
+        return response, metadata
 
     def add_uri_to_queue(self, uri):
         """Adds the URI to the queue
@@ -1430,3 +1509,8 @@ RADIO_SHOWS = 1
 NS = {'dc': '{http://purl.org/dc/elements/1.1/}',
       'upnp': '{urn:schemas-upnp-org:metadata-1-0/upnp/}',
       '': '{urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/}'}
+# Valid play modes
+PLAY_MODES = ('NORMAL', 'SHUFFLE_NOREPEAT', 'SHUFFLE', 'REPEAT_ALL')
+
+if config.SOCO_CLASS is None:
+    config.SOCO_CLASS = SoCo
diff --git a/script.sonos/resources/lib/soco/data_structures.py 
b/script.sonos/resources/lib/soco/data_structures.py
index 63a4b54..9291c22 100644
--- a/script.sonos/resources/lib/soco/data_structures.py
+++ b/script.sonos/resources/lib/soco/data_structures.py
@@ -24,27 +24,11 @@ def ns_tag(ns_id, tag):
 
 def get_ml_item(xml):
     """Return the music library item that corresponds to xml. The class is
-    identified by getting the parentID and making a lookup in the
-    PARENT_ID_TO_CLASS module variable dictionary.
+    identified by getting the UPNP class making a lookup in the
+    DIDL_CLASS_TO_CLASS module variable dictionary.
 
     """
-    # Add the option to auto detect if the given parent ID is not in
-    # the array (The case when you have a sub-category, because a
-    # request of A:GENRE/Pop will actually return Artists, not genres)
-    cls = MusicLibraryItem
-    if xml.get('parentID') in PARENT_ID_TO_CLASS.keys():
-        cls = PARENT_ID_TO_CLASS[xml.get('parentID')]
-    else:
-        # Try and auto detect which type this is from the XML returned
-        class_type = xml.findtext(ns_tag('upnp', 'class'))
-        if class_type is not None:
-            if 'musicTrack' in class_type:
-                cls = MLTrack
-            elif 'musicAlbum' in class_type:
-                cls = MLAlbum
-            elif 'musicArtist' in class_type:
-                cls = MLArtist
-
+    cls = DIDL_CLASS_TO_CLASS[xml.find(ns_tag('upnp', 'class')).text]
     return cls.from_xml(xml=xml)
 
 
@@ -134,9 +118,9 @@ class MusicInfoItem(object):
 class MusicLibraryItem(MusicInfoItem):
     """Abstract class for a queueable item from the music library.
 
-    :ivar parent_id: The parent ID for the music library item is ``None``,
-        since it is a abstract class and it should be overwritten in the sub
-        classes
+    :ivar item_class: The DIDL Lite class for the music library item is
+        ``None``, since it is a abstract class and it should be overwritten in
+        the sub classes
     :ivar _translation: The dictionary-key-to-xml-tag-and-namespace-
         translation used when instantiating a MusicLibraryItems from XML. The
         default value is shown below. This default value applies to most sub
@@ -147,25 +131,23 @@ class MusicLibraryItem(MusicInfoItem):
             # key: (ns, tag)
             _translation = {
                 'title': ('dc', 'title'),
-                'uri': ('', 'res'),
-                'item_class': ('upnp', 'class')
+                'uri': ('', 'res')
             }
 
     """
-    parent_id = None
+    item_class = None
     # key: (ns, tag)
     _translation = {
         'title': ('dc', 'title'),
-        'uri': ('', 'res'),
-        'item_class': ('upnp', 'class')
+        'uri': ('', 'res')
     }
 
-    def __init__(self, uri, title, item_class, **kwargs):
+    def __init__(self, uri, title, parent_id, **kwargs):
         r"""Initialize the MusicLibraryItem from parameter arguments.
 
         :param uri: The URI for the item
         :param title: The title for the item
-        :param item_class: The UPnP class for the item
+        :param parent_id: The parent ID for the item
         :param \*\*kwargs: Extra information items to form the music library
             item from. Valid keys are ``album``, ``album_art_uri``,
             ``creator`` and ``original_track_number``.
@@ -176,10 +158,10 @@ class MusicLibraryItem(MusicInfoItem):
         super(MusicLibraryItem, self).__init__()
 
         # Parse the input arguments
-        arguments = {'uri': uri, 'title': title, 'item_class': item_class}
+        arguments = {'uri': uri, 'title': title, 'parent_id': parent_id}
         arguments.update(kwargs)
         for key, value in arguments.items():
-            if key in self._translation:
+            if key in self._translation or key == 'parent_id':
                 self.content[key] = value
             else:
                 raise ValueError(
@@ -199,18 +181,25 @@ class MusicLibraryItem(MusicInfoItem):
 
         """
         content = {}
+        # Get values from _translation
         for key, value in cls._translation.items():
             result = xml.find(ns_tag(*value))
             if result is None:
                 content[key] = None
+            elif result.text is None:
+                content[key] = None
             else:
                 # The xml objects should contain utf-8 internally
                 content[key] = really_unicode(result.text)
-        args = [content.pop(arg) for arg in ['uri', 'title', 'item_class']]
+
+        # Extract the parent ID
+        content['parent_id'] = xml.get('parentID')
+
+        # Convert type for original track number
         if content.get('original_track_number') is not None:
             content['original_track_number'] = \
                 int(content['original_track_number'])
-        return cls(*args, **content)
+        return cls.from_dict(content)
 
     @classmethod
     def from_dict(cls, content):
@@ -224,7 +213,7 @@ class MusicLibraryItem(MusicInfoItem):
         """
         # Make a copy since this method will modify the input dict
         content_in = content.copy()
-        args = [content_in.pop(arg) for arg in ['uri', 'title', 'item_class']]
+        args = [content_in.pop(arg) for arg in ['uri', 'title', 'parent_id']]
         return cls(*args, **content_in)
 
     @property
@@ -320,13 +309,13 @@ class MusicLibraryItem(MusicInfoItem):
         self.content['uri'] = uri
 
     @property
-    def item_class(self):
-        """Get and set the UPnP object class as an unicode object."""
-        return self.content['item_class']
+    def parent_id(self):
+        """Get and set the parent ID."""
+        return self.content['parent_id']
 
-    @item_class.setter
-    def item_class(self, item_class):  # pylint: disable=C0111
-        self.content['item_class'] = item_class
+    @parent_id.setter
+    def parent_id(self, parent_id):  # pylint: disable=C0111
+        self.content['parent_id'] = parent_id
 
 
 class MLTrack(MusicLibraryItem):
@@ -346,13 +335,12 @@ class MLTrack(MusicLibraryItem):
                 'album': ('upnp', 'album'),
                 'album_art_uri': ('upnp', 'albumArtURI'),
                 'uri': ('', 'res'),
-                'original_track_number': ('upnp', 'originalTrackNumber'),
-                'item_class': ('upnp', 'class')
+                'original_track_number': ('upnp', 'originalTrackNumber')
             }
 
     """
 
-    parent_id = 'A:TRACKS'
+    item_class = 'object.item.audioItem.musicTrack'
     # name: (ns, tag)
     _translation = {
         'title': ('dc', 'title'),
@@ -360,26 +348,9 @@ class MLTrack(MusicLibraryItem):
         'album': ('upnp', 'album'),
         'album_art_uri': ('upnp', 'albumArtURI'),
         'uri': ('', 'res'),
-        'original_track_number': ('upnp', 'originalTrackNumber'),
-        'item_class': ('upnp', 'class')
+        'original_track_number': ('upnp', 'originalTrackNumber')
     }
 
-    def __init__(self, uri, title,
-                 item_class='object.item.audioItem.musicTrack', **kwargs):
-        r"""Instantiate the MLTrack item by passing the arguments to the
-        super class :py:meth:`.MusicLibraryItem.__init__`.
-
-        :param uri: The URI for the track
-        :param title: The title of the track
-        :param item_class: The UPnP class for the track. The default value is:
-            ``object.item.audioItem.musicTrack``
-        :param \*\*kwargs: Optional extra information items, valid keys are:
-            ``album``, ``album_art_uri``, ``creator``,
-            ``original_track_number``. ``original_track_number`` is an ``int``.
-            All other values are unicode objects.
-        """
-        MusicLibraryItem.__init__(self, uri, title, item_class, **kwargs)
-
     @property
     def item_id(self):  # pylint: disable=C0103
         """Return the id."""
@@ -434,7 +405,8 @@ class MLTrack(MusicLibraryItem):
 class MLAlbum(MusicLibraryItem):
     """Class that represents a music library album.
 
-    :ivar parent_id: The parent ID for the MLTrack is 'A:ALBUM'
+    :ivar item_class: The item_class for MLTrack is
+        'object.container.album.musicAlbum'
     :ivar _translation: The dictionary-key-to-xml-tag-and-namespace-
         translation used when instantiating a MLAlbum from XML. The value is
         shown below
@@ -446,37 +418,20 @@ class MLAlbum(MusicLibraryItem):
                 'title': ('dc', 'title'),
                 'creator': ('dc', 'creator'),
                 'album_art_uri': ('upnp', 'albumArtURI'),
-                'uri': ('', 'res'),
-                'item_class': ('upnp', 'class')
+                'uri': ('', 'res')
             }
 
     """
 
-    parent_id = 'A:ALBUM'
+    item_class = 'object.container.album.musicAlbum'
     # name: (ns, tag)
     _translation = {
         'title': ('dc', 'title'),
         'creator': ('dc', 'creator'),
         'album_art_uri': ('upnp', 'albumArtURI'),
-        'uri': ('', 'res'),
-        'item_class': ('upnp', 'class')
+        'uri': ('', 'res')
     }
 
-    def __init__(self, uri, title,
-                 item_class='object.container.album.musicAlbum', **kwargs):
-        r"""Instantiate the MLAlbum item by passing the arguments to the
-        super class :py:meth:`.MusicLibraryItem.__init__`.
-
-        :param uri: The URI for the alum
-        :param title: The title of the album
-        :param item_class: The UPnP class for the album. The default value is:
-            ``object.container.album.musicAlbum``
-        :param \*\*kwargs: Optional extra information items, valid keys are:
-            ``album_art_uri`` and ``creator``. All value should be unicode
-            objects.
-        """
-        MusicLibraryItem.__init__(self, uri, title, item_class, **kwargs)
-
     @property
     def creator(self):
         """Get and set the creator as an unicode object."""
@@ -499,129 +454,66 @@ class MLAlbum(MusicLibraryItem):
 class MLArtist(MusicLibraryItem):
     """Class that represents a music library artist.
 
-    :ivar parent_id: The parent ID for the MLArtist is 'A:ARTIST'
+    :ivar item_class: The item_class for MLArtist is
+        'object.container.person.musicArtist'
     :ivar _translation: The dictionary-key-to-xml-tag-and-namespace-
         translation used when instantiating a MLArtist from XML is inherited
         from :py:class:`.MusicLibraryItem`.
     """
 
-    parent_id = 'A:ARTIST'
+    item_class = 'object.container.person.musicArtist'
 
-    def __init__(self, uri, title,
-                 item_class='object.container.person.musicArtist'):
+    def __init__(self, uri, title, parent_id):
         """Instantiate the MLArtist item by passing the arguments to the
         super class :py:meth:`.MusicLibraryItem.__init__`.
 
         :param uri: The URI for the artist
         :param title: The title of the artist
-        :param item_class: The UPnP class for the artist. The default value is:
-            ``object.container.person.musicArtist``
-        """
-        MusicLibraryItem.__init__(self, uri, title, item_class)
-
-
-class MLAlbumArtist(MusicLibraryItem):
-    """Class that represents a music library album artist.
-
-    :ivar parent_id: The parent ID for the MLAlbumArtist is 'A:ALBUMARTIST'
-    :ivar _translation: The dictionary-key-to-xml-tag-and-namespace-
-        translation used when instantiating a MLAlbumArtist from XML is
-        inherited from :py:class:`.MusicLibraryItem`.
-
-    """
-
-    parent_id = 'A:ALBUMARTIST'
-
-    def __init__(self, uri, title,
-                 item_class='object.container.person.musicArtist'):
-        """Instantiate the MLAlbumArtist item by passing the arguments to the
-        super class :py:meth:`.MusicLibraryItem.__init__`.
-
-        :param uri: The URI for the alum artist
-        :param title: The title of the album artist
-        :param item_class: The UPnP class for the album artist. The default
-            value is: ``object.container.person.musicArtist``
-
+        :param item_class: The parent ID for the artist
         """
-        MusicLibraryItem.__init__(self, uri, title, item_class)
+        MusicLibraryItem.__init__(self, uri, title, parent_id)
 
 
 class MLGenre(MusicLibraryItem):
     """Class that represents a music library genre.
 
-    :ivar parent_id: The parent ID for the MLGenre is 'A:GENRE'
+    :ivar item_class: The item class for the MLGenre is
+        'object.container.genre.musicGenre'
     :ivar _translation: The dictionary-key-to-xml-tag-and-namespace-
         translation used when instantiating a MLGenre from XML is inherited
         from :py:class:`.MusicLibraryItem`.
 
     """
 
-    parent_id = 'A:GENRE'
-
-    def __init__(self, uri, title,
-                 item_class='object.container.genre.musicGenre'):
-        """Instantiate the MLGenre item by passing the arguments to the
-        super class :py:meth:`.MusicLibraryItem.__init__`.
-
-        :param uri: The URI for the genre
-        :param title: The title of the genre
-        :param item_class: The UPnP class for the genre. The default value is:
-            ``object.container.genre.musicGenre``
-
-        """
-        MusicLibraryItem.__init__(self, uri, title, item_class)
+    item_class = 'object.container.genre.musicGenre'
 
 
 class MLComposer(MusicLibraryItem):
     """Class that represents a music library composer.
 
-    :ivar parent_id: The parent ID for the MLComposer is 'A:COMPOSER'
+    :ivar item_class: The item_class for MLComposer is
+        'object.container.person.composer'
     :ivar _translation: The dictionary-key-to-xml-tag-and-namespace-
         translation used when instantiating a MLComposer from XML is inherited
         from :py:class:`.MusicLibraryItem`.
 
     """
 
-    parent_id = 'A:COMPOSER'
-
-    def __init__(self, uri, title,
-                 item_class='object.container.person.composer'):
-        """Instantiate the MLComposer item by passing the arguments to the
-        super class :py:meth:`.MusicLibraryItem.__init__`.
-
-        :param uri: The URI for the composer
-        :param title: The title of the composer
-        :param item_class: The UPnP class for the composer. The default value
-            is: ``object.container.person.composer``
-
-        """
-        MusicLibraryItem.__init__(self, uri, title, item_class)
+    item_class = 'object.container.person.composer'
 
 
 class MLPlaylist(MusicLibraryItem):
     """Class that represents a music library play list.
 
-    :ivar parent_id: The parent ID for the MLPlaylist is 'A:PLAYLIST'
+    :ivar item_class: The item_class for the MLPlaylist is
+        'object.container.playlistContainer'
     :ivar _translation: The dictionary-key-to-xml-tag-and-namespace-
         translation used when instantiating a MLPlaylist from XML is inherited
         from :py:class:`.MusicLibraryItem`.
 
     """
 
-    parent_id = 'A:PLAYLISTS'
-
-    def __init__(self, uri, title,
-                 item_class='object.container.playlistContainer'):
-        """Instantiate the MLPlaylist item by passing the arguments to the
-        super class :py:meth:`.MusicLibraryItem.__init__`.
-
-        :param uri: The URI for the playlist
-        :param title: The title of the playlist
-        :param item_class: The UPnP class for the playlist. The default value
-            is: ``object.container.playlistContainer``
-
-        """
-        MusicLibraryItem.__init__(self, uri, title, item_class)
+    item_class = 'object.container.playlistContainer'
 
     @property
     def item_id(self):  # pylint: disable=C0103
@@ -630,59 +522,89 @@ class MLPlaylist(MusicLibraryItem):
         if 'x-file-cifs' in out:
             out = out.replace('x-file-cifs', 'S')
         else:
-            out = None
+            out = super(MLPlaylist, self).item_id
         return out
 
 
 class MLSonosPlaylist(MusicLibraryItem):
     """ Class that represents a sonos playlist.
 
-    :ivar parent_id: The parent ID for the MLSonosPlaylist is 'SQ:'
+    :ivar item_class: The item_class for MLSonosPlaylist is
+        'object.container.playlistContainer'
     :ivar _translation: The dictionary-key-to-xml-tag-and-namespace-
         translation used when instantiating MLSonosPlaylist from
         XML is inherited from :py:class:`.MusicLibraryItem`.
 
     """
 
-    parent_id = 'SQ:'
-
-    def __init__(self, uri, title,
-                 item_class='object.container.playlistContainer'):
-        """ Instantiate the MLSonosPlaylist item by passing the arguments to
-        the super class :py:meth:`.MusicLibraryItem.__init__`.
-
-        :param uri: The URI for the playlist
-        :param title: The title of the playlist
-        :param item_class: The UPnP class for the playlist. The default value
-            is: ``object.container.playlistContainer``
-
-        """
-        MusicLibraryItem.__init__(self, uri, title, item_class)
+    item_class = 'object.container.playlistContainer'
 
 
 class MLShare(MusicLibraryItem):
     """Class that represents a music library share.
 
-    :ivar parent_id: The parent ID for the MLShare is 'S:'
+    :ivar item_class: The item_class for the MLShare is 'object.container'
     :ivar _translation: The dictionary-key-to-xml-tag-and-namespace-
         translation used when instantiating a MLShare from XML is inherited
         from :py:class:`.MusicLibraryItem`.
 
     """
 
-    parent_id = 'S:'
+    item_class = 'object.container'
+
 
-    def __init__(self, uri, title, item_class='object.container'):
-        """Instantiate the MLShare item by passing the arguments to the
+class MLCategory(MusicLibraryItem):
+    """Class that represents a music library category.
+
+    :ivar item_class: The item_class for the MLCategory is 'object.container'
+    :ivar _translation: The dictionary-key-to-xml-tag-and-namespace-
+        translation used when instantiating a MLCategory from XML is inherited
+        from :py:class:`.MusicLibraryItem`.
+
+    """
+
+    item_class = 'object.container'
+
+
+class MLAlbumList(MusicLibraryItem):
+    """Class that represents a music library album list.
+
+    :ivar item_class: The item_class for MLAlbumList is
+        'object.container.albumlist'
+    :ivar _translation: The dictionary-key-to-xml-tag-and-namespace-
+        translation used when instantiating a MLAlbumList from XML is inherited
+        from :py:class:`.MusicLibraryItem`.
+
+    """
+
+    item_class = 'object.container.albumlist'
+
+
+class MLSameArtist(MusicLibraryItem):
+    """Class that represents all by the artist.
+
+    This type is returned by browsing an artist or a composer
+
+    :ivar item_class: The item_class for MLSameArtist is
+        'object.container.playlistContainer.sameArtist'
+    :ivar _translation: The dictionary-key-to-xml-tag-and-namespace-
+        translation used when instantiating a MLSameArtist from XML is
+        inherited from :py:class:`.MusicLibraryItem`.
+
+    """
+
+    item_class = 'object.container.playlistContainer.sameArtist'
+
+    def __init__(self, uri, title, parent_id):
+        """Instantiate the MLSameArtist item by passing the arguments to the
         super class :py:meth:`.MusicLibraryItem.__init__`.
 
-        :param uri: The URI for the share
-        :param title: The title of the share
-        :param item_class: The UPnP class for the share. The default value is:
-            ``object.container``
+        :param uri: The URI for the composer
+        :param title: The title of the composer
+        :param item_class: The parent ID for the composer
 
         """
-        MusicLibraryItem.__init__(self, uri, title, item_class)
+        MusicLibraryItem.__init__(self, uri, title, parent_id)
 
 
 ###############################################################################
@@ -1378,11 +1300,17 @@ class MSCollection(MusicServiceItem):
         super(MSCollection, self).__init__(**content)
 
 
-PARENT_ID_TO_CLASS = {'A:TRACKS': MLTrack, 'A:ALBUM': MLAlbum,
-                      'A:ARTIST': MLArtist, 'A:ALBUMARTIST': MLAlbumArtist,
-                      'A:GENRE': MLGenre, 'A:COMPOSER': MLComposer,
-                      'A:PLAYLISTS': MLPlaylist, 'S:': MLShare,
-                      'SQ:': MLSonosPlaylist}
+DIDL_CLASS_TO_CLASS = {'object.item.audioItem.musicTrack': MLTrack,
+                       'object.container.album.musicAlbum': MLAlbum,
+                       'object.container.person.musicArtist': MLArtist,
+                       'object.container.genre.musicGenre': MLGenre,
+                       'object.container.person.composer': MLComposer,
+                       'object.container.playlistContainer': MLPlaylist,
+                       'object.container': MLCategory,
+                       'object.container.albumlist': MLAlbumList,
+                       'object.container.playlistContainer.sameArtist':
+                           MLSameArtist}
+
 
 MS_TYPE_TO_CLASS = {'artist': MSArtist, 'album': MSAlbum, 'track': MSTrack,
                     'albumList': MSAlbumList, 'favorites': MSFavorites,
diff --git a/script.sonos/resources/lib/soco/events.py 
b/script.sonos/resources/lib/soco/events.py
index 9df1df4..716f723 100644
--- a/script.sonos/resources/lib/soco/events.py
+++ b/script.sonos/resources/lib/soco/events.py
@@ -16,6 +16,7 @@ import logging
 import weakref
 from collections import namedtuple
 import time
+import atexit
 
 import requests
 
@@ -182,6 +183,7 @@ class Subscription(object):
     """ A class representing the subscription to a UPnP event
 
     """
+# pylint: disable=too-many-instance-attributes
 
     def __init__(self, service, event_queue=None):
         """ Pass a SoCo Service instance as a parameter. If event_queue is
@@ -196,24 +198,61 @@ class Subscription(object):
         self.is_subscribed = False
         #: A queue of events received
         self.events = Queue() if event_queue is None else event_queue
+        #: The period for which the subscription is requested
+        self.requested_timeout = None
         # A flag to make sure that an unsubscribed instance is not
         # resubscribed
         self._has_been_unsubscribed = False
         # The time when the subscription was made
         self._timestamp = None
+        # Used to keep track of the auto_renew thread
+        self._auto_renew_thread = None
+        self._auto_renew_thread_flag = threading.Event()
 
-    def subscribe(self, requested_timeout=None):
+    def subscribe(self, requested_timeout=None, auto_renew=False):
         """ Subscribe to the service.
 
         If requested_timeout is provided, a subscription valid for that number
         of seconds will be requested, but not guaranteed. Check
         :attrib:`timeout` on return to find out what period of validity is
-        actually allocated. """
+        actually allocated.
+
+        Note:
+            SoCo will try to unsubscribe any subscriptions which are still
+            subscribed on program termination, but it is good practice for
+            you to clean up by making sure that you call :meth:`unsubscribe`
+            yourself.
+
+        Args:
+            requested_timeout(int, optional): The timeout to be requested
+            auto_renew:(bool, optional): If True, renew the subscription
+                automatically shortly before timeout. Default False
+        """
+
+        class AutoRenewThread(threading.Thread):
+            """ Used by the auto_renew code to renew a subscription from
+                within a thread.
+                """
+
+            def __init__(self, interval, stop_flag, sub, *args, **kwargs):
+                super(AutoRenewThread, self).__init__(*args, **kwargs)
+                self.interval = interval
+                self.sub = sub
+                self.stop_flag = stop_flag
+                self.daemon = True
+
+            def run(self):
+                sub = self.sub
+                stop_flag = self.stop_flag
+                interval = self.interval
+                while not stop_flag.wait(interval):
+                    log.debug("Autorenewing subscription %s", sub.sid)
+                    sub.renew()
 
         # TIMEOUT is provided for in the UPnP spec, but it is not clear if
         # Sonos pays any attention to it. A timeout of 86400 secs always seems
         # to be allocated
-
+        self.requested_timeout = requested_timeout
         if self._has_been_unsubscribed:
             raise SoCoException(
                 'Cannot resubscribe instance once unsubscribed')
@@ -235,7 +274,7 @@ class Subscription(object):
             'NT': 'upnp:event'
         }
         if requested_timeout is not None:
-            headers["TIMEOUT"] = "Seconds-{0}".format(requested_timeout)
+            headers["TIMEOUT"] = "Second-{0}".format(requested_timeout)
         response = requests.request(
             'SUBSCRIBE', service.base_url + service.event_subscription_url,
             headers=headers)
@@ -261,6 +300,19 @@ class Subscription(object):
         # And do the same for the sid to service mapping
         with _sid_to_service_lock:
             _sid_to_service[self.sid] = self.service
+        # Register this subscription to be unsubscribed at exit if still alive
+        # This will not happen if exit is abnormal (eg in response to a
+        # signal or fatal interpreter error - see the docs for `atexit`).
+        atexit.register(self.unsubscribe)
+
+        # Set up auto_renew
+        if not auto_renew:
+            return
+        # Autorenew just before expiry, say at 85% of self.timeout seconds
+        interval = self.timeout * 85/100
+        auto_renew_thread = AutoRenewThread(
+            interval, self._auto_renew_thread_flag, self)
+        auto_renew_thread.start()
 
     def renew(self, requested_timeout=None):
         """Renew the event subscription.
@@ -268,10 +320,21 @@ class Subscription(object):
         You should not try to renew a subscription which has been
         unsubscribed, or once it has expired.
 
+        Args:
+            requested_timeout (int, optional): The period for which a renewal
+                request should be made. If None (the default), use the timeout
+                requested on subscription.
+
         """
+        # NB This code is sometimes called from a separate thread (when
+        # subscriptions are auto-renewed. Be careful to ensure thread-safety
+
         if self._has_been_unsubscribed:
             raise SoCoException(
                 'Cannot renew subscription once unsubscribed')
+        if not self.is_subscribed:
+            raise SoCoException(
+                'Cannot renew subscription before subscribing')
         if self.time_left == 0:
             raise SoCoException(
                 'Cannot renew subscription after expiry')
@@ -280,10 +343,11 @@ class Subscription(object):
         # HOST: publisher host:publisher port
         # SID: uuid:subscription UUID
         # TIMEOUT: Second-requested subscription duration (optional)
-
         headers = {
             'SID': self.sid
         }
+        if requested_timeout is None:
+            requested_timeout = self.requested_timeout
         if requested_timeout is not None:
             headers["TIMEOUT"] = "Second-{0}".format(requested_timeout)
         response = requests.request(
@@ -312,6 +376,14 @@ class Subscription(object):
         Once unsubscribed, a Subscription instance should not be reused
 
         """
+        # Trying to unsubscribe if already unsubscribed, or not yet
+        # subscribed, fails silently
+        if self._has_been_unsubscribed or not self.is_subscribed:
+            return
+
+        # Cancel any auto renew
+        self._auto_renew_thread_flag.set()
+        # Send an unsubscribe request like this:
         # UNSUBSCRIBE publisher path HTTP/1.1
         # HOST: publisher host:publisher port
         # SID: uuid:subscription UUID
diff --git a/script.sonos/resources/lib/soco/services.py 
b/script.sonos/resources/lib/soco/services.py
index d50fdb5..bd022bc 100644
--- a/script.sonos/resources/lib/soco/services.py
+++ b/script.sonos/resources/lib/soco/services.py
@@ -44,8 +44,9 @@ from xml.sax.saxutils import escape
 import logging
 
 import requests
+from .cache import Cache
 from .exceptions import SoCoUPnPException, UnknownSoCoException
-from .utils import prettify, TimedCache
+from .utils import prettify
 from .events import Subscription
 from .xml import XML
 
@@ -60,7 +61,7 @@ Argument = namedtuple('Argument', 'name, vartype')
 # SoCo instance is asked for group info, we can cache it and return it when
 # another instance is asked. To do this we need a cache to be shared between
 # instances
-zone_group_state_shared_cache = TimedCache()
+zone_group_state_shared_cache = Cache()
 
 
 # pylint: disable=too-many-instance-attributes
@@ -105,7 +106,7 @@ class Service(object):
         self.event_subscription_url = '/{0}/Event'.format(self.service_type)
         #: A cache for storing the result of network calls. By default, this is
         #: TimedCache(default_timeout=0). See :class:`TimedCache`
-        self.cache = TimedCache(default_timeout=0)
+        self.cache = Cache(default_timeout=0)
         log.debug(
             "Created service %s, ver %s, id %s, base_url %s, control_url %s",
             self.service_type, self.version, self.service_id, self.base_url,
@@ -413,13 +414,15 @@ class Service(object):
             log.error("Unknown error received from %s", self.soco.ip_address)
             raise UnknownSoCoException(xml_error)
 
-    def subscribe(self, requested_timeout=None, event_queue=None):
+    def subscribe(
+            self, requested_timeout=None, auto_renew=False, event_queue=None):
         """Subscribe to the service's events.
 
         If requested_timeout is provided, a subscription valid for that number
         of seconds will be requested, but not guaranteed. Check
         :attrib:`Subscription.timeout` on return to find out what period of
-        validity is actually allocated.
+        validity is actually allocated. If auto_renew is True, the subscription
+        will be automatically renewed just before it expires, if possible
 
         event_queue is a thread-safe queue object onto which events will be
         put. If None, a Queue object will be created and used.
@@ -431,7 +434,8 @@ class Service(object):
         """
         subscription = Subscription(
             self, event_queue)
-        subscription.subscribe(requested_timeout=requested_timeout)
+        subscription.subscribe(
+            requested_timeout=requested_timeout, auto_renew=auto_renew)
         return subscription
 
     def _update_cache_on_event(self, event):
@@ -530,6 +534,10 @@ class AlarmClock(Service):
     """ Sonos alarm service, for setting and getting time and alarms. """
     def __init__(self, soco):
         super(AlarmClock, self).__init__(soco)
+        self.UPNP_ERRORS.update(
+            {
+                801: 'Already an alarm for this time',
+            })
 
 
 class MusicServices(Service):
diff --git a/script.sonos/resources/lib/soco/utils.py 
b/script.sonos/resources/lib/soco/utils.py
index 4532a7f..59a2b70 100644
--- a/script.sonos/resources/lib/soco/utils.py
+++ b/script.sonos/resources/lib/soco/utils.py
@@ -2,12 +2,14 @@
 
 """ Provides general utility functions to be used across modules """
 
-from __future__ import unicode_literals, absolute_import
+from __future__ import unicode_literals, absolute_import, print_function
 
 import re
-import threading
-from time import time
-from .compat import StringType, UnicodeType, dumps
+import functools
+import warnings
+
+from .compat import StringType, UnicodeType
+from .xml import XML
 
 
 def really_unicode(in_string):
@@ -63,99 +65,71 @@ def prettify(unicode_text):
     return reparsed.toprettyxml(indent="  ", newl="\n")
 
 
-class TimedCache(object):
-
-    """ A simple thread-safe cache for caching method return values
+def show_xml(xml):
+    """Pretty print an ElementTree XML object
 
-    At present, the cache can theoretically grow and grow, since entries are
-    not automatically purged, though in practice this is unlikely since there
-    are not that many different combinations of arguments in the places where
-    it is used in SoCo, so not that many different cache entries will be
-    created. If this becomes a problem, use a thread and timer to purge the
-    cache, or rewrite this to use LRU logic!
+    Args:
+        xml (ElementTree): The :py:class:`xml.etree.ElementTree` to pretty
+            print
 
+    NOTE: This function is a convenience function used during development, it
+    is not used anywhere in the main code base
     """
+    string = XML.tostring(xml)
+    print(prettify(string))
 
-    def __init__(self, default_timeout=0):
-        super(TimedCache, self).__init__()
-        self._cache = {}
-        # A thread lock for the cache
-        self._cache_lock = threading.Lock()
-        #: The default caching interval in seconds. Set to 0
-        #: to disable the cache by default
-        self.default_timeout = default_timeout
-
-    @staticmethod
-    def make_key(args, kwargs):
-        """
-        Generate a unique, hashable, representation of the args and kwargs
-
-        """
-        # This is not entirely straightforward, since args and kwargs may
-        # contain mutable items and unicode. Possibiities include using
-        # __repr__, frozensets, and code from Py3's LRU cache. But pickle
-        # works, and although it is not as fast as some methods, it is good
-        # enough at the moment
-        cache_key = dumps((args, kwargs))
-        return cache_key
-
-    def get(self, *args, **kwargs):
-
-        """
-
-        Get an item from the cache for this combination of args and kwargs.
-
-        Return None if no unexpired item is found. This means that there is no
-        point storing an item in the cache if it is None.
-
-        """
-        # Look in the cache to see if there is an unexpired item. If there is
-        # we can just return the cached result.
-        cache_key = self.make_key(args, kwargs)
-        # Lock and load
-        with self._cache_lock:
-            if cache_key in self._cache:
-                expirytime, item = self._cache[cache_key]
-
-                if expirytime >= time():
-                    return item
-                else:
-                    # An expired item is present - delete it
-                    del self._cache[cache_key]
-        # Nothing found
-        return None
-
-    def put(self, item, *args, **kwargs):
-
-        """ Put an item into the cache, for this combination of args and
-        kwargs.
-
-        If `timeout` is specified as one of the keyword arguments, the item
-        will remain available for retrieval for `timeout` seconds. If `timeout`
-        is None or not specified, the default cache timeout for this cache will
-        be used. Specify a `timeout` of 0 (or ensure that the default timeout
-        for this cache is 0) if this item is not to be cached."""
-
-        # Check for a timeout keyword, store and remove it.
-        timeout = kwargs.pop('timeout', None)
-        if timeout is None:
-            timeout = self.default_timeout
-        cache_key = self.make_key(args, kwargs)
-        # Store the item, along with the time at which it will expire
-        with self._cache_lock:
-            self._cache[cache_key] = (time() + timeout, item)
-
-    def delete(self, *args, **kwargs):
-        """Delete an item from the cache for this combination of args and
-        kwargs"""
-        cache_key = self.make_key(args, kwargs)
-        with self._cache_lock:
-            try:
-                del self._cache[cache_key]
-            except KeyError:
+
+class deprecated(object):
+    """ A decorator to mark deprecated objects.
+
+    Causes a warning to be issued when the object is used, and marks the object
+    as deprecated in the Sphinx docs.
+
+    args:
+        since (str): The version in which the object is deprecated
+        alternative (str, optional): The name of an alternative object to use
+
+    Example:
+
+        ::
+
+            @deprecated(since="0.7", alternative="new_function")
+            def old_function(args):
                 pass
 
-    def clear(self):
-        """Empty the whole cache"""
-        with self._cache_lock:
-            self._cache.clear()
+
+    """
+    # pylint really doesn't like decorators!
+    # pylint: disable=invalid-name, too-few-public-methods
+    # pylint: disable=no-member, missing-docstring
+    def __init__(self, since, alternative=None, will_be_removed_in=None):
+        self.since_version = since
+        self.alternative = alternative
+        self.will_be_removed_in = will_be_removed_in
+
+    def __call__(self, deprecated_fn):
+
+        @functools.wraps(deprecated_fn)
+        def decorated(*args, **kwargs):
+
+            message = "Call to deprecated function {0}.".format(
+                deprecated_fn.__name__)
+            if self.will_be_removed_in is not None:
+                message += " Will be removed in version {0}.".format(
+                    self.will_be_removed_in)
+            if self.alternative is not None:
+                message += " Use {0} instead.".format(self.alternative)
+            warnings.warn(message, stacklevel=2)
+
+            return deprecated_fn(*args, **kwargs)
+
+        docs = "\n\n  .. deprecated:: {0}\n".format(self.since_version)
+        if self.will_be_removed_in is not None:
+            docs += "\n     Will be removed in version {0}.".format(
+                self.will_be_removed_in)
+        if self.alternative is not None:
+            docs += "\n     Use {0} instead.".format(self.alternative)
+        if decorated.__doc__ is None:
+            decorated.__doc__ = ''
+        decorated.__doc__ += docs
+        return decorated
diff --git a/script.sonos/resources/lib/sonos.py 
b/script.sonos/resources/lib/sonos.py
index 876a21c..5d2cb65 100644
--- a/script.sonos/resources/lib/sonos.py
+++ b/script.sonos/resources/lib/sonos.py
@@ -6,6 +6,7 @@ import logging
 # Load the Soco classes

 from soco import SoCo

 from soco.event_structures import LastChangeEvent

+from soco.data_structures import MusicLibraryItem

 

 # Use the SoCo logger

 LOGGER = logging.getLogger('soco')

@@ -29,11 +30,12 @@ class Sonos(SoCo):
 

     # Override method so that the album art http reference can be added

     def get_music_library_information(self, search_type, start=0, 
max_items=100, sub_category=''):

-        # Make sure the sub category is valid for the message, escape invalid 
characters

-        sub_category = cgi.escape(sub_category)

-

-        # Call the base version

-        musicInfo = SoCo.get_music_library_information(self, search_type, 
start, max_items, sub_category)

+        # Call the normal view if not browsing deeper

+        if (sub_category is None) or (sub_category == ''):

+            musicInfo = SoCo.get_music_library_information(self, search_type, 
start, max_items)

+        else:

+            # Call the browse version

+            musicInfo = self.browse(search_type, sub_category, start, 
max_items)

 

         if musicInfo is not None:

             for anItem in musicInfo['item_list']:

@@ -42,6 +44,28 @@ class Sonos(SoCo):
 

         return musicInfo

 

+    def browse(self, search_type, sub_category, start=0, max_items=100):

+        # Make sure the sub category is valid for the message, escape invalid 
characters

+        sub_category = cgi.escape(sub_category)

+

+        search_translation = {'artists': 'A:ARTIST',

+                              'album_artists': 'A:ALBUMARTIST',

+                              'albums': 'A:ALBUM',

+                              'genres': 'A:GENRE',

+                              'composers': 'A:COMPOSER',

+                              'tracks': 'A:TRACKS',

+                              'playlists': 'A:PLAYLISTS',

+                              'share': 'S:',

+                              'sonos_playlists': 'SQ:',

+                              'categories': 'A:'}

+        search = search_translation[search_type]

+

+        search_uri = "#%s%s" % (search, sub_category)

+        search_item = MusicLibraryItem(uri=search_uri, title='', parent_id='')

+

+        # Call the base version

+        return SoCo.browse(self, search_item, start, max_items)

+

     # Override method so that the album art http reference can be added

     def get_queue(self, start=0, max_items=100):

         list = SoCo.get_queue(self, start=start, max_items=max_items)

diff --git a/script.sonos/resources/settings.xml 
b/script.sonos/resources/settings.xml
index 0181787..826afb1 100644
--- a/script.sonos/resources/settings.xml
+++ b/script.sonos/resources/settings.xml
@@ -10,11 +10,13 @@
                <setting id="useTestData" type="bool" label="32017" 
default="false"/>

        </category>

        <category label="32015">

-               <setting id="refreshInterval" label="32016" type="slider" 
default="2" range="0.5,0.5,5" option="float"/>

                <setting id="displayArtistInfo" type="bool" label="32021" 
default="false"/>

+               <setting id="artistInfoLayout" subsetting="true" type="enum" 
label="32035" enable="eq(-1,true)" lvalues="32036|32037|32039"/>

+               <setting id="refreshInterval" label="32016" type="slider" 
default="2" range="0.5,0.5,5" option="float"/>

                <setting id="avoidDuplicateCommands" label="32022" 
type="slider" default="1.5" range="0.5,0.5,5" option="float"/>

                <setting id="volumeChangeIncrements" label="32025" 
type="slider" default="3" range="1,1,10" option="int"/>

-

+       </category>

+       <category label="32034">

                <setting id="linkAudioWithSonos" type="bool" label="32023" 
default="false"/>

                <setting id="switchSonosToLineIn" subsetting="true" 
enable="eq(-1,true)" type="bool" label="32024" default="false"/>

                <setting id="switchSonosToLineInOnMediaStart" subsetting="true" 
enable="eq(-2,true)" type="bool" label="32030" default="false"/>

@@ -22,15 +24,17 @@
                <setting id="autoPauseSonos" enable="eq(-2,false)" type="bool" 
label="32026" default="false"/>

                <setting id="autoResumeSonos" subsetting="true" 
enable="eq(-1,true)" label="32027" type="slider" default="0" range="0,3,60" 
option="int"/>

        </category>

+       <category label="32018">

+               <setting id="batchSize" label="32019" type="slider" 
default="100" range="10,10,1000" option="int"/>

+               <setting id="maxListEntries" label="32020" type="slider" 
default="1000" range="0,100,3000" option="int"/>

+               <setting id="useSkinIcons" type="bool" label="32038" 
default="false"/>          

+       </category>

        <category label="32011">

                <setting id="notifEnabled" type="bool" label="32003" 
default="true"/>

                <setting id="notifDisplayDuration" enable="eq(-1,true)" 
label="32004" type="slider" default="3" range="0,1,60" option="int"/>

                <setting id="notifCheckFrequency" enable="eq(-2,true)" 
label="32005" type="slider" default="10" range="0,1,60" option="int"/>

                <setting id="notifNotIfVideoPlaying" enable="eq(-3,true)" 
type="bool" label="32006" default="true"/>

-               <setting id="xbmcNotifDialog" enable="eq(-4,true)" type="bool" 
label="32007" default="false"/>

-       </category>

-       <category label="32018">

-               <setting id="batchSize" label="32019" type="slider" 
default="100" range="10,10,1000" option="int"/>

-               <setting id="maxListEntries" label="32020" type="slider" 
default="1000" range="0,100,3000" option="int"/>

+               <setting id="notifNotIfControllerShowing" enable="eq(-4,true)" 
type="bool" label="32040" default="true"/>

+               <setting id="xbmcNotifDialog" enable="eq(-5,true)" type="bool" 
label="32007" default="false"/>

        </category>

 </settings>

diff --git 
a/script.sonos/resources/skins/Default/720p/script-sonos-controller.xml 
b/script.sonos/resources/skins/Default/720p/script-sonos-controller.xml
index a42cbce..d94c91d 100644
--- a/script.sonos/resources/skins/Default/720p/script-sonos-controller.xml
+++ b/script.sonos/resources/skins/Default/720p/script-sonos-controller.xml
@@ -156,9 +156,9 @@
                        <control type="button" id="600">

                                <description>Previous Button</description>

                                <posx>0</posx>

-                               <posy>0</posy>

-                               <width>40</width>

-                               <height>40</height>

+                               <posy>10</posy>

+                               <width>20</width>

+                               <height>20</height>

                                <label>-</label>

                                
<texturefocus>control_buttons/tbTransportBack_sel.png</texturefocus>

                                
<texturenofocus>control_buttons/tbTransportBack.png</texturenofocus>

@@ -172,9 +172,9 @@
                                <control type="button" id="601">

                                        <description>Play Button</description>

                                        <posx>40</posx>

-                                       <posy>0</posy>

-                                       <width>40</width>

-                                       <height>40</height>

+                                       <posy>10</posy>

+                                       <width>20</width>

+                                       <height>20</height>

                                        <label>-</label>

                                        
<texturefocus>control_buttons/tbPlay_sel.png</texturefocus>

                                        
<texturenofocus>control_buttons/tbPlay.png</texturenofocus>

@@ -187,9 +187,9 @@
                                <control type="button" id="602">

                                        <description>Pause Button</description>

                                        <posx>40</posx>

-                                       <posy>0</posy>

-                                       <width>40</width>

-                                       <height>40</height>

+                                       <posy>10</posy>

+                                       <width>20</width>

+                                       <height>20</height>

                                        <label>-</label>

                                        
<texturefocus>control_buttons/tbPause_sel.png</texturefocus>

                                        
<texturenofocus>control_buttons/tbPause.png</texturenofocus>

@@ -203,9 +203,9 @@
                        <control type="button" id="603">

                                <description>Stop Button</description>

                                <posx>80</posx>

-                               <posy>0</posy>

-                               <width>40</width>

-                               <height>40</height>

+                               <posy>10</posy>

+                               <width>20</width>

+                               <height>20</height>

                                <label>-</label>

                                
<texturefocus>control_buttons/tbStop_sel.png</texturefocus>

                                
<texturenofocus>control_buttons/tbStop.png</texturenofocus>

@@ -217,9 +217,9 @@
                        <control type="button" id="604">

                                <description>Next Button</description>

                                <posx>120</posx>

-                               <posy>0</posy>

-                               <width>40</width>

-                               <height>40</height>

+                               <posy>10</posy>

+                               <width>20</width>

+                               <height>20</height>

                                <label>-</label>

                                
<texturefocus>control_buttons/tbTransportForward_sel.png</texturefocus>

                                
<texturenofocus>control_buttons/tbTransportForward.png</texturenofocus>

@@ -234,11 +234,11 @@
                                        <description>Repeat Button</description>

                                        <posx>190</posx>

                                        <posy>10</posy>

-                                       <width>20</width>

+                                       <width>25</width>

                                        <height>20</height>

                                        <label>-</label>

-                                       
<texturefocus>control_buttons/[email protected]</texturefocus>

-                                       
<texturenofocus>control_buttons/[email protected]</texturenofocus>

+                                       
<texturefocus>control_buttons/tbRepeat_sel.png</texturefocus>

+                                       
<texturenofocus>control_buttons/tbRepeat.png</texturenofocus>

                                        <onleft>604</onleft>

                                        <onright>103</onright>

                                        <onup>900</onup>

@@ -248,11 +248,11 @@
                                        <description>Repeat Button 
Enabled</description>

                                        <posx>190</posx>

                                        <posy>10</posy>

-                                       <width>20</width>

+                                       <width>25</width>

                                        <height>20</height>

                                        <label>-</label>

-                                       
<texturefocus>control_buttons/[email protected]</texturefocus>

-                                       
<texturenofocus>control_buttons/[email protected]</texturenofocus>

+                                       
<texturefocus>control_buttons/tbRepeatActive_sel.png</texturefocus>

+                                       
<texturenofocus>control_buttons/tbRepeatActive.png</texturenofocus>

                                        <onleft>604</onleft>

                                        <onright>103</onright>

                                        <onup>900</onup>

@@ -266,11 +266,11 @@
                                        <description>Random Button</description>

                                        <posx>220</posx>

                                        <posy>10</posy>

-                                       <width>20</width>

+                                       <width>25</width>

                                        <height>20</height>

                                        <label>-</label>

-                                       
<texturefocus>control_buttons/[email protected]</texturefocus>

-                                       
<texturenofocus>control_buttons/[email protected]</texturenofocus>

+                                       
<texturefocus>control_buttons/tbShuffle_sel.png</texturefocus>

+                                       
<texturenofocus>control_buttons/tbShuffle.png</texturenofocus>

                                        <onleft>104</onleft>

                                        <onright>105</onright>

                                        <onup>900</onup>

@@ -280,11 +280,11 @@
                                        <description>Random Button 
Enabled</description>

                                        <posx>220</posx>

                                        <posy>10</posy>

-                                       <width>20</width>

+                                       <width>25</width>

                                        <height>20</height>

                                        <label>-</label>

-                                       
<texturefocus>control_buttons/[email protected]</texturefocus>

-                                       
<texturenofocus>control_buttons/[email protected]</texturenofocus>

+                                       
<texturefocus>control_buttons/tbShuffleActive_sel.png</texturefocus>

+                                       
<texturenofocus>control_buttons/tbShuffleActive.png</texturenofocus>

                                        <onleft>104</onleft>

                                        <onright>105</onright>

                                        <onup>900</onup>

@@ -298,11 +298,11 @@
                                        <description>Crossfade 
Button</description>

                                        <posx>250</posx>

                                        <posy>10</posy>

-                                       <width>20</width>

+                                       <width>25</width>

                                        <height>20</height>

                                        <label>-</label>

-                                       
<texturefocus>control_buttons/[email protected]</texturefocus>

-                                       
<texturenofocus>control_buttons/[email protected]</texturenofocus>

+                                       
<texturefocus>control_buttons/tbCrossfade_sel.png</texturefocus>

+                                       
<texturenofocus>control_buttons/tbCrossfade.png</texturenofocus>

                                        <onleft>103</onleft>

                                        <onright>102</onright>

                                        <onup>900</onup>

@@ -312,11 +312,11 @@
                                        <description>Crossfade Button 
Enabled</description>

                                        <posx>250</posx>

                                        <posy>10</posy>

-                                       <width>20</width>

+                                       <width>25</width>

                                        <height>20</height>

                                        <label>-</label>

-                                       
<texturefocus>control_buttons/[email protected]</texturefocus>

-                                       
<texturenofocus>control_buttons/[email protected]</texturenofocus>

+                                       
<texturefocus>control_buttons/tbCrossfadeActive_sel.png</texturefocus>

+                                       
<texturenofocus>control_buttons/tbCrossfadeActive.png</texturenofocus>

                                        <onleft>103</onleft>

                                        <onright>102</onright>

                                        <onup>900</onup>

@@ -328,13 +328,13 @@
                        <control type="group" id="102">

                                <control type="button" id="620">

                                        <description>Sound Volume 
Button</description>

-                                       <posx>290</posx>

-                                       <posy>0</posy>

-                                       <width>40</width>

-                                       <height>40</height>

+                                       <posx>310</posx>

+                                       <posy>10</posy>

+                                       <width>20</width>

+                                       <height>20</height>

                                        <label>-</label>

-                                       
<texturefocus>control_buttons/tbUnMute_sel.png</texturefocus>

-                                       
<texturenofocus>control_buttons/tbMute.png</texturenofocus>

+                                       
<texturefocus>control_buttons/tbVolume_sel.png</texturefocus>

+                                       
<texturenofocus>control_buttons/tbVolume.png</texturenofocus>

                                        <onleft>105</onleft>

                                        <onright>600</onright>

                                        <onup>900</onup>

@@ -343,13 +343,13 @@
                                </control>

                                <control type="button" id="621">

                                        <description>Sound Mute 
Button</description>

-                                       <posx>290</posx>

-                                       <posy>0</posy>

-                                       <width>40</width>

-                                       <height>40</height>

+                                       <posx>310</posx>

+                                       <posy>10</posy>

+                                       <width>20</width>

+                                       <height>20</height>

                                        <label>-</label>

-                                       
<texturefocus>control_buttons/tbMute_sel.png</texturefocus>

-                                       
<texturenofocus>control_buttons/tbMute_on.png</texturenofocus>

+                                       
<texturefocus>control_buttons/tbVolumeMute_sel.png</texturefocus>

+                                       
<texturenofocus>control_buttons/tbVolumeMute.png</texturenofocus>

                                        <onleft>105</onleft>

                                        <onright>600</onright>

                                        <onup>900</onup>

@@ -360,14 +360,14 @@
                        <control type="slider" id="622">

                                <description>Volume Slider</description>

                                <posx>330</posx>

-                               <posy>10</posy>

+                               <posy>12</posy>

                                <width>120</width>

-                               <height>20</height>

+                               <height>15</height>

                                <texturefocus>-</texturefocus>

                                <texturenofocus>-</texturenofocus>

-                               
<texturesliderbar>control_buttons/snAutoBtn.png</texturesliderbar>

-                               
<textureslidernib>control_buttons/tbVolumeScrubber_dis.png</textureslidernib>

-                               
<textureslidernibfocus>control_buttons/tbVolumeScrubber.png</textureslidernibfocus>

+                               
<texturesliderbar>control_buttons/volumeSliderbar.png</texturesliderbar>

+                               
<textureslidernib>control_buttons/tbVolumeScrubber.png</textureslidernib>

+                               
<textureslidernibfocus>control_buttons/tbVolumeScrubber_sel.png</textureslidernibfocus>

                                <onup>100</onup>

                                <ondown>900</ondown>

                        </control>

diff --git 
a/script.sonos/resources/skins/Default/media/control_buttons/tbPause.png 
b/script.sonos/resources/skins/Default/media/control_buttons/tbPause.png
index babed29..07a643a 100644
Binary files 
a/script.sonos/resources/skins/Default/media/control_buttons/tbPause.png and 
b/script.sonos/resources/skins/Default/media/control_buttons/tbPause.png differ
diff --git 
a/script.sonos/resources/skins/Default/media/control_buttons/tbPause_sel.png 
b/script.sonos/resources/skins/Default/media/control_buttons/tbPause_sel.png
index 60e709d..274948c 100644
Binary files 
a/script.sonos/resources/skins/Default/media/control_buttons/tbPause_sel.png 
and 
b/script.sonos/resources/skins/Default/media/control_buttons/tbPause_sel.png 
differ
diff --git 
a/script.sonos/resources/skins/Default/media/control_buttons/tbPlay.png 
b/script.sonos/resources/skins/Default/media/control_buttons/tbPlay.png
index 98112a5..ba0b640 100644
Binary files 
a/script.sonos/resources/skins/Default/media/control_buttons/tbPlay.png and 
b/script.sonos/resources/skins/Default/media/control_buttons/tbPlay.png differ
diff --git 
a/script.sonos/resources/skins/Default/media/control_buttons/tbPlay_sel.png 
b/script.sonos/resources/skins/Default/media/control_buttons/tbPlay_sel.png
index b80ebc8..117309d 100644
Binary files 
a/script.sonos/resources/skins/Default/media/control_buttons/tbPlay_sel.png and 
b/script.sonos/resources/skins/Default/media/control_buttons/tbPlay_sel.png 
differ
diff --git 
a/script.sonos/resources/skins/Default/media/control_buttons/tbStop.png 
b/script.sonos/resources/skins/Default/media/control_buttons/tbStop.png
index 0e59e23..45c2cee 100644
Binary files 
a/script.sonos/resources/skins/Default/media/control_buttons/tbStop.png and 
b/script.sonos/resources/skins/Default/media/control_buttons/tbStop.png differ
diff --git 
a/script.sonos/resources/skins/Default/media/control_buttons/tbStop_sel.png 
b/script.sonos/resources/skins/Default/media/control_buttons/tbStop_sel.png
index b5f3e7c..4cab3b4 100644
Binary files 
a/script.sonos/resources/skins/Default/media/control_buttons/tbStop_sel.png and 
b/script.sonos/resources/skins/Default/media/control_buttons/tbStop_sel.png 
differ
diff --git 
a/script.sonos/resources/skins/Default/media/control_buttons/tbTransportBack.png
 
b/script.sonos/resources/skins/Default/media/control_buttons/tbTransportBack.png
index 5fd274e..fb431ab 100644
Binary files 
a/script.sonos/resources/skins/Default/media/control_buttons/tbTransportBack.png
 and 
b/script.sonos/resources/skins/Default/media/control_buttons/tbTransportBack.png
 differ
diff --git 
a/script.sonos/resources/skins/Default/media/control_buttons/tbTransportBack_sel.png
 
b/script.sonos/resources/skins/Default/media/control_buttons/tbTransportBack_sel.png
index 11120f7..504685a 100644
Binary files 
a/script.sonos/resources/skins/Default/media/control_buttons/tbTransportBack_sel.png
 and 
b/script.sonos/resources/skins/Default/media/control_buttons/tbTransportBack_sel.png
 differ
diff --git 
a/script.sonos/resources/skins/Default/media/control_buttons/tbTransportForward.png
 
b/script.sonos/resources/skins/Default/media/control_buttons/tbTransportForward.png
index 9c31f81..7a4ba50 100644
Binary files 
a/script.sonos/resources/skins/Default/media/control_buttons/tbTransportForward.png
 and 
b/script.sonos/resources/skins/Default/media/control_buttons/tbTransportForward.png
 differ
diff --git 
a/script.sonos/resources/skins/Default/media/control_buttons/tbTransportForward_sel.png
 
b/script.sonos/resources/skins/Default/media/control_buttons/tbTransportForward_sel.png
index e52f67e..29440b2 100644
Binary files 
a/script.sonos/resources/skins/Default/media/control_buttons/tbTransportForward_sel.png
 and 
b/script.sonos/resources/skins/Default/media/control_buttons/tbTransportForward_sel.png
 differ
diff --git 
a/script.sonos/resources/skins/Default/media/control_buttons/tbVolumeScrubber.png
 
b/script.sonos/resources/skins/Default/media/control_buttons/tbVolumeScrubber.png
index 0fb0e80..b885e5e 100644
Binary files 
a/script.sonos/resources/skins/Default/media/control_buttons/tbVolumeScrubber.png
 and 
b/script.sonos/resources/skins/Default/media/control_buttons/tbVolumeScrubber.png
 differ
diff --git a/script.sonos/service.py b/script.sonos/service.py
index 41bdfec..18afe68 100644
--- a/script.sonos/service.py
+++ b/script.sonos/service.py
@@ -332,7 +332,7 @@ if __name__ == '__main__':
                 if (timeUntilNextCheck < 1) and 
Settings.isNotificationEnabled():
                     if Settings.stopNotifIfVideoPlaying() and 
xbmc.Player().isPlayingVideo():
                         log("SonosService: Video Playing, Skipping 
Notification Display")
-                    elif 
xbmcgui.Window(10000).getProperty("SonosControllerShowing") == 'true':
+                    elif Settings.stopNotifIfControllerShowing() and 
(xbmcgui.Window(10000).getProperty("SonosControllerShowing") == 'true'):
                         log("SonosService: Sonos Controller Showing, Skipping 
Notification Display")
                         # Reset the "just started" flag to ensure that when we 
exit it does not
                         # show the notification immediately

-----------------------------------------------------------------------

Summary of changes:
 script.sonos/.project                              |   17 +
 script.sonos/.pydevproject                         |    5 +
 script.sonos/addon.xml                             |    2 +-
 script.sonos/changelog.txt                         |    7 +
 script.sonos/default.py                            |    2 +-
 script.sonos/discovery.py                          |   73 +-
 script.sonos/plugin.py                             |   87 ++-
 script.sonos/resources/language/English/strings.po |   36 +-
 script.sonos/resources/lib/settings.py             |   15 +
 script.sonos/resources/lib/soco/__init__.py        |    2 +-
 script.sonos/resources/lib/soco/alarms.py          |  315 +++++++++
 script.sonos/resources/lib/soco/cache.py           |  165 +++++
 script.sonos/resources/lib/soco/compat.py          |    2 +-
 script.sonos/resources/lib/soco/config.py          |   22 +
 script.sonos/resources/lib/soco/core.py            |  174 ++++--
 script.sonos/resources/lib/soco/data_structures.py |  308 ++++------
 script.sonos/resources/lib/soco/events.py          |   82 +++-
 script.sonos/resources/lib/soco/services.py        |   20 +-
 script.sonos/resources/lib/soco/utils.py           |  162 ++---
 script.sonos/resources/lib/sonos.py                |   34 +-
 script.sonos/resources/media/[email protected]       |  Bin 5074 -> 0 bytes
 script.sonos/resources/media/[email protected]          |  Bin 3290 -> 0 bytes
 script.sonos/resources/media/albums.png            |  Bin 0 -> 4062 bytes
 script.sonos/resources/media/artists.png           |  Bin 0 -> 3630 bytes
 script.sonos/resources/media/composers.png         |  Bin 0 -> 3237 bytes
 script.sonos/resources/media/genres.png            |  Bin 0 -> 5366 bytes
 script.sonos/resources/media/library.png           |  Bin 0 -> 4344 bytes
 script.sonos/resources/media/playlist.png          |  Bin 0 -> 3982 bytes
 script.sonos/resources/media/radio.png             |  Bin 0 -> 7537 bytes
 script.sonos/resources/media/radiostation.png      |  Bin 0 -> 4139 bytes
 script.sonos/resources/media/[email protected] |  Bin 4317 -> 0 bytes
 script.sonos/resources/media/sonosplaylist.png     |  Bin 0 -> 5133 bytes
 script.sonos/resources/media/tracks.png            |  Bin 0 -> 3674 bytes
 script.sonos/resources/settings.xml                |   18 +-
 .../720p/script-sonos-artist-slideshow-1.xml       |  715 ++++++++++++++++++++
 .../720p/script-sonos-artist-slideshow-2.xml       |  437 ++++++++++++
 .../720p/script-sonos-artist-slideshow-3.xml       |  420 ++++++++++++
 .../Default/720p/script-sonos-artist-slideshow.xml |  713 -------------------
 .../skins/Default/720p/script-sonos-controller.xml |  100 ++--
 .../Default/media/control_buttons/snAutoBtn.png    |  Bin 630 -> 0 bytes
 .../Default/media/control_buttons/tbCrossfade.png  |  Bin 0 -> 4169 bytes
 .../media/control_buttons/tbCrossfadeActive.png    |  Bin 0 -> 3993 bytes
 .../control_buttons/tbCrossfadeActive_sel.png      |  Bin 0 -> 4000 bytes
 .../media/control_buttons/[email protected]    |  Bin 1664 -> 0 bytes
 .../control_buttons/[email protected]      |  Bin 1260 -> 0 bytes
 .../[email protected]            |  Bin 3936 -> 0 bytes
 .../[email protected]                |  Bin 4407 -> 0 bytes
 .../media/control_buttons/tbCrossfade_sel.png      |  Bin 0 -> 3571 bytes
 .../skins/Default/media/control_buttons/tbMute.png |  Bin 1751 -> 0 bytes
 .../Default/media/control_buttons/tbMute_on.png    |  Bin 1812 -> 0 bytes
 .../Default/media/control_buttons/tbMute_sel.png   |  Bin 3904 -> 0 bytes
 .../Default/media/control_buttons/tbPause.png      |  Bin 1881 -> 1152 bytes
 .../Default/media/control_buttons/tbPause_sel.png  |  Bin 4518 -> 1147 bytes
 .../skins/Default/media/control_buttons/tbPlay.png |  Bin 2042 -> 1670 bytes
 .../Default/media/control_buttons/tbPlay_sel.png   |  Bin 4651 -> 1345 bytes
 .../Default/media/control_buttons/tbRepeat.png     |  Bin 0 -> 3211 bytes
 .../media/control_buttons/tbRepeatActive.png       |  Bin 0 -> 2030 bytes
 .../media/control_buttons/tbRepeatActive_sel.png   |  Bin 0 -> 3950 bytes
 .../media/control_buttons/[email protected]       |  Bin 1331 -> 0 bytes
 .../media/control_buttons/[email protected]   |  Bin 1019 -> 0 bytes
 .../[email protected]               |  Bin 3643 -> 0 bytes
 .../control_buttons/[email protected]   |  Bin 4031 -> 0 bytes
 .../Default/media/control_buttons/tbRepeat_sel.png |  Bin 0 -> 2057 bytes
 .../Default/media/control_buttons/tbShuffle.png    |  Bin 0 -> 3787 bytes
 .../media/control_buttons/tbShuffleActive.png      |  Bin 0 -> 2165 bytes
 .../media/control_buttons/tbShuffleActive_sel.png  |  Bin 0 -> 4135 bytes
 .../media/control_buttons/[email protected]      |  Bin 2085 -> 0 bytes
 .../media/control_buttons/[email protected]  |  Bin 1572 -> 0 bytes
 .../[email protected]              |  Bin 4313 -> 0 bytes
 .../control_buttons/[email protected]  |  Bin 4954 -> 0 bytes
 .../media/control_buttons/tbShuffle_sel.png        |  Bin 0 -> 2299 bytes
 .../skins/Default/media/control_buttons/tbStop.png |  Bin 1848 -> 1071 bytes
 .../Default/media/control_buttons/tbStop_sel.png   |  Bin 4465 -> 1027 bytes
 .../media/control_buttons/tbTransportBack.png      |  Bin 1597 -> 1888 bytes
 .../media/control_buttons/tbTransportBack_sel.png  |  Bin 3711 -> 1508 bytes
 .../media/control_buttons/tbTransportForward.png   |  Bin 1598 -> 1938 bytes
 .../control_buttons/tbTransportForward_sel.png     |  Bin 3709 -> 1506 bytes
 .../Default/media/control_buttons/tbUnMute_sel.png |  Bin 3866 -> 0 bytes
 .../Default/media/control_buttons/tbVolume.png     |  Bin 0 -> 1232 bytes
 .../Default/media/control_buttons/tbVolumeMute.png |  Bin 0 -> 1914 bytes
 .../media/control_buttons/tbVolumeMute_sel.png     |  Bin 0 -> 3498 bytes
 .../media/control_buttons/tbVolumeScrubber.png     |  Bin 1807 -> 3374 bytes
 .../media/control_buttons/tbVolumeScrubber_dis.png |  Bin 1368 -> 0 bytes
 .../media/control_buttons/tbVolumeScrubber_sel.png |  Bin 0 -> 3221 bytes
 .../Default/media/control_buttons/tbVolume_sel.png |  Bin 0 -> 1228 bytes
 .../media/control_buttons/volumeSliderbar.png      |  Bin 0 -> 2849 bytes
 script.sonos/service.py                            |    2 +-
 87 files changed, 2740 insertions(+), 1195 deletions(-)
 create mode 100644 script.sonos/.project
 create mode 100644 script.sonos/.pydevproject
 create mode 100644 script.sonos/resources/lib/soco/alarms.py
 create mode 100644 script.sonos/resources/lib/soco/cache.py
 create mode 100644 script.sonos/resources/lib/soco/config.py
 delete mode 100644 script.sonos/resources/media/[email protected]
 delete mode 100644 script.sonos/resources/media/[email protected]
 create mode 100644 script.sonos/resources/media/albums.png
 create mode 100644 script.sonos/resources/media/artists.png
 create mode 100644 script.sonos/resources/media/composers.png
 create mode 100644 script.sonos/resources/media/genres.png
 create mode 100644 script.sonos/resources/media/library.png
 create mode 100644 script.sonos/resources/media/playlist.png
 create mode 100644 script.sonos/resources/media/radio.png
 create mode 100644 script.sonos/resources/media/radiostation.png
 delete mode 100644 script.sonos/resources/media/[email protected]
 create mode 100644 script.sonos/resources/media/sonosplaylist.png
 create mode 100644 script.sonos/resources/media/tracks.png
 create mode 100644 
script.sonos/resources/skins/Default/720p/script-sonos-artist-slideshow-1.xml
 create mode 100644 
script.sonos/resources/skins/Default/720p/script-sonos-artist-slideshow-2.xml
 create mode 100644 
script.sonos/resources/skins/Default/720p/script-sonos-artist-slideshow-3.xml
 delete mode 100644 
script.sonos/resources/skins/Default/720p/script-sonos-artist-slideshow.xml
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/snAutoBtn.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbCrossfade.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbCrossfadeActive.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbCrossfadeActive_sel.png
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/[email protected]
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/[email protected]
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/[email protected]
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/[email protected]
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbCrossfade_sel.png
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbMute.png
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbMute_on.png
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbMute_sel.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbRepeat.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbRepeatActive.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbRepeatActive_sel.png
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/[email protected]
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/[email protected]
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/[email protected]
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/[email protected]
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbRepeat_sel.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbShuffle.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbShuffleActive.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbShuffleActive_sel.png
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/[email protected]
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/[email protected]
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/[email protected]
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/[email protected]
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbShuffle_sel.png
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbUnMute_sel.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbVolume.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbVolumeMute.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbVolumeMute_sel.png
 delete mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbVolumeScrubber_dis.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbVolumeScrubber_sel.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/tbVolume_sel.png
 create mode 100644 
script.sonos/resources/skins/Default/media/control_buttons/volumeSliderbar.png


hooks/post-receive
-- 
Scripts

------------------------------------------------------------------------------
Slashdot TV.  
Video for Nerds.  Stuff that matters.
http://tv.slashdot.org/
_______________________________________________
Xbmc-addons mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/xbmc-addons

Reply via email to