Allow roku to browse and play local media (#64799)
parent
8ea2f865ed
commit
3e29fe5a67
|
@ -1,5 +1,11 @@
|
|||
"""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.const import (
|
||||
MEDIA_CLASS_APP,
|
||||
|
@ -10,6 +16,11 @@ from homeassistant.components.media_player.const import (
|
|||
MEDIA_TYPE_CHANNEL,
|
||||
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 = {
|
||||
MEDIA_TYPE_APP: MEDIA_CLASS_APP,
|
||||
|
@ -33,8 +44,133 @@ EXPANDABLE_MEDIA_TYPES = [
|
|||
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."""
|
||||
search_id = payload["search_id"]
|
||||
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
|
||||
elif search_type == MEDIA_TYPE_CHANNELS:
|
||||
title = "Channels"
|
||||
title = "TV Channels"
|
||||
media = [
|
||||
{
|
||||
"channel_number": item.number,
|
||||
|
@ -63,7 +199,7 @@ def build_item_response(coordinator, payload, get_thumbnail_url=None):
|
|||
]
|
||||
children_media_class = MEDIA_CLASS_CHANNEL
|
||||
|
||||
if media is None:
|
||||
if title is None or media is None:
|
||||
return None
|
||||
|
||||
return BrowseMedia(
|
||||
|
@ -75,13 +211,19 @@ def build_item_response(coordinator, payload, get_thumbnail_url=None):
|
|||
title=title,
|
||||
can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id,
|
||||
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,
|
||||
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.
|
||||
|
||||
|
@ -92,8 +234,7 @@ def item_payload(item, coordinator, get_thumbnail_url=None):
|
|||
if "app_id" in item:
|
||||
media_content_type = MEDIA_TYPE_APP
|
||||
media_content_id = item["app_id"]
|
||||
if get_thumbnail_url:
|
||||
thumbnail = get_thumbnail_url(media_content_type, media_content_id)
|
||||
thumbnail = get_browse_image_url(media_content_type, media_content_id, None)
|
||||
elif "channel_number" in item:
|
||||
media_content_type = MEDIA_TYPE_CHANNEL
|
||||
media_content_id = item["channel_number"]
|
||||
|
@ -114,52 +255,3 @@ def item_payload(item, coordinator, get_thumbnail_url=None):
|
|||
can_expand=can_expand,
|
||||
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
|
||||
|
|
|
@ -4,9 +4,12 @@ from __future__ import annotations
|
|||
import datetime as dt
|
||||
import logging
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
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 (
|
||||
BrowseMedia,
|
||||
MediaPlayerDeviceClass,
|
||||
|
@ -29,7 +32,6 @@ from homeassistant.components.media_player.const import (
|
|||
SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_VOLUME_STEP,
|
||||
)
|
||||
from homeassistant.components.media_player.errors import BrowseError
|
||||
from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, HLS_PROVIDER
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
|
@ -44,10 +46,10 @@ from homeassistant.const import (
|
|||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_platform
|
||||
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 .browse_media import build_item_response, library_payload
|
||||
from .browse_media import async_browse_media
|
||||
from .const import (
|
||||
ATTR_CONTENT_ID,
|
||||
ATTR_FORMAT,
|
||||
|
@ -287,35 +289,13 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
|||
media_content_id: str | None = None,
|
||||
) -> BrowseMedia:
|
||||
"""Implement the websocket media browsing helper."""
|
||||
is_internal = is_internal_request(self.hass)
|
||||
|
||||
def _get_thumbnail_url(
|
||||
media_content_type, media_content_id, media_image_id=None
|
||||
):
|
||||
if is_internal:
|
||||
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
|
||||
return await async_browse_media(
|
||||
self.hass,
|
||||
self.coordinator,
|
||||
self.get_browse_image_url,
|
||||
media_content_id,
|
||||
media_content_type,
|
||||
)
|
||||
|
||||
@roku_exception_handler
|
||||
async def async_turn_on(self) -> None:
|
||||
|
@ -380,9 +360,27 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
|||
|
||||
@roku_exception_handler
|
||||
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 {}
|
||||
|
||||
# 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:
|
||||
_LOGGER.error(
|
||||
"Invalid media type %s. Only %s, %s, %s, and camera HLS streams are supported",
|
||||
|
|
|
@ -22,6 +22,7 @@ from homeassistant.components.media_player.const import (
|
|||
MEDIA_CLASS_APP,
|
||||
MEDIA_CLASS_CHANNEL,
|
||||
MEDIA_CLASS_DIRECTORY,
|
||||
MEDIA_CLASS_VIDEO,
|
||||
MEDIA_TYPE_APP,
|
||||
MEDIA_TYPE_APPS,
|
||||
MEDIA_TYPE_CHANNEL,
|
||||
|
@ -75,6 +76,7 @@ from homeassistant.const import (
|
|||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
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 tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
@ -523,6 +525,38 @@ async def test_services(
|
|||
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)
|
||||
async def test_tv_services(
|
||||
hass: HomeAssistant,
|
||||
|
@ -572,7 +606,6 @@ async def test_tv_services(
|
|||
mock_roku.tune.assert_called_with("55")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_roku", ["roku/rokutv-7820x.json"], indirect=True)
|
||||
async def test_media_browse(
|
||||
hass,
|
||||
init_integration,
|
||||
|
@ -582,6 +615,237 @@ async def test_media_browse(
|
|||
"""Test browsing media."""
|
||||
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(
|
||||
{
|
||||
"id": 1,
|
||||
|
@ -597,9 +861,9 @@ async def test_media_browse(
|
|||
assert msg["success"]
|
||||
|
||||
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_content_type"] == "library"
|
||||
assert msg["result"]["media_content_type"] == "root"
|
||||
assert msg["result"]["can_expand"]
|
||||
assert not msg["result"]["can_play"]
|
||||
assert len(msg["result"]["children"]) == 2
|
||||
|
@ -663,7 +927,7 @@ async def test_media_browse(
|
|||
assert msg["success"]
|
||||
|
||||
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_content_type"] == MEDIA_TYPE_CHANNELS
|
||||
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]["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(
|
||||
hass: HomeAssistant,
|
||||
|
|
Loading…
Reference in New Issue