Allow roku to browse and play local media (#64799)

This commit is contained in:
Chris Talkington 2022-01-24 10:34:09 -06:00 committed by GitHub
parent 8ea2f865ed
commit 3e29fe5a67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 447 additions and 169 deletions

View File

@ -1,5 +1,11 @@
"""Support for media browsing.""" """Support for media browsing."""
from __future__ import annotations
from collections.abc import Callable
from functools import partial
from urllib.parse import quote_plus
from homeassistant.components import media_source
from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
MEDIA_CLASS_APP, MEDIA_CLASS_APP,
@ -10,6 +16,11 @@ from homeassistant.components.media_player.const import (
MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS, MEDIA_TYPE_CHANNELS,
) )
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.network import is_internal_request
from .coordinator import RokuDataUpdateCoordinator
CONTENT_TYPE_MEDIA_CLASS = { CONTENT_TYPE_MEDIA_CLASS = {
MEDIA_TYPE_APP: MEDIA_CLASS_APP, MEDIA_TYPE_APP: MEDIA_CLASS_APP,
@ -33,8 +44,133 @@ EXPANDABLE_MEDIA_TYPES = [
MEDIA_TYPE_CHANNELS, MEDIA_TYPE_CHANNELS,
] ]
GetBrowseImageUrlType = Callable[[str, str, "str | None"], str]
def build_item_response(coordinator, payload, get_thumbnail_url=None):
def get_thumbnail_url_full(
coordinator: RokuDataUpdateCoordinator,
is_internal: bool,
get_browse_image_url: GetBrowseImageUrlType,
media_content_type: str,
media_content_id: str,
media_image_id: str | None = None,
) -> str | None:
"""Get thumbnail URL."""
if is_internal:
if media_content_type == MEDIA_TYPE_APP and media_content_id:
return coordinator.roku.app_icon_url(media_content_id)
return None
return get_browse_image_url(
media_content_type,
quote_plus(media_content_id),
media_image_id,
)
async def async_browse_media(
hass,
coordinator: RokuDataUpdateCoordinator,
get_browse_image_url: GetBrowseImageUrlType,
media_content_id: str | None,
media_content_type: str | None,
):
"""Browse media."""
if media_content_id is None:
return await root_payload(
hass,
coordinator,
get_browse_image_url,
)
if media_source.is_media_source_id(media_content_id):
return await media_source.async_browse_media(hass, media_content_id)
payload = {
"search_type": media_content_type,
"search_id": media_content_id,
}
response = await hass.async_add_executor_job(
build_item_response,
coordinator,
payload,
partial(
get_thumbnail_url_full,
coordinator,
is_internal_request(hass),
get_browse_image_url,
),
)
if response is None:
raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}")
return response
async def root_payload(
hass: HomeAssistant,
coordinator: RokuDataUpdateCoordinator,
get_browse_image_url: GetBrowseImageUrlType,
):
"""Return root payload for Roku."""
device = coordinator.data
children = [
item_payload(
{"title": "Apps", "type": MEDIA_TYPE_APPS},
coordinator,
get_browse_image_url,
)
]
if device.info.device_type == "tv" and len(device.channels) > 0:
children.append(
item_payload(
{"title": "TV Channels", "type": MEDIA_TYPE_CHANNELS},
coordinator,
get_browse_image_url,
)
)
try:
browse_item = await media_source.async_browse_media(hass, None)
# If domain is None, it's overview of available sources
if browse_item.domain is None:
if browse_item.children is not None:
children.extend(browse_item.children)
else:
children.append(browse_item)
except media_source.BrowseError:
pass
if len(children) == 1:
return await async_browse_media(
hass,
coordinator,
get_browse_image_url,
children[0].media_content_id,
children[0].media_content_type,
)
return BrowseMedia(
title="Roku",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="",
media_content_type="root",
can_play=False,
can_expand=True,
children=children,
)
def build_item_response(
coordinator: RokuDataUpdateCoordinator,
payload: dict,
get_browse_image_url: GetBrowseImageUrlType,
) -> BrowseMedia | None:
"""Create response payload for the provided media query.""" """Create response payload for the provided media query."""
search_id = payload["search_id"] search_id = payload["search_id"]
search_type = payload["search_type"] search_type = payload["search_type"]
@ -52,7 +188,7 @@ def build_item_response(coordinator, payload, get_thumbnail_url=None):
] ]
children_media_class = MEDIA_CLASS_APP children_media_class = MEDIA_CLASS_APP
elif search_type == MEDIA_TYPE_CHANNELS: elif search_type == MEDIA_TYPE_CHANNELS:
title = "Channels" title = "TV Channels"
media = [ media = [
{ {
"channel_number": item.number, "channel_number": item.number,
@ -63,7 +199,7 @@ def build_item_response(coordinator, payload, get_thumbnail_url=None):
] ]
children_media_class = MEDIA_CLASS_CHANNEL children_media_class = MEDIA_CLASS_CHANNEL
if media is None: if title is None or media is None:
return None return None
return BrowseMedia( return BrowseMedia(
@ -75,13 +211,19 @@ def build_item_response(coordinator, payload, get_thumbnail_url=None):
title=title, title=title,
can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id, can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id,
can_expand=True, can_expand=True,
children=[item_payload(item, coordinator, get_thumbnail_url) for item in media], children=[
item_payload(item, coordinator, get_browse_image_url) for item in media
],
children_media_class=children_media_class, children_media_class=children_media_class,
thumbnail=thumbnail, thumbnail=thumbnail,
) )
def item_payload(item, coordinator, get_thumbnail_url=None): def item_payload(
item: dict,
coordinator: RokuDataUpdateCoordinator,
get_browse_image_url: GetBrowseImageUrlType,
):
""" """
Create response payload for a single media item. Create response payload for a single media item.
@ -92,8 +234,7 @@ def item_payload(item, coordinator, get_thumbnail_url=None):
if "app_id" in item: if "app_id" in item:
media_content_type = MEDIA_TYPE_APP media_content_type = MEDIA_TYPE_APP
media_content_id = item["app_id"] media_content_id = item["app_id"]
if get_thumbnail_url: thumbnail = get_browse_image_url(media_content_type, media_content_id, None)
thumbnail = get_thumbnail_url(media_content_type, media_content_id)
elif "channel_number" in item: elif "channel_number" in item:
media_content_type = MEDIA_TYPE_CHANNEL media_content_type = MEDIA_TYPE_CHANNEL
media_content_id = item["channel_number"] media_content_id = item["channel_number"]
@ -114,52 +255,3 @@ def item_payload(item, coordinator, get_thumbnail_url=None):
can_expand=can_expand, can_expand=can_expand,
thumbnail=thumbnail, thumbnail=thumbnail,
) )
def library_payload(coordinator, get_thumbnail_url=None):
"""
Create response payload to describe contents of a specific library.
Used by async_browse_media.
"""
library_info = BrowseMedia(
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="library",
media_content_type="library",
title="Media Library",
can_play=False,
can_expand=True,
children=[],
)
library = {
MEDIA_TYPE_APPS: "Apps",
MEDIA_TYPE_CHANNELS: "Channels",
}
for item in [{"title": name, "type": type_} for type_, name in library.items()]:
if (
item["type"] == MEDIA_TYPE_CHANNELS
and coordinator.data.info.device_type != "tv"
):
continue
library_info.children.append(
item_payload(
{"title": item["title"], "type": item["type"]},
coordinator,
get_thumbnail_url,
)
)
if all(
child.media_content_type == MEDIA_TYPE_APPS for child in library_info.children
):
library_info.children_media_class = MEDIA_CLASS_APP
elif all(
child.media_content_type == MEDIA_TYPE_CHANNELS
for child in library_info.children
):
library_info.children_media_class = MEDIA_CLASS_CHANNEL
return library_info

View File

@ -4,9 +4,12 @@ from __future__ import annotations
import datetime as dt import datetime as dt
import logging import logging
from typing import Any from typing import Any
from urllib.parse import quote
import voluptuous as vol import voluptuous as vol
from homeassistant.components import media_source
from homeassistant.components.http.auth import async_sign_path
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
BrowseMedia, BrowseMedia,
MediaPlayerDeviceClass, MediaPlayerDeviceClass,
@ -29,7 +32,6 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_STEP,
) )
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, HLS_PROVIDER from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, HLS_PROVIDER
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -44,10 +46,10 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.network import is_internal_request from homeassistant.helpers.network import get_url
from . import roku_exception_handler from . import roku_exception_handler
from .browse_media import build_item_response, library_payload from .browse_media import async_browse_media
from .const import ( from .const import (
ATTR_CONTENT_ID, ATTR_CONTENT_ID,
ATTR_FORMAT, ATTR_FORMAT,
@ -287,35 +289,13 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
media_content_id: str | None = None, media_content_id: str | None = None,
) -> BrowseMedia: ) -> BrowseMedia:
"""Implement the websocket media browsing helper.""" """Implement the websocket media browsing helper."""
is_internal = is_internal_request(self.hass) return await async_browse_media(
self.hass,
def _get_thumbnail_url( self.coordinator,
media_content_type, media_content_id, media_image_id=None self.get_browse_image_url,
): media_content_id,
if is_internal: media_content_type,
if media_content_type == MEDIA_TYPE_APP and media_content_id: )
return self.coordinator.roku.app_icon_url(media_content_id)
return None
return self.get_browse_image_url(
media_content_type, media_content_id, media_image_id
)
if media_content_type in [None, "library"]:
return library_payload(self.coordinator, _get_thumbnail_url)
payload = {
"search_type": media_content_type,
"search_id": media_content_id,
}
response = build_item_response(self.coordinator, payload, _get_thumbnail_url)
if response is None:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
)
return response
@roku_exception_handler @roku_exception_handler
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:
@ -380,9 +360,27 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
@roku_exception_handler @roku_exception_handler
async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None:
"""Tune to channel.""" """Play media from a URL or file, launch an application, or tune to a channel."""
extra: dict[str, Any] = kwargs.get(ATTR_MEDIA_EXTRA) or {} extra: dict[str, Any] = kwargs.get(ATTR_MEDIA_EXTRA) or {}
# Handle media_source
if media_source.is_media_source_id(media_id):
sourced_media = await media_source.async_resolve_media(self.hass, media_id)
media_type = MEDIA_TYPE_URL
media_id = sourced_media.url
# Sign and prefix with URL if playing a relative URL
if media_id[0] == "/":
media_id = async_sign_path(
self.hass,
quote(media_id),
dt.timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME),
)
# prepend external URL
hass_url = get_url(self.hass)
media_id = f"{hass_url}{media_id}"
if media_type not in PLAY_MEDIA_SUPPORTED_TYPES: if media_type not in PLAY_MEDIA_SUPPORTED_TYPES:
_LOGGER.error( _LOGGER.error(
"Invalid media type %s. Only %s, %s, %s, and camera HLS streams are supported", "Invalid media type %s. Only %s, %s, %s, and camera HLS streams are supported",

View File

@ -22,6 +22,7 @@ from homeassistant.components.media_player.const import (
MEDIA_CLASS_APP, MEDIA_CLASS_APP,
MEDIA_CLASS_CHANNEL, MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_DIRECTORY, MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_VIDEO,
MEDIA_TYPE_APP, MEDIA_TYPE_APP,
MEDIA_TYPE_APPS, MEDIA_TYPE_APPS,
MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNEL,
@ -75,6 +76,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
@ -523,6 +525,38 @@ async def test_services(
mock_roku.launch.assert_called_with("12") mock_roku.launch.assert_called_with("12")
async def test_services_play_media_local_source(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_roku: MagicMock,
) -> None:
"""Test the media player services related to playing media."""
local_media = hass.config.path("media")
await async_process_ha_core_config(
hass, {"media_dirs": {"local": local_media, "recordings": local_media}}
)
await hass.async_block_till_done()
assert await async_setup_component(hass, "media_source", {})
await hass.async_block_till_done()
await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: MAIN_ENTITY_ID,
ATTR_MEDIA_CONTENT_TYPE: "video/mp4",
ATTR_MEDIA_CONTENT_ID: "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4",
},
blocking=True,
)
assert mock_roku.play_video.call_count == 1
assert mock_roku.play_video.call_args
call_args = mock_roku.play_video.call_args.args
assert "/media/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[0]
@pytest.mark.parametrize("mock_roku", ["roku/rokutv-7820x.json"], indirect=True) @pytest.mark.parametrize("mock_roku", ["roku/rokutv-7820x.json"], indirect=True)
async def test_tv_services( async def test_tv_services(
hass: HomeAssistant, hass: HomeAssistant,
@ -572,7 +606,6 @@ async def test_tv_services(
mock_roku.tune.assert_called_with("55") mock_roku.tune.assert_called_with("55")
@pytest.mark.parametrize("mock_roku", ["roku/rokutv-7820x.json"], indirect=True)
async def test_media_browse( async def test_media_browse(
hass, hass,
init_integration, init_integration,
@ -582,6 +615,237 @@ async def test_media_browse(
"""Test browsing media.""" """Test browsing media."""
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
await client.send_json(
{
"id": 1,
"type": "media_player/browse_media",
"entity_id": MAIN_ENTITY_ID,
}
)
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
assert msg["result"]
assert msg["result"]["title"] == "Apps"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == MEDIA_TYPE_APPS
assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP
assert msg["result"]["can_expand"]
assert not msg["result"]["can_play"]
assert len(msg["result"]["children"]) == 8
assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP
assert msg["result"]["children"][0]["title"] == "Roku Channel Store"
assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP
assert msg["result"]["children"][0]["media_content_id"] == "11"
assert "/browse_media/app/11" in msg["result"]["children"][0]["thumbnail"]
assert msg["result"]["children"][0]["can_play"]
# test invalid media type
await client.send_json(
{
"id": 2,
"type": "media_player/browse_media",
"entity_id": MAIN_ENTITY_ID,
"media_content_type": "invalid",
"media_content_id": "invalid",
}
)
msg = await client.receive_json()
assert msg["id"] == 2
assert msg["type"] == TYPE_RESULT
assert not msg["success"]
async def test_media_browse_internal(
hass,
init_integration,
mock_roku,
hass_ws_client,
):
"""Test browsing media with internal url."""
await async_process_ha_core_config(
hass,
{"internal_url": "http://example.local:8123"},
)
assert hass.config.internal_url == "http://example.local:8123"
client = await hass_ws_client(hass)
with patch(
"homeassistant.helpers.network._get_request_host", return_value="example.local"
):
await client.send_json(
{
"id": 1,
"type": "media_player/browse_media",
"entity_id": MAIN_ENTITY_ID,
"media_content_type": MEDIA_TYPE_APPS,
"media_content_id": "apps",
}
)
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
assert msg["result"]
assert msg["result"]["title"] == "Apps"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == MEDIA_TYPE_APPS
assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP
assert msg["result"]["can_expand"]
assert not msg["result"]["can_play"]
assert len(msg["result"]["children"]) == 8
assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP
assert msg["result"]["children"][0]["title"] == "Roku Channel Store"
assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP
assert msg["result"]["children"][0]["media_content_id"] == "11"
assert "/query/icon/11" in msg["result"]["children"][0]["thumbnail"]
assert msg["result"]["children"][0]["can_play"]
async def test_media_browse_local_source(
hass,
init_integration,
mock_roku,
hass_ws_client,
):
"""Test browsing local media source."""
local_media = hass.config.path("media")
await async_process_ha_core_config(
hass, {"media_dirs": {"local": local_media, "recordings": local_media}}
)
await hass.async_block_till_done()
assert await async_setup_component(hass, "media_source", {})
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 1,
"type": "media_player/browse_media",
"entity_id": MAIN_ENTITY_ID,
}
)
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
assert msg["result"]
assert msg["result"]["title"] == "Roku"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == "root"
assert msg["result"]["can_expand"]
assert not msg["result"]["can_play"]
assert len(msg["result"]["children"]) == 2
assert msg["result"]["children"][0]["title"] == "Apps"
assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APPS
assert msg["result"]["children"][1]["title"] == "Local Media"
assert msg["result"]["children"][1]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["children"][1]["media_content_type"] is None
assert (
msg["result"]["children"][1]["media_content_id"]
== "media-source://media_source"
)
assert not msg["result"]["children"][1]["can_play"]
assert msg["result"]["children"][1]["can_expand"]
# test local media
await client.send_json(
{
"id": 2,
"type": "media_player/browse_media",
"entity_id": MAIN_ENTITY_ID,
"media_content_type": "",
"media_content_id": "media-source://media_source",
}
)
msg = await client.receive_json()
assert msg["id"] == 2
assert msg["type"] == TYPE_RESULT
assert msg["success"]
assert msg["result"]
assert msg["result"]["title"] == "Local Media"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] is None
assert len(msg["result"]["children"]) == 2
assert msg["result"]["children"][0]["title"] == "media"
assert msg["result"]["children"][0]["media_content_type"] == ""
assert (
msg["result"]["children"][0]["media_content_id"]
== "media-source://media_source/local/."
)
assert msg["result"]["children"][1]["title"] == "media"
assert msg["result"]["children"][1]["media_content_type"] == ""
assert (
msg["result"]["children"][1]["media_content_id"]
== "media-source://media_source/recordings/."
)
# test local media directory
await client.send_json(
{
"id": 3,
"type": "media_player/browse_media",
"entity_id": MAIN_ENTITY_ID,
"media_content_type": "",
"media_content_id": "media-source://media_source/local/.",
}
)
msg = await client.receive_json()
assert msg["id"] == 3
assert msg["type"] == TYPE_RESULT
assert msg["success"]
assert msg["result"]["title"] == "media"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == ""
assert len(msg["result"]["children"]) == 2
assert msg["result"]["children"][0]["title"] == "Epic Sax Guy 10 Hours.mp4"
assert msg["result"]["children"][0]["media_class"] == MEDIA_CLASS_VIDEO
assert msg["result"]["children"][0]["media_content_type"] == "video/mp4"
assert (
msg["result"]["children"][0]["media_content_id"]
== "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4"
)
@pytest.mark.parametrize("mock_roku", ["roku/rokutv-7820x.json"], indirect=True)
async def test_tv_media_browse(
hass,
init_integration,
mock_roku,
hass_ws_client,
):
"""Test browsing media."""
client = await hass_ws_client(hass)
await client.send_json( await client.send_json(
{ {
"id": 1, "id": 1,
@ -597,9 +861,9 @@ async def test_media_browse(
assert msg["success"] assert msg["success"]
assert msg["result"] assert msg["result"]
assert msg["result"]["title"] == "Media Library" assert msg["result"]["title"] == "Roku"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == "library" assert msg["result"]["media_content_type"] == "root"
assert msg["result"]["can_expand"] assert msg["result"]["can_expand"]
assert not msg["result"]["can_play"] assert not msg["result"]["can_play"]
assert len(msg["result"]["children"]) == 2 assert len(msg["result"]["children"]) == 2
@ -663,7 +927,7 @@ async def test_media_browse(
assert msg["success"] assert msg["success"]
assert msg["result"] assert msg["result"]
assert msg["result"]["title"] == "Channels" assert msg["result"]["title"] == "TV Channels"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == MEDIA_TYPE_CHANNELS assert msg["result"]["media_content_type"] == MEDIA_TYPE_CHANNELS
assert msg["result"]["children_media_class"] == MEDIA_CLASS_CHANNEL assert msg["result"]["children_media_class"] == MEDIA_CLASS_CHANNEL
@ -677,82 +941,6 @@ async def test_media_browse(
assert msg["result"]["children"][0]["media_content_id"] == "1.1" assert msg["result"]["children"][0]["media_content_id"] == "1.1"
assert msg["result"]["children"][0]["can_play"] assert msg["result"]["children"][0]["can_play"]
# test invalid media type
await client.send_json(
{
"id": 4,
"type": "media_player/browse_media",
"entity_id": TV_ENTITY_ID,
"media_content_type": "invalid",
"media_content_id": "invalid",
}
)
msg = await client.receive_json()
assert msg["id"] == 4
assert msg["type"] == TYPE_RESULT
assert not msg["success"]
@pytest.mark.parametrize("mock_roku", ["roku/rokutv-7820x.json"], indirect=True)
async def test_media_browse_internal(
hass,
init_integration,
mock_roku,
hass_ws_client,
):
"""Test browsing media with internal url."""
await async_process_ha_core_config(
hass,
{"internal_url": "http://example.local:8123"},
)
assert hass.config.internal_url == "http://example.local:8123"
client = await hass_ws_client(hass)
with patch(
"homeassistant.helpers.network._get_request_host", return_value="example.local"
):
await client.send_json(
{
"id": 2,
"type": "media_player/browse_media",
"entity_id": TV_ENTITY_ID,
"media_content_type": MEDIA_TYPE_APPS,
"media_content_id": "apps",
}
)
msg = await client.receive_json()
assert msg["id"] == 2
assert msg["type"] == TYPE_RESULT
assert msg["success"]
assert msg["result"]
assert msg["result"]["title"] == "Apps"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == MEDIA_TYPE_APPS
assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP
assert msg["result"]["can_expand"]
assert not msg["result"]["can_play"]
assert len(msg["result"]["children"]) == 11
assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP
assert msg["result"]["children"][0]["title"] == "Satellite TV"
assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP
assert msg["result"]["children"][0]["media_content_id"] == "tvinput.hdmi2"
assert "/query/icon/tvinput.hdmi2" in msg["result"]["children"][0]["thumbnail"]
assert msg["result"]["children"][0]["can_play"]
assert msg["result"]["children"][3]["title"] == "Roku Channel Store"
assert msg["result"]["children"][3]["media_content_type"] == MEDIA_TYPE_APP
assert msg["result"]["children"][3]["media_content_id"] == "11"
assert "/query/icon/11" in msg["result"]["children"][3]["thumbnail"]
assert msg["result"]["children"][3]["can_play"]
async def test_integration_services( async def test_integration_services(
hass: HomeAssistant, hass: HomeAssistant,