def test_add_item_to_sonos_playlist(self, moco): moco.contentDirectory.reset_mock() playlist = mock.Mock() playlist.item_id = 7 track = mock.Mock() track.uri = 'fake_uri' track.didl_metadata = XML.Element('a') update_id = 100 moco.contentDirectory.Browse.return_value = { 'NumberReturned': '0', 'Result': '<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"></DIDL-Lite>', 'TotalMatches': '0', 'UpdateID': update_id } moco.add_item_to_sonos_playlist(track, playlist) moco.contentDirectory.Browse.assert_called_once_with([ ('ObjectID', playlist.item_id), ('BrowseFlag', 'BrowseDirectChildren'), ('Filter', '*'), ('StartingIndex', 0), ('RequestedCount', 1), ('SortCriteria', '') ]) moco.avTransport.AddURIToSavedQueue.assert_called_once_with([ ('InstanceID', 0), ('UpdateID', update_id), ('ObjectID', playlist.item_id), ('EnqueuedURI', track.uri), ('EnqueuedURIMetaData', XML.tostring(track.didl_metadata)), ('AddAtIndex', 4294967295) ])
def common_tests(class_, xml_, dict_, parent_id, helpers): """Common tests for the MS classes.""" xml_content = XML.fromstring(xml_.encode("utf8")) # MusicServiceItem.from_xml and MusicServiceItem.to_dict item_from_xml = class_.from_xml(xml_content, FAKE_MUSIC_SERVICE, parent_id) assert item_from_xml.to_dict == dict_ # MusicServiceItem.from_dict and MusicServiceItem.to_dict item_from_dict = class_.from_dict(dict_) assert item_from_dict.to_dict == dict_ # MusicServiceItem.didl_metadata # NOTE! These tests is reliant on the attributes being put in the same # order by ElementTree and so it might fail if that changes if item_from_xml.can_play: dict_encoded = {} for key, value in dict_.items(): try: is_str = isinstance(value, unicode) except NameError: is_str = isinstance(value, str) if is_str: dict_encoded[key] = (escape(value).encode( "ascii", "xmlcharrefreplace").decode("ascii")) else: dict_encoded[key] = value didl = DIDL_TEMPLATE.format(item_class=class_.item_class, **dict_encoded) assert helpers.compare_xml(item_from_xml.didl_metadata, XML.fromstring(didl)) assert helpers.compare_xml(item_from_dict.didl_metadata, XML.fromstring(didl)) else: with pytest.raises(DIDLMetadataError): # pylint: disable=pointless-statement item_from_xml.didl_metadata # Text attributes with mandatory content for name in ["item_id", "extended_id", "title", "service_id"]: getter_attributes_test(name, item_from_xml, item_from_dict, dict_[name]) # Text attributes with voluntary content for name in ["parent_id", "album_art_uri"]: getter_attributes_test(name, item_from_xml, item_from_dict, dict_.get(name)) # Boolean attribute getter_attributes_test("can_play", item_from_xml, item_from_dict, bool(dict_.get("can_play"))) return item_from_xml, item_from_dict
def common_tests(class_, xml_, dict_, parent_id): """Common tests for the MS classes.""" xml_content = XML.fromstring(xml_.encode('utf8')) # MusicServiceItem.from_xml and MusicServiceItem.to_dict item_from_xml = class_.from_xml(xml_content, FAKE_MUSIC_SERVICE, parent_id) assert item_from_xml.to_dict == dict_ # MusicServiceItem.from_dict and MusicServiceItem.to_dict item_from_dict = class_.from_dict(dict_) assert item_from_dict.to_dict == dict_ # MusicServiceItem.didl_metadata # NOTE! These tests is reliant on the attributes being put in the same # order by ElementTree and so it might fail if that changes if item_from_xml.can_play: dict_encoded = {} for key, value in dict_.items(): try: is_str = isinstance(value, unicode) except NameError: is_str = isinstance(value, str) if is_str: dict_encoded[key] = escape(value).\ encode('ascii', 'xmlcharrefreplace').decode('ascii') else: dict_encoded[key] = value didl = DIDL_TEMPLATE.format(item_class=class_.item_class, **dict_encoded) assert XML.tostring(item_from_xml.didl_metadata).decode('ascii') == \ didl assert XML.tostring(item_from_dict.didl_metadata).decode('ascii') == \ didl else: with pytest.raises(DIDLMetadataError): # pylint: disable=pointless-statement item_from_xml.didl_metadata # Text attributes with mandatory content for name in ['item_id', 'extended_id', 'title', 'service_id']: getter_attributes_test(name, item_from_xml, item_from_dict, dict_[name]) # Text attributes with voluntary content for name in ['parent_id', 'album_art_uri']: getter_attributes_test(name, item_from_xml, item_from_dict, dict_.get(name)) # Boolean attribute getter_attributes_test('can_play', item_from_xml, item_from_dict, bool(dict_.get('can_play'))) return item_from_xml, item_from_dict
def common_tests(class_, xml_, dict_, parent_id): """Common tests for the MS classes""" xml_content = XML.fromstring(xml_.encode('utf8')) # MusicServiceItem.from_xml and MusicServiceItem.to_dict item_from_xml = class_.from_xml(xml_content, FAKE_MUSIC_SERVICE, parent_id) assert item_from_xml.to_dict == dict_ # MusicServiceItem.from_dict and MusicServiceItem.to_dict item_from_dict = class_.from_dict(dict_) assert item_from_dict.to_dict == dict_ # MusicServiceItem.didl_metadata # NOTE! These tests is reliant on the attributes being put in the same # order by ElementTree and so it might fail if that changes if item_from_xml.can_play: dict_encoded = {} for key, value in dict_.items(): try: is_str = isinstance(value, unicode) except NameError: is_str = isinstance(value, str) if is_str: dict_encoded[key] = escape(value).\ encode('ascii', 'xmlcharrefreplace').decode('ascii') else: dict_encoded[key] = value didl = DIDL_TEMPLATE.format(item_class=class_.item_class, **dict_encoded) assert XML.tostring(item_from_xml.didl_metadata).decode('ascii') == \ didl assert XML.tostring(item_from_dict.didl_metadata).decode('ascii') == \ didl else: with pytest.raises(CannotCreateDIDLMetadata): # pylint: disable=pointless-statement item_from_xml.didl_metadata # Text attributes with mandatory content for name in ['item_id', 'extended_id', 'title', 'service_id']: getter_attributes_test(name, item_from_xml, item_from_dict, dict_[name]) # Text attributes with voluntary content for name in ['parent_id', 'album_art_uri']: getter_attributes_test(name, item_from_xml, item_from_dict, dict_.get(name)) # Boolean attribute getter_attributes_test('can_play', item_from_xml, item_from_dict, bool(dict_.get('can_play'))) return item_from_xml, item_from_dict
def test_call(): """Calling a command should result in an http request.""" s = SoapMessage( endpoint='http://endpoint.example.com', method='getData', parameters=[('one', '1')], http_headers={'user-agent': 'sonos'}, soap_action='ACTION', soap_header="<a_header>data</a_header>", namespace="http://namespace.com", other_arg=4) response = mock.MagicMock() response.headers = {} response.status_code = 200 response.content = DUMMY_VALID_RESPONSE with mock.patch('requests.post', return_value=response) as fake_post: result = s.call() assert XML.tostring(result) fake_post.assert_called_once_with( 'http://endpoint.example.com', headers={'SOAPACTION': '"ACTION"', 'Content-Type': 'text/xml; charset="utf-8"', 'user-agent': 'sonos'}, data=mock.ANY, other_arg=4)
def test_didl_object_from_wrong_element(self): # Using the wrong element elt = XML.fromstring("""<res>URI</res>""") with pytest.raises(DIDLMetadataError) as excinfo: didl_object = data_structures.DidlObject.from_element(elt) assert "Wrong element. Expected <item> or <container>, " "got <res> for class object" in str(excinfo.value)
def test_call(): """Calling a command should result in an http request.""" s = SoapMessage( endpoint="http://endpoint.example.com", method="getData", parameters=[("one", "1")], http_headers={"user-agent": "sonos"}, soap_action="ACTION", soap_header="<a_header>data</a_header>", namespace="http://namespace.com", other_arg=4, ) response = mock.MagicMock() response.headers = {} response.status_code = 200 response.content = DUMMY_VALID_RESPONSE with mock.patch("requests.post", return_value=response) as fake_post: result = s.call() assert XML.tostring(result) fake_post.assert_called_once_with( "http://endpoint.example.com", headers={"SOAPACTION": '"ACTION"', "Content-Type": 'text/xml; charset="utf-8"', "user-agent": "sonos"}, data=mock.ANY, other_arg=4, )
def test_call(): """Calling a command should result in an http request.""" s = SoapMessage( endpoint="http://endpoint.example.com", method="getData", parameters=[("one", "1")], http_headers={"user-agent": "sonos"}, soap_action="ACTION", soap_header="<a_header>data</a_header>", namespace="http://namespace.com", other_arg=4, ) response = mock.MagicMock() response.headers = {} response.status_code = 200 response.content = DUMMY_VALID_RESPONSE with mock.patch("requests.post", return_value=response) as fake_post: result = s.call() assert XML.tostring(result) fake_post.assert_called_once_with( "http://endpoint.example.com", headers={ "SOAPACTION": '"ACTION"', "Content-Type": 'text/xml; charset="utf-8"', "user-agent": "sonos", }, data=mock.ANY, other_arg=4, )
def call(self, method, args=None): http_headers = { 'Accept-Encoding': 'gzip, deflate', 'User-Agent': 'Linux UPnP/1.0 Sonos/26.99-12345' } message = SoapMessage(endpoint=self.endpoint, method=method, parameters=[] if args is None else args, http_headers=http_headers, soap_action="http://www.sonos.com/Services/1" ".1#{0}".format(method), soap_header=self.get_soap_header(), namespace='http://www.sonos.com/Services/1.1', timeout=self.timeout) try: result_elt = message.call() except SoapFault as exc: raise MusicServiceException(exc.faultstring, exc.faultcode) result = list( parse(XML.tostring(result_elt), process_namespaces=True, namespaces={ 'http://www.sonos.com/Services/1.1': None }).values())[0] return result if result is not None else {}
def test_call(): """ Calling a command should result in an http request""" s = SoapMessage( endpoint='http://endpoint.example.com', method='getData', parameters=[('one', '1')], http_headers={'user-agent': 'sonos'}, soap_action='ACTION', soap_header="<a_header>data</a_header>", namespace="http://namespace.com", other_arg=4) response = mock.MagicMock() response.headers = {} response.status_code = 200 response.content = DUMMY_VALID_RESPONSE with mock.patch('requests.post', return_value=response) as fake_post: result = s.call() assert XML.tostring(result) fake_post.assert_called_once_with( 'http://endpoint.example.com', headers={'SOAPACTION': '"ACTION"', 'Content-Type': 'text/xml; charset="utf-8"', 'user-agent': 'sonos'}, data=mock.ANY, other_arg=4)
def test_didl_object_from_wrong_element(self): # Using the wrong element elt = XML.fromstring("""<res>URI</res>""") with pytest.raises(DIDLMetadataError) as excinfo: didl_object = data_structures.DidlObject.from_element(elt) assert "Wrong element. Expected '<item>', got '<res>'" in str( excinfo.value)
def test_didl_object_to_element(self): didl_object = data_structures.DidlObject(title='a_title', parent_id='pid', item_id='iid', creator='a_creator') # we seem to have to go through this to get ElementTree to deal # with namespaces properly! elt = XML.fromstring(XML.tostring(didl_object.to_element(True))) elt2 = XML.fromstring('<dummy xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" ' + 'xmlns:dc="http://purl.org/dc/elements/1.1/" ' + 'xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">' + '<item id="iid" parentID="pid" restricted="true">' + '<dc:title>a_title</dc:title>' + '<dc:creator>a_creator</dc:creator>' + '<upnp:class>object</upnp:class><desc id="cdudn" '+ 'nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">' + 'RINCON_AssociatedZPUDN</desc></item></dummy>')[0] assert_xml_equal(elt2, elt)
def test_create_didl_resource_to_from_element(self): res = data_structures.DidlResource('a%20uri', 'a:protocol:info:xx', bitrate=3) elt = res.to_element() assert XML.tostring(elt) == ( b'<res bitrate="3" protocolInfo="a:protocol:info:xx">a%20uri</res>') assert data_structures.DidlResource.from_element(elt) == res
def test_didl_object_to_element(self): didl_object = data_structures.DidlObject( title='a_title', parent_id='pid', item_id='iid', creator='a_creator') # we seem to have to go through this to get ElementTree to deal # with namespaces properly! elt = XML.fromstring(XML.tostring(didl_object.to_element(True))) elt2 = XML.fromstring( '<dummy xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" ' + 'xmlns:dc="http://purl.org/dc/elements/1.1/" ' + 'xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">' + '<item id="iid" parentID="pid" restricted="true">' + '<dc:title>a_title</dc:title>' + '<dc:creator>a_creator</dc:creator>' + '<upnp:class>object</upnp:class><desc id="cdudn" ' + 'nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">' + 'RINCON_AssociatedZPUDN</desc></item></dummy>')[0] assert_xml_equal(elt2, elt)
def test_didl_object_from_element(self): elt = XML.fromstring(self.didl_xml) didl_object = data_structures.DidlObject.from_element(elt) assert didl_object.title == 'the_title' assert didl_object.parent_id == 'pid' assert didl_object.item_id == 'iid' assert didl_object.creator == 'a_creator' assert didl_object.desc == 'DUMMY' assert didl_object.item_class == 'object'
def test_didl_object_from_element_unoff_subelement(self): """Test that for a DidlObject created from an element with an unofficial .# specified sub class, that the sub class is completely ignored """ elt = XML.fromstring(self.didl_xml.replace("object", "object.#SubClass")) didl_object = data_structures.DidlObject.from_element(elt) assert didl_object.item_class == "object"
def test_didl_object_from_element(self): elt = XML.fromstring(self.didl_xml) didl_object = data_structures.DidlObject.from_element(elt) assert didl_object.title == "the_title" assert didl_object.parent_id == "pid" assert didl_object.item_id == "iid" assert didl_object.creator == "a_creator" assert didl_object.desc == "DUMMY" assert didl_object.item_class == "object"
def get_soap_header(self): """ Generate the SOAP authentication header for the related service. This header contains all the necessary authentication details. Returns: (str): A string representation of the XML content of the SOAP header """ # According to the SONOS SMAPI, this header must be sent with all # SOAP requests. Building this is an expensive operation (though # occasionally necessary), so f we have a cached value, return it if self._cached_soap_header is not None: return self._cached_soap_header music_service = self.music_service credentials_header = XML.Element( "credentials", {'xmlns': "http://www.sonos.com/Services/1.1"}) device_id = XML.SubElement(credentials_header, 'deviceId') device_id.text = self._device_id device_provider = XML.SubElement(credentials_header, 'deviceProvider') device_provider.text = 'Sonos' if music_service.account.oa_device_id: # OAuth account credentials are present. We must use them to # authenticate. login_token = XML.Element('loginToken') token = XML.SubElement(login_token, 'token') token.text = music_service.account.oa_device_id key = XML.SubElement(login_token, 'key') key.text = music_service.account.key household_id = XML.SubElement(login_token, 'householdId') household_id.text = self._device.household_id credentials_header.append(login_token) # otherwise, perhaps use DeviceLink or UserId auth elif music_service.auth_type in ['DeviceLink', 'UserId']: # We need a session ID from Sonos session_id = self._device.musicServices.GetSessionId([ ('ServiceId', music_service.service_id), ('Username', music_service.account.username) ])['SessionId'] session_elt = XML.Element('sessionId') session_elt.text = session_id credentials_header.append(session_elt) # Anonymous auth. No need for anything further. self._cached_soap_header = XML.tostring(credentials_header) return self._cached_soap_header
def test_create_didl_resource_to_from_element(self, helpers): res = data_structures.DidlResource('a%20uri', 'a:protocol:info:xx', bitrate=3) elt = res.to_element() assert helpers.compare_xml( elt, XML.fromstring(b'<res bitrate="3" ' b'protocolInfo="a:protocol:info:xx">a%20uri</res>')) assert data_structures.DidlResource.from_element(elt) == res
def call(self): """ Call the SOAP method on the server """ headers, data = self.prepare() # Check log level before logging XML, since prettifying it is # expensive if _LOG.isEnabledFor(logging.DEBUG): _LOG.debug("Sending %s, %s", headers, prettify(data)) response = requests.post( self.endpoint, headers=headers, data=data.encode('utf-8'), **self.request_args ) _LOG.debug("Received %s, %s", response.headers, response.text) status = response.status_code if status == 200: # The response is good. Extract the Body tree = XML.fromstring(response.content) # Get the first child of the <Body> tag. NB There should only be # one if the RPC standard is followed. body = tree.find( "{http://schemas.xmlsoap.org/soap/envelope/}Body")[0] return body elif status == 500: # We probably have a SOAP Fault tree = XML.fromstring(response.content) fault = tree.find( './/{http://schemas.xmlsoap.org/soap/envelope/}Fault' ) if fault is None: # Not a SOAP fault. Must be something else. response.raise_for_status() faultcode = fault.findtext("faultcode") faultstring = fault.findtext("faultstring") faultdetail = fault.find("detail") raise SoapFault(faultcode, faultstring, faultdetail) else: # Something else has gone wrong. Probably a network error. Let # Requests handle it response.raise_for_status()
def get_soap_header(self): if self._session_id is None: return '' if self._cached_soap_header is not None: return self._cached_soap_header credentials_header = XML.Element( "credentials", {'xmlns': "http://www.sonos.com/Services/1.1"}) if self.music_service.auth_type in ['UserId']: session_elt = XML.Element('sessionId') session_elt.text = self._session_id credentials_header.append(session_elt) else: raise Exception('unknown auth_type = ' + self.music_service.auth_type) self._cached_soap_header = XML.tostring( credentials_header, encoding='utf-8').decode(encoding='utf-8') return self._cached_soap_header
def call(self): """ Call the SOAP method on the server """ headers, data = self.prepare() # Check log level before logging XML, since prettifying it is # expensive if _LOG.isEnabledFor(logging.DEBUG): _LOG.debug("Sending %s, %s", headers, prettify(data)) response = requests.post(self.endpoint, headers=headers, data=data.encode('utf-8'), **self.request_args) _LOG.debug("Received %s, %s", response.headers, response.text) status = response.status_code if status == 200: # The response is good. Extract the Body tree = XML.fromstring(response.content) # Get the first child of the <Body> tag. NB There should only be # one if the RPC standard is followed. body = tree.find( "{http://schemas.xmlsoap.org/soap/envelope/}Body")[0] return body elif status == 500: # We probably have a SOAP Fault tree = XML.fromstring(response.content) fault = tree.find( './/{http://schemas.xmlsoap.org/soap/envelope/}Fault') if fault is None: # Not a SOAP fault. Must be something else. response.raise_for_status() faultcode = fault.findtext("faultcode") faultstring = fault.findtext("faultstring") faultdetail = fault.find("detail") raise SoapFault(faultcode, faultstring, faultdetail) else: # Something else has gone wrong. Probably a network error. Let # Requests handle it response.raise_for_status()
def __init__(self, faultcode, faultstring, detail=None): """ Args: faultcode (str): The SOAP faultcode faultstring (str): The SOAP faultstring detail (Element): The SOAP fault detail, as an ElementTree Element. Default, None """ self.faultcode = faultcode self.faultstring = faultstring self.detail = detail self.detail_string = XML.tostring(detail) if detail is not None else '' super(SoapFault, self).__init__(faultcode, faultstring)
def test_add_item_to_sonos_playlist(self, moco): playlist = mock.Mock() playlist.item_id = 7 track = mock.Mock() track.uri = 'fake_uri' track.didl_metadata = XML.Element('a') update_id = 100 moco._music_lib_search = mock.Mock(return_value=( {'UpdateID': update_id}, None)) moco.add_item_to_sonos_playlist(track, playlist) moco._music_lib_search.assert_called_once_with(playlist.item_id, 0, 1) moco.avTransport.AddURIToSavedQueue.assert_called_once_with( [('InstanceID', 0), ('UpdateID', update_id), ('ObjectID', playlist.item_id), ('EnqueuedURI', track.uri), ('EnqueuedURIMetaData', XML.tostring(track.didl_metadata)), ('AddAtIndex', 4294967295)] )
def _get_search_prefix_map(self): """Fetch and parse the service search category mapping. Standard Sonos search categories are 'all', 'artists', 'albums', 'tracks', 'playlists', 'genres', 'stations', 'tags'. Not all are available for each music service """ # TuneIn does not have a pmap. Its search keys are is search:station, # search:show, search:host # Presentation maps can also define custom categories. See eg # http://sonos-pmap.ws.sonos.com/hypemachine_pmap.6.xml # <SearchCategories> # ... # <CustomCategory mappedId="SBLG" stringId="Blogs"/> # </SearchCategories> # Is it already cached? If so, return it if self._search_prefix_map is not None: return self._search_prefix_map # Not cached. Fetch and parse presentation map self._search_prefix_map = {} # Tunein is a special case. It has no pmap, but supports searching if self.service_name == "TuneIn": self._search_prefix_map = { 'stations': 'search:station', 'shows': 'search:show', 'hosts': 'search:host', } return self._search_prefix_map if self.presentation_map_uri is None: # Assume not searchable? return self._search_prefix_map log.info('Fetching presentation map from %s', self.presentation_map_uri) pmap = requests.get(self.presentation_map_uri, timeout=9) pmap_root = XML.fromstring(pmap.content) # Search translations can appear in Category or CustomCategory elements categories = pmap_root.findall(".//SearchCategories/Category") if categories is None: return self._search_prefix_map for cat in categories: self._search_prefix_map[cat.get('id')] = cat.get('mappedId') custom_categories = pmap_root.findall( ".//SearchCategories/CustomCategory") for cat in custom_categories: self._search_prefix_map[cat.get('stringId')] = cat.get('mappedId') return self._search_prefix_map
def test_didl_object_from_wrong_class(self): # mismatched upnp class bad_elt1 = XML.fromstring(\ """<item xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" id="iid" parentID="pid" restricted="true"> <dc:title>the_title</dc:title> <upnp:class>object.item</upnp:class> <dc:creator>a_creator</dc:creator> <desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/"> RINCON_AssociatedZPUDN </desc> </item> """) with pytest.raises(DIDLMetadataError) as excinfo: didl_object = data_structures.DidlObject.from_element(bad_elt1) assert ("UPnP class is incorrect. Expected 'object', got 'object.item'" ) in str(excinfo.value)
def test_didl_object_from_wrong_class(self): # mismatched upnp class bad_elt1 = XML.fromstring( """<item xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" id="iid" parentID="pid" restricted="true"> <dc:title>the_title</dc:title> <upnp:class>object.item</upnp:class> <dc:creator>a_creator</dc:creator> <desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/"> RINCON_AssociatedZPUDN </desc> </item> """) with pytest.raises(DIDLMetadataError) as excinfo: didl_object = data_structures.DidlObject.from_element(bad_elt1) assert ("UPnP class is incorrect. Expected 'object', got 'object.item'" ) in str(excinfo.value)
def test_didl_object_from_element(self): elt = XML.fromstring( """<item xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" id="iid" parentID="pid" restricted="true"> <dc:title>the_title</dc:title> <upnp:class>object</upnp:class> <dc:creator>a_creator</dc:creator> <desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">DUMMY</desc> </item> """) didl_object = data_structures.DidlObject.from_element(elt) assert didl_object.title == 'the_title' assert didl_object.parent_id == 'pid' assert didl_object.item_id == 'iid' assert didl_object.creator == 'a_creator' assert didl_object.desc == 'DUMMY'
def test_add_item_to_sonos_playlist(self, moco): moco.contentDirectory.reset_mock() playlist = mock.Mock() playlist.item_id = 7 track = mock.Mock() track.uri = 'fake_uri' track.didl_metadata = XML.Element('a') update_id = 100 moco.contentDirectory.Browse.return_value = { 'NumberReturned': '0', 'Result': '<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"></DIDL-Lite>', 'TotalMatches': '0', 'UpdateID': update_id } moco.add_item_to_sonos_playlist(track, playlist) moco.contentDirectory.Browse.assert_called_once_with([ ('ObjectID', playlist.item_id), ('BrowseFlag', 'BrowseDirectChildren'), ('Filter', '*'), ('StartingIndex', 0), ('RequestedCount', 1), ('SortCriteria', '') ]) moco.avTransport.AddURIToSavedQueue.assert_called_once_with( [('InstanceID', 0), ('UpdateID', update_id), ('ObjectID', playlist.item_id), ('EnqueuedURI', track.uri), ('EnqueuedURIMetaData', XML.tostring(track.didl_metadata)), ('AddAtIndex', 4294967295)] )
def call(self, method, args=None): """ Call a method on the server Args: method (str): The name of the method to call. args (list): A list of (parameter, value) pairs representing the parameters of the method. Default None. Returns: (OrderedDict): An OrderedDict representing the response Raises: MusicServiceException """ message = SoapMessage( endpoint=self.endpoint, method=method, parameters=[] if args is None else args, http_headers=self.http_headers, soap_action="http://www.sonos.com/Services/1" ".1#{0}".format(method), soap_header=self.get_soap_header(), namespace=self.namespace, timeout=self.timeout) try: result_elt = message.call() except SoapFault as exc: if 'Client.TokenRefreshRequired' in exc.faultcode: log.debug('Token refresh required. Trying again') # Remove any cached value for the SOAP header self._cached_soap_header = None # <detail> # <refreshAuthTokenResult> # <authToken>xxxxxxx</authToken> # <privateKey>zzzzzz</privateKey> # </refreshAuthTokenResult> # </detail> auth_token = exc.detail.findtext('.//authToken') private_key = exc.detail.findtext('.//privateKey') # We have new details - update the account self.music_service.account.oa_device_id = auth_token self.music_service.account.key = private_key message = SoapMessage( endpoint=self.endpoint, method=method, parameters=args, http_headers=self.http_headers, soap_action="http://www.sonos.com/Services/1" ".1#{0}".format(method), soap_header=self.get_soap_header(), namespace=self.namespace, timeout=self.timeout) result_elt = message.call() else: raise MusicServiceException(exc.faultstring, exc.faultcode) # The top key in the OrderedDict will be the methodResult. Its # value may be None if no results were returned. result = parse( XML.tostring(result_elt), process_namespaces=True, namespaces={'http://www.sonos.com/Services/1.1': None} ).values()[0] return result if result is not None else {}
def call(self, method, args=None): """ Call a method on the server Args: method (str): The name of the method to call. args (list): A list of (parameter, value) pairs representing the parameters of the method. Default None. Returns: (OrderedDict): An OrderedDict representing the response Raises: MusicServiceException """ message = SoapMessage(endpoint=self.endpoint, method=method, parameters=[] if args is None else args, http_headers=self.http_headers, soap_action="http://www.sonos.com/Services/1" ".1#{0}".format(method), soap_header=self.get_soap_header(), namespace=self.namespace, timeout=self.timeout) try: result_elt = message.call() except SoapFault as exc: if 'Client.TokenRefreshRequired' in exc.faultcode: log.debug('Token refresh required. Trying again') # Remove any cached value for the SOAP header self._cached_soap_header = None # <detail> # <refreshAuthTokenResult> # <authToken>xxxxxxx</authToken> # <privateKey>zzzzzz</privateKey> # </refreshAuthTokenResult> # </detail> auth_token = exc.detail.findtext('.//authToken') private_key = exc.detail.findtext('.//privateKey') # We have new details - update the account self.music_service.account.oa_device_id = auth_token self.music_service.account.key = private_key message = SoapMessage( endpoint=self.endpoint, method=method, parameters=args, http_headers=self.http_headers, soap_action="http://www.sonos.com/Services/1" ".1#{0}".format(method), soap_header=self.get_soap_header(), namespace=self.namespace, timeout=self.timeout) result_elt = message.call() else: raise MusicServiceException(exc.faultstring, exc.faultcode) # The top key in the OrderedDict will be the methodResult. Its # value may be None if no results were returned. result = parse(XML.tostring(result_elt), process_namespaces=True, namespaces={ 'http://www.sonos.com/Services/1.1': None }).values()[0] return result if result is not None else {}
def get_accounts(cls, soco=None): """Get a dict containing all accounts known to the Sonos system. Args: soco (SoCo, optional): a SoCo instance to query. If None, a random instance is used. Defaults to None Returns: dict: A dict containing account instances. Each key is the account's serial number, and each value is the related Account instance. Accounts which have been marked as deleted are excluded. Note: Although an Account for TuneIn is always present, it is handled specially by Sonos, and will not appear in the returned dict. Any existing Account instance will have its attributes updated to those currently stored on the Sonos system. """ root = XML.fromstring(cls._get_account_xml(soco)) # _get_account_xml returns an ElementTree element like this: # <ZPSupportInfo type="User"> # <Accounts # LastUpdateDevice="RINCON_000XXXXXXXX400" # Version="8" NextSerialNum="5"> # <Account Type="2311" SerialNum="1"> # <UN>12345678</UN> # <MD>1</MD> # <NN></NN> # <OADevID></OADevID> # <Key></Key> # </Account> # <Account Type="41735" SerialNum="3" Deleted="1"> # <UN></UN> # <MD>1</MD> # <NN>Nickname</NN> # <OADevID></OADevID> # <Key></Key> # </Account> # ... # <Accounts /> # pylint: disable=protected-access xml_accounts = root.findall('.//Account') result = {} for xml_account in xml_accounts: serial_number = xml_account.get('SerialNum') is_deleted = True if xml_account.get('Deleted') == '1' else False # cls._all_accounts is a weakvaluedict keyed by serial number. # We use it as a database to store details of the accounts we # know about. We need to update it with info obtained from the # XML just obtained, so (1) check to see if we already have an # entry in cls._all_accounts for the account we have found in # XML; (2) if so, delete it if the XML says it has been deleted; # and (3) if not, create an entry for it if cls._all_accounts.get(serial_number): # We have an existing entry in our database. Do we need to # delete it? if is_deleted: # Yes, so delete it and move to the next XML account del cls._all_accounts[serial_number] continue else: # No, so load up its details, ready to update them account = cls._all_accounts.get(serial_number) else: # We have no existing entry for this account if is_deleted: # but it is marked as deleted, so we don't need one continue # If it is not marked as deleted, we need to create an entry account = Account() account.serial_number = serial_number cls._all_accounts[serial_number] = account # Now, update the entry in our database with the details from XML account.service_type = xml_account.get('Type') account.deleted = is_deleted account.username = xml_account.findtext('UN') # Not sure what 'MD' stands for. Metadata? account.metadata = xml_account.findtext('MD') account.nickname = xml_account.findtext('NN') account.oa_device_id = xml_account.findtext('OADevID') account.key = xml_account.findtext('Key') result[serial_number] = account return result
def _get_music_services_data(cls): """Parse raw account data xml into a useful python datastructure. Returns: (dict): A dict. Each key is a service_type, and each value is a dict containing relevant data """ # Return from cache if we have it. if cls._music_services_data is not None: return cls._music_services_data result = {} root = XML.fromstring( cls._get_music_services_data_xml().encode('utf-8')) # <Services SchemaVersion="1"> # <Service Id="163" Name="Spreaker" Version="1.1" # Uri="http://sonos.spreaker.com/sonos/service/v1" # SecureUri="https://sonos.spreaker.com/sonos/service/v1" # ContainerType="MService" # Capabilities="513" # MaxMessagingChars="0"> # <Policy Auth="Anonymous" PollInterval="30" /> # <Presentation> # <Strings # Version="1" # Uri="https:...string_table.xml" /> # <PresentationMap Version="2" # Uri="https://...presentation_map.xml" /> # </Presentation> # </Service> # ... # </ Services> # Ideally, the search path should be './/Service' to find Service # elements at any level, but Python 2.6 breaks with this if Service # is a child of the current element. Since 'Service' works here, we use # that instead services = root.findall('Service') for service in services: result_value = service.attrib.copy() name = service.get('Name') result_value['Name'] = name auth_element = (service.find('Policy')) auth = auth_element.attrib result_value.update(auth) presentation_element = (service.find('.//PresentationMap')) if presentation_element is not None: result_value['PresentationMapUri'] = \ presentation_element.get('Uri') result_value['ServiceID'] = service.get('Id') # ServiceType is used elsewhere in Sonos, eg to form tokens, # and get_subscribed_music_services() below. It is also the # 'Type' used in account_xml (see above). Its value always # seems to be (ID*256) + 7. Some serviceTypes are also # listed in available_services['AvailableServiceTypeList'] # but this does not seem to be comprehensive service_type = str(int(service.get('Id')) * 256 + 7) result_value['ServiceType'] = service_type result[service_type] = result_value # Cache this so we don't need to do it again. cls._music_services_data = result return result
def test_zone_group_parser(self): tree = XML.fromstring(zone_group_xml().encode('utf-8')) zone_groups = parser.parse_zone_group_state(tree) self.assertEqual(len(zone_groups), 2) ids = [z.uid for z in zone_groups] self.assertCountEqual(["RINCON_000XXXX1400:0", "RINCON_000XXX1400:46"], ids)
def _get_music_services_data(cls): """Parse raw account data xml into a useful python datastructure. Returns: (dict): A dict. Each key is a service_type, and each value is a dict containing relevant data """ # Return from cache if we have it. if cls._music_services_data is not None: return cls._music_services_data result = {} root = XML.fromstring( cls._get_music_services_data_xml().encode('utf-8') ) # <Services SchemaVersion="1"> # <Service Id="163" Name="Spreaker" Version="1.1" # Uri="http://sonos.spreaker.com/sonos/service/v1" # SecureUri="https://sonos.spreaker.com/sonos/service/v1" # ContainerType="MService" # Capabilities="513" # MaxMessagingChars="0"> # <Policy Auth="Anonymous" PollInterval="30" /> # <Presentation> # <Strings # Version="1" # Uri="https:...string_table.xml" /> # <PresentationMap Version="2" # Uri="https://...presentation_map.xml" /> # </Presentation> # </Service> # ... # </ Services> # Ideally, the search path should be './/Service' to find Service # elements at any level, but Python 2.6 breaks with this if Service # is a child of the current element. Since 'Service' works here, we use # that instead services = root.findall('Service') for service in services: result_value = service.attrib.copy() name = service.get('Name') result_value['Name'] = name auth_element = (service.find('Policy')) auth = auth_element.attrib result_value.update(auth) presentation_element = (service.find('.//PresentationMap')) if presentation_element is not None: result_value['PresentationMapUri'] = \ presentation_element.get('Uri') result_value['ServiceID'] = service.get('Id') # ServiceType is used elsewhere in Sonos, eg to form tokens, # and get_subscribed_music_services() below. It is also the # 'Type' used in account_xml (see above). Its value always # seems to be (ID*256) + 7. Some serviceTypes are also # listed in available_services['AvailableServiceTypeList'] # but this does not seem to be comprehensive service_type = str(int(service.get('Id')) * 256 + 7) result_value['ServiceType'] = service_type result[service_type] = result_value # Cache this so we don't need to do it again. cls._music_services_data = result return result