async def test_thumbnail( device_source_mock: DmsDeviceSource, dms_device_mock: Mock ) -> None: """Test getting thumbnails URLs for items.""" # Use browse_search to get multiple items at once for least effort dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( [ # Thumbnail as albumArtURI property didl_lite.MusicAlbum( id="a", restricted="false", title="a", res=[], album_art_uri="a_thumb.jpg", ), # Thumbnail as resource (1st resource is media item, 2nd is missing # a URI, 3rd is thumbnail) didl_lite.MusicTrack( id="b", restricted="false", title="b", res=[ didl_lite.Resource( uri="b_track.mp3", protocol_info="http-get:*:audio/mpeg:" ), didl_lite.Resource(uri="", protocol_info="internal:*::"), didl_lite.Resource( uri="b_thumb.png", protocol_info="http-get:*:image/png:" ), ], ), # No thumbnail didl_lite.MusicTrack( id="c", restricted="false", title="c", res=[ didl_lite.Resource( uri="c_track.mp3", protocol_info="http-get:*:audio/mpeg:" ) ], ), ], 3, 3, 0, ) result = await device_source_mock.async_browse_media("?query") assert result.children assert result.children[0].thumbnail == f"{MOCK_DEVICE_BASE_URL}/a_thumb.jpg" assert result.children[1].thumbnail == f"{MOCK_DEVICE_BASE_URL}/b_thumb.png" assert result.children[2].thumbnail is None
async def test_catch_upnp_connection_error( hass: HomeAssistant, dms_device_mock: Mock ) -> None: """Test UpnpConnectionError causes the device source to disconnect from the device.""" # First check the source can be used object_id = "foo" didl_item = didl_lite.Item( id=object_id, restricted="false", title="Object", res=[didl_lite.Resource(uri="foo", protocol_info="http-get:*:audio/mpeg")], ) dms_device_mock.async_browse_metadata.return_value = didl_item await async_browse_media(hass, f":{object_id}") dms_device_mock.async_browse_metadata.assert_awaited_once_with( object_id, metadata_filter=ANY ) # Cause a UpnpConnectionError when next browsing dms_device_mock.async_browse_metadata.side_effect = UpnpConnectionError with pytest.raises( BrowseError, match="Server disconnected: UpnpConnectionError(.*)" ): await async_browse_media(hass, f":{object_id}") # Clear the error, but the device should be disconnected dms_device_mock.async_browse_metadata.side_effect = None with pytest.raises(BrowseError, match="DMS is not connected"): await async_browse_media(hass, f":{object_id}")
async def test_resolve_media_success(hass: HomeAssistant, dms_device_mock: Mock, device_source_mock: None) -> None: """Test resolving an item via a DmsDeviceSource.""" object_id = "123" res_url = "foo/bar" res_mime = "audio/mpeg" didl_item = didl_lite.Item( id=object_id, restricted=False, title="Object", res=[ didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:") ], ) dms_device_mock.async_browse_metadata.return_value = didl_item result = await media_source.async_resolve_media( hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:{object_id}") assert isinstance(result, DidlPlayMedia) assert result.url == f"{MOCK_DEVICE_BASE_URL}/{res_url}" assert result.mime_type == res_mime assert result.didl_metadata is didl_item
async def test_become_unavailable( hass: HomeAssistant, connected_source_mock: None, dms_device_mock: Mock, ) -> None: """Test a device becoming unavailable.""" # Mock a good resolve result dms_device_mock.async_browse_metadata.return_value = didl_lite.Item( id="object_id", restricted=False, title="Object", res=[didl_lite.Resource(uri="foo", protocol_info="http-get:*:audio/mpeg:")], ) # Check async_resolve_object currently works assert await media_source.async_resolve_media( hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:object_id", None ) # Now break the network connection dms_device_mock.async_browse_metadata.side_effect = UpnpConnectionError # async_resolve_object should fail with pytest.raises(Unresolvable): await media_source.async_resolve_media( hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:object_id", None ) # The device should now be unavailable await assert_source_unavailable(hass)
async def test_become_unavailable( hass: HomeAssistant, connected_source_mock: DmsDeviceSource, dms_device_mock: Mock, ) -> None: """Test a device becoming unavailable.""" # Mock a good resolve result dms_device_mock.async_browse_metadata.return_value = didl_lite.Item( id="object_id", restricted=False, title="Object", res=[didl_lite.Resource(uri="foo", protocol_info="http-get:*:audio/mpeg:")], ) # Check async_resolve_object currently works await connected_source_mock.async_resolve_media(":object_id") # Now break the network connection dms_device_mock.async_browse_metadata.side_effect = UpnpConnectionError # The device should be considered available until next contacted assert connected_source_mock.available # async_resolve_object should fail with pytest.raises(Unresolvable): await connected_source_mock.async_resolve_media(":object_id") # The device should now be unavailable assert not connected_source_mock.available
async def test_resolve_media_path(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test the async_resolve_path method via async_resolve_media.""" # Path resolution involves searching each component of the path, then # browsing the metadata of the final object found. path: Final = "path/to/thing" object_ids: Final = ["path_id", "to_id", "thing_id"] res_url: Final = "foo/bar" res_abs_url: Final = f"{MOCK_DEVICE_BASE_URL}/{res_url}" res_mime: Final = "audio/mpeg" search_directory_result = [] for ob_id, ob_title in zip(object_ids, path.split("/")): didl_item = didl_lite.Item( id=ob_id, restricted="false", title=ob_title, res=[], ) search_directory_result.append( DmsDevice.BrowseResult([didl_item], 1, 1, 0)) # Test that path is resolved correctly dms_device_mock.async_search_directory.side_effect = search_directory_result dms_device_mock.async_browse_metadata.return_value = didl_lite.Item( id=object_ids[-1], restricted="false", title="thing", res=[ didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:") ], ) result = await async_resolve_media(hass, f"/{path}") assert dms_device_mock.async_search_directory.await_args_list == [ call( parent_id, search_criteria=f'@parentID="{parent_id}" and dc:title="{title}"', metadata_filter=["id", "upnp:class", "dc:title"], requested_count=1, ) for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) ] assert result.url == res_abs_url assert result.mime_type == res_mime # Test a path starting with a / (first / is path action, second / is root of path) dms_device_mock.async_search_directory.reset_mock() dms_device_mock.async_search_directory.side_effect = search_directory_result result = await async_resolve_media(hass, f"//{path}") assert dms_device_mock.async_search_directory.await_args_list == [ call( parent_id, search_criteria=f'@parentID="{parent_id}" and dc:title="{title}"', metadata_filter=["id", "upnp:class", "dc:title"], requested_count=1, ) for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) ] assert result.url == res_abs_url assert result.mime_type == res_mime
async def test_resolve_media_search( device_source_mock: DmsDeviceSource, dms_device_mock: Mock ) -> None: """Test the async_resolve_search method via async_resolve_media.""" res_url: Final = "foo/bar" res_abs_url: Final = f"{MOCK_DEVICE_BASE_URL}/{res_url}" res_mime: Final = "audio/mpeg" # No results dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( [], 0, 0, 0 ) with pytest.raises(Unresolvable, match='Nothing found for dc:title="thing"'): await device_source_mock.async_resolve_media('?dc:title="thing"') assert dms_device_mock.async_search_directory.await_args_list == [ call( container_id="0", search_criteria='dc:title="thing"', metadata_filter="*", requested_count=1, ) ] # One result dms_device_mock.async_search_directory.reset_mock() didl_item = didl_lite.Item( id="thing's id", restricted="false", title="thing", res=[didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:")], ) dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( [didl_item], 1, 1, 0 ) result = await device_source_mock.async_resolve_media('?dc:title="thing"') assert result.url == res_abs_url assert result.mime_type == res_mime assert result.didl_metadata is didl_item assert dms_device_mock.async_search_directory.await_count == 1 # Values should be taken from search result, not querying the item's metadata assert dms_device_mock.async_browse_metadata.await_count == 0 # Two results - uses the first dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( [didl_item], 1, 2, 0 ) result = await device_source_mock.async_resolve_media('?dc:title="thing"') assert result.url == res_abs_url assert result.mime_type == res_mime assert result.didl_metadata is didl_item # Bad result dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( [didl_lite.Descriptor("id", "namespace")], 1, 1, 0 ) with pytest.raises(Unresolvable, match="Descriptor.* is not a DidlObject"): await device_source_mock.async_resolve_media('?dc:title="thing"')
async def test_resolve_path_browsed(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test async_resolve_path: action error results in browsing.""" path: Final = "path/to/thing" object_ids: Final = ["path_id", "to_id", "thing_id"] res_url: Final = "foo/bar" res_mime: Final = "audio/mpeg" # Setup expected calls search_directory_result = [] for ob_id, ob_title in zip(object_ids, path.split("/")): didl_item = didl_lite.Item( id=ob_id, restricted="false", title=ob_title, res=[], ) search_directory_result.append(DmsDevice.BrowseResult([didl_item], 1, 1, 0)) dms_device_mock.async_search_directory.side_effect = [ search_directory_result[0], # 2nd level can't be searched (this happens with Kodi) UpnpActionError(), search_directory_result[2], ] browse_children_result: BrowseResultList = [] for title in ("Irrelevant", "to", "Ignored"): browse_children_result.append( didl_lite.Item(id=f"{title}_id", restricted="false", title=title, res=[]) ) dms_device_mock.async_browse_direct_children.side_effect = [ DmsDevice.BrowseResult(browse_children_result, 3, 3, 0) ] dms_device_mock.async_browse_metadata.return_value = didl_lite.Item( id=object_ids[-1], restricted="false", title="thing", res=[didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:")], ) # Perform the action to test result = await async_resolve_media(hass, path) # All levels should have an attempted search assert dms_device_mock.async_search_directory.await_args_list == [ call( parent_id, search_criteria=f'@parentID="{parent_id}" and dc:title="{title}"', metadata_filter=["id", "upnp:class", "dc:title"], requested_count=1, ) for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) ] assert result.didl_metadata.id == object_ids[-1] # 2nd level should also be browsed assert dms_device_mock.async_browse_direct_children.await_args_list == [ call("path_id", metadata_filter=["id", "upnp:class", "dc:title"]) ]
async def connected_source_mock( dms_device_mock: Mock, device_source_mock: None ) -> None: """Fixture to set up a mock DmsDeviceSource in a connected state.""" # Make async_browse_metadata work for assert_source_available didl_item = didl_lite.Item( id=DUMMY_OBJECT_ID, restricted=False, title="Object", res=[didl_lite.Resource(uri="foo/bar", protocol_info="http-get:*:audio/mpeg:")], ) dms_device_mock.async_browse_metadata.return_value = didl_item
def test_item_to_xml(self) -> None: """Test item to XML.""" resource = didl_lite.Resource("url", "protocol_info") items = [ didl_lite.AudioItem( id="0", parent_id="0", title="Audio Item Title", restricted="1", resources=[resource], language="English", longDescription="Long description", ), ] didl_string = didl_lite.to_xml_string(*items).decode("utf-8") assert 'xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"' in didl_string assert 'xmlns:dc="http://purl.org/dc/elements/1.1/"' in didl_string assert 'xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"' in didl_string assert 'xmlns:sec="http://www.sec.co.kr/"' in didl_string assert 'xmlns:ns1="urn:schemas-upnp-org:metadata-1-0/upnp/"' not in didl_string didl_el = ET.fromstring(didl_string) item_el = didl_el.find("./didl_lite:item", NAMESPACES) assert item_el is not None assert item_el.attrib["id"] == "0" assert item_el.attrib["parentID"] == "0" assert item_el.attrib["restricted"] == "1" title_el = item_el.find("./dc:title", NAMESPACES) assert title_el is not None assert title_el.text == "Audio Item Title" class_el = item_el.find("./upnp:class", NAMESPACES) assert class_el is not None assert class_el.text == "object.item.audioItem" language_el = item_el.find("./dc:language", NAMESPACES) assert language_el is not None assert language_el.text == "English" long_description_el = item_el.find("./upnp:longDescription", NAMESPACES) assert long_description_el is not None assert long_description_el.text == "Long description" res_el = item_el.find("./didl_lite:res", NAMESPACES) assert res_el is not None assert res_el.attrib["protocolInfo"] == "protocol_info" assert res_el.text == "url"
def test_container_to_xml(self) -> None: """Test container to XML.""" container = didl_lite.Album(id="0", parent_id="0", title="Audio Item Title", restricted="1") resource = didl_lite.Resource("url", "protocol_info") item = didl_lite.AudioItem( id="0", parent_id="0", title="Audio Item Title", restricted="1", resources=[resource], language="English", ) container.append(item) didl_string = didl_lite.to_xml_string(container).decode("utf-8") didl_el = ET.fromstring(didl_string) container_el = didl_el.find("./didl_lite:container", NAMESPACES) assert container_el is not None assert container_el.attrib["id"] == "0" assert container_el.attrib["parentID"] == "0" assert container_el.attrib["restricted"] == "1" item_el = container_el.find("./didl_lite:item", NAMESPACES) assert item_el is not None assert item_el.attrib["id"] == "0" assert item_el.attrib["parentID"] == "0" assert item_el.attrib["restricted"] == "1" title_el = item_el.find("./dc:title", NAMESPACES) assert title_el is not None assert title_el.text == "Audio Item Title" class_el = item_el.find("./upnp:class", NAMESPACES) assert class_el is not None assert class_el.text == "object.item.audioItem" language_el = item_el.find("./dc:language", NAMESPACES) assert language_el is not None assert language_el.text == "English" res_el = item_el.find("./didl_lite:res", NAMESPACES) assert res_el is not None assert res_el.attrib["protocolInfo"] == "protocol_info" assert res_el.text == "url"
async def test_browse_media_object( device_source_mock: DmsDeviceSource, dms_device_mock: Mock ) -> None: """Test async_browse_object via async_browse_media.""" object_id = "1234" child_titles = ("Item 1", "Thing", "Item 2") dms_device_mock.async_browse_metadata.return_value = didl_lite.Container( id=object_id, restricted="false", title="subcontainer" ) children_result = DmsDevice.BrowseResult([], 3, 3, 0) for title in child_titles: children_result.result.append( didl_lite.Item( id=title + "_id", restricted="false", title=title, res=[ didl_lite.Resource( uri=title + "_url", protocol_info="http-get:*:audio/mpeg:" ) ], ), ) dms_device_mock.async_browse_direct_children.return_value = children_result result = await device_source_mock.async_browse_media(f":{object_id}") dms_device_mock.async_browse_metadata.assert_awaited_once_with( object_id, metadata_filter=ANY ) dms_device_mock.async_browse_direct_children.assert_awaited_once_with( object_id, metadata_filter=ANY, sort_criteria=ANY ) assert result.domain == DOMAIN assert result.identifier == f"{MOCK_SOURCE_ID}/:{object_id}" assert result.title == "subcontainer" assert not result.can_play assert result.can_expand assert result.children for child, title in zip(result.children, child_titles): assert isinstance(child, BrowseMediaSource) assert child.identifier == f"{MOCK_SOURCE_ID}/:{title}_id" assert child.title == title assert child.can_play assert not child.can_expand assert not child.children
async def disconnected_source_mock( hass: HomeAssistant, upnp_factory_mock: Mock, config_entry_mock: MockConfigEntry, ssdp_scanner_mock: Mock, dms_device_mock: Mock, ) -> AsyncIterable[None]: """Fixture to set up a mock DmsDeviceSource in a disconnected state.""" # Cause the connection attempt to fail upnp_factory_mock.async_create_device.side_effect = UpnpConnectionError config_entry_mock.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry_mock.entry_id) await hass.async_block_till_done() # Check the DmsDeviceSource has registered all needed listeners assert len(config_entry_mock.update_listeners) == 0 assert ssdp_scanner_mock.async_register_callback.await_count == 2 assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 # Make async_browse_metadata work for assert_source_available when this # source is connected didl_item = didl_lite.Item( id=DUMMY_OBJECT_ID, restricted=False, title="Object", res=[ didl_lite.Resource(uri="foo/bar", protocol_info="http-get:*:audio/mpeg:") ], ) dms_device_mock.async_browse_metadata.return_value = didl_item # Run the test yield # Unload config entry to clean up assert await hass.config_entries.async_remove(config_entry_mock.entry_id ) == { "require_restart": False } # Check device source has cleaned up its resources assert not config_entry_mock.update_listeners assert (ssdp_scanner_mock.async_register_callback.await_count == ssdp_scanner_mock.async_register_callback.return_value.call_count)
async def test_can_play( device_source_mock: DmsDeviceSource, dms_device_mock: Mock ) -> None: """Test determination of playability for items.""" protocol_infos = [ # No protocol info for resource ("", True), # Protocol info is poorly formatted but can play ("http-get", True), # Protocol info is poorly formatted and can't play ("internal", False), # Protocol is HTTP ("http-get:*:audio/mpeg", True), # Protocol is RTSP ("rtsp-rtp-udp:*:MPA:", True), # Protocol is something else ("internal:*:audio/mpeg:", False), ] search_results: BrowseResultList = [] # No resources search_results.append(didl_lite.DidlObject(id="", restricted="false", title="")) search_results.extend( didl_lite.MusicTrack( id="", restricted="false", title="", res=[didl_lite.Resource(uri="", protocol_info=protocol_info)], ) for protocol_info, _ in protocol_infos ) # Use browse_search to get multiple items at once for least effort dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( search_results, len(search_results), len(search_results), 0 ) result = await device_source_mock.async_browse_media("?query") assert result.children assert not result.children[0].can_play for idx, info_can_play in enumerate(protocol_infos): protocol_info, can_play = info_can_play assert result.children[idx + 1].can_play is can_play, f"Checked {protocol_info}"
async def _construct_play_media_metadata(self, media_url, media_title, mime_type, upnp_class): """Construct the metadata for play_media command.""" media_info = { 'mime_type': mime_type, 'dlna_features': 'DLNA.ORG_OP=01;DLNA.ORG_CI=0;' 'DLNA.ORG_FLAGS=00000000000000000000000000000000', } # do a HEAD/GET, to retrieve content-type/mime-type try: headers = await self._fetch_headers( media_url, {'GetContentFeatures.dlna.org': '1'}) if headers: if 'Content-Type' in headers: media_info['mime_type'] = headers['Content-Type'] if 'ContentFeatures.dlna.org' in headers: media_info['dlna_features'] = headers[ 'contentFeatures.dlna.org'] except Exception: # pylint: disable=broad-except pass # build DIDL-Lite item + resource protocol_info = "http-get:*:{mime_type}:{dlna_features}".format( **media_info) resource = didl_lite.Resource(uri=media_url, protocol_info=protocol_info) didl_item_type = didl_lite.type_by_upnp_class(upnp_class) item = didl_item_type(id="0", parent_id="0", title=media_title, restricted="1", resources=[resource]) return didl_lite.to_xml_string(item).decode('utf-8')
async def construct_play_media_metadata( self, media_url: str, media_title: str, default_mime_type: Optional[str] = None, default_upnp_class: Optional[str] = None, override_mime_type: Optional[str] = None, override_upnp_class: Optional[str] = None, override_dlna_features: Optional[str] = None, ) -> str: """ Construct the metadata for play_media command. This queries the source and takes mime_type/dlna_features from it. :arg media_url URL to media :arg media_title :arg default_mime_type Suggested mime type, will be overridden by source if possible :arg default_upnp_class Suggested UPnP class, will be used as fallback for autodetection :arg override_mime_type Enforce mime_type, even if source reports a different mime_type :arg override_upnp_class Enforce upnp_class, even if autodetection finds something usable :arg override_dlna_features Enforce DLNA features, even if source reports different features :return String containing metadata """ # pylint: disable=too-many-arguments, too-many-locals, too-many-branches mime_type = override_mime_type or "" upnp_class = override_upnp_class or "" dlna_features = override_dlna_features or "*" if None in (override_mime_type, override_dlna_features): # do a HEAD/GET, to retrieve content-type/mime-type try: headers = await self._fetch_headers( media_url, {"GetContentFeatures.dlna.org": "1"}) if headers: if not override_mime_type and "Content-Type" in headers: mime_type = headers["Content-Type"] if (not override_dlna_features and "ContentFeatures.dlna.org" in headers): dlna_features = headers["contentFeatures.dlna.org"] except Exception: # pylint: disable=broad-except pass if not mime_type: _type = guess_type(media_url.split("?")[0]) mime_type = _type[0] or "" if not mime_type: mime_type = default_mime_type or "application/octet-stream" # use CM/GetProtocolInfo to improve on dlna_features if (not override_dlna_features and dlna_features != "*" and self.has_get_protocol_info): protocol_info_entries = ( await self._async_get_sink_protocol_info_for_mime_type(mime_type) ) for entry in protocol_info_entries: if entry[3] == "*": # device accepts anything, send this dlna_features = "*" # Try to derive a basic upnp_class from mime_type if not override_upnp_class: mime_type = mime_type.lower() for _mime, _class in MIME_TO_UPNP_CLASS_MAPPING.items(): if mime_type.startswith(_mime): upnp_class = _class break else: upnp_class = default_upnp_class or "object.item" # build DIDL-Lite item + resource didl_item_type = didl_lite.type_by_upnp_class(upnp_class) if not didl_item_type: raise UpnpError("Unknown DIDL-lite type") protocol_info = f"http-get:*:{mime_type}:{dlna_features}" resource = didl_lite.Resource(uri=media_url, protocol_info=protocol_info) item = didl_item_type( id="0", parent_id="-1", title=media_title, restricted="false", resources=[resource], ) xml_string: bytes = didl_lite.to_xml_string(item) return xml_string.decode("utf-8")
async def test_resolve_media_object( device_source_mock: DmsDeviceSource, dms_device_mock: Mock ) -> None: """Test the async_resolve_object method via async_resolve_media.""" object_id: Final = "123" res_url: Final = "foo/bar" res_abs_url: Final = f"{MOCK_DEVICE_BASE_URL}/{res_url}" res_mime: Final = "audio/mpeg" # Success case: one resource didl_item = didl_lite.Item( id=object_id, restricted="false", title="Object", res=[didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:")], ) dms_device_mock.async_browse_metadata.return_value = didl_item result = await device_source_mock.async_resolve_media(f":{object_id}") dms_device_mock.async_browse_metadata.assert_awaited_once_with( object_id, metadata_filter="*" ) assert result.url == res_abs_url assert result.mime_type == res_mime assert result.didl_metadata is didl_item # Success case: two resources, first is playable didl_item = didl_lite.Item( id=object_id, restricted="false", title="Object", res=[ didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:"), didl_lite.Resource( uri="thumbnail.png", protocol_info="http-get:*:image/png:" ), ], ) dms_device_mock.async_browse_metadata.return_value = didl_item result = await device_source_mock.async_resolve_media(f":{object_id}") assert result.url == res_abs_url assert result.mime_type == res_mime assert result.didl_metadata is didl_item # Success case: three resources, only third is playable didl_item = didl_lite.Item( id=object_id, restricted="false", title="Object", res=[ didl_lite.Resource(uri="", protocol_info=""), didl_lite.Resource(uri="internal:thing", protocol_info="internal:*::"), didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:"), ], ) dms_device_mock.async_browse_metadata.return_value = didl_item result = await device_source_mock.async_resolve_media(f":{object_id}") assert result.url == res_abs_url assert result.mime_type == res_mime assert result.didl_metadata is didl_item # Failure case: no resources didl_item = didl_lite.Item( id=object_id, restricted="false", title="Object", res=[], ) dms_device_mock.async_browse_metadata.return_value = didl_item with pytest.raises(Unresolvable, match="Object has no resources"): await device_source_mock.async_resolve_media(f":{object_id}") # Failure case: resources are not playable didl_item = didl_lite.Item( id=object_id, restricted="false", title="Object", res=[didl_lite.Resource(uri="internal:thing", protocol_info="internal:*::")], ) dms_device_mock.async_browse_metadata.return_value = didl_item with pytest.raises(Unresolvable, match="Object has no playable resources"): await device_source_mock.async_resolve_media(f":{object_id}")
async def _construct_play_media_metadata( self, media_url: str, media_title: str, mime_type: str, upnp_class: str, override_mime_type: Optional[str] = None, override_dlna_features: Optional[str] = None, ) -> str: """ Construct the metadata for play_media command. This queries the source and takes mime_type/dlna_features from it. :arg media_url URL to media :arg media_title :arg mime_type Suggested mime type, will be overridden by source if possible :arg upnp_class UPnP class :arg override_mime_type Enfore mime_type, even if source reports a different mime_type :arg override_dlna_features Enforce DLNA features, even if source reports different features :return String containing metadata """ # pylint: disable=too-many-arguments, too-many-locals media_info = { "mime_type": mime_type, "dlna_features": "*", } # do a HEAD/GET, to retrieve content-type/mime-type try: headers = await self._fetch_headers( media_url, {"GetContentFeatures.dlna.org": "1"}) if headers: if "Content-Type" in headers: media_info["mime_type"] = headers["Content-Type"] if "ContentFeatures.dlna.org" in headers: media_info["dlna_features"] = headers[ "contentFeatures.dlna.org"] except Exception: # pylint: disable=broad-except pass # use CM/GetProtocolInfo to improve on dlna_features if self.has_get_protocol_info: protocol_info_entries = ( await self._async_get_sink_protocol_info_for_mime_type( media_info["mime_type"])) for entry in protocol_info_entries: if entry[3] == "*": # device accepts anything, send this media_info["dlna_features"] = "*" # allow overriding of mime_type/dlna_features if override_mime_type: media_info["mime_type"] = override_mime_type if override_dlna_features: media_info["dlna_features"] = override_dlna_features # build DIDL-Lite item + resource didl_item_type = didl_lite.type_by_upnp_class(upnp_class) if not didl_item_type: raise UpnpError("Unknown DIDL-lite type") protocol_info = "http-get:*:{mime_type}:{dlna_features}".format( **media_info) resource = didl_lite.Resource(uri=media_url, protocol_info=protocol_info) item = didl_item_type( id="0", parent_id="-1", title=media_title, restricted="false", resources=[resource], ) xml_string: bytes = didl_lite.to_xml_string(item) return xml_string.decode("utf-8")