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