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
Esempio n. 2
0
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
Esempio n. 4
0
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)
Esempio n. 5
0
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
Esempio n. 6
0
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"')
Esempio n. 8
0
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"])
    ]
Esempio n. 9
0
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}"
Esempio n. 15
0
    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')
Esempio n. 16
0
    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}")
Esempio n. 18
0
    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")