Media Source implementation for Chromecast (#39305)
* Implement local media finder and integrate into cast * update to media source as a platform * Tweak media source design * fix websocket and local source * fix websocket schema * fix playing media * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Add resolve_media websocket * Register that shit * Square brackets * Sign path * add support for multiple media sources and address PR review * fix lint * fix tests from auto whitelisting config/media * allow specifying a name on the media source * add tests * fix for python 3.7 * Apply suggestions from code review Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> * add http back to cast and remove guess_type from executor as there is no i/o Co-authored-by: Paulus Schoutsen <balloob@gmail.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
f01a0f9151
commit
f2b3e63ff6
18 changed files with 738 additions and 6 deletions
1
tests/components/media_source/__init__.py
Normal file
1
tests/components/media_source/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""The tests for Media Source integration."""
|
158
tests/components/media_source/test_init.py
Normal file
158
tests/components/media_source/test_init.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
"""Test Media Source initialization."""
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player.errors import BrowseError
|
||||
from homeassistant.components.media_source import const
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.async_mock import patch
|
||||
|
||||
|
||||
async def test_is_media_source_id():
|
||||
"""Test media source validation."""
|
||||
assert media_source.is_media_source_id(const.URI_SCHEME)
|
||||
assert media_source.is_media_source_id(f"{const.URI_SCHEME}domain")
|
||||
assert media_source.is_media_source_id(f"{const.URI_SCHEME}domain/identifier")
|
||||
assert not media_source.is_media_source_id("test")
|
||||
|
||||
|
||||
async def test_generate_media_source_id():
|
||||
"""Test identifier generation."""
|
||||
tests = [
|
||||
(None, None),
|
||||
(None, ""),
|
||||
("", ""),
|
||||
("domain", None),
|
||||
("domain", ""),
|
||||
("domain", "identifier"),
|
||||
]
|
||||
|
||||
for domain, identifier in tests:
|
||||
assert media_source.is_media_source_id(
|
||||
media_source.generate_media_source_id(domain, identifier)
|
||||
)
|
||||
|
||||
|
||||
async def test_async_browse_media(hass):
|
||||
"""Test browse media."""
|
||||
assert await async_setup_component(hass, const.DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test non-media ignored (/media has test.mp3 and not_media.txt)
|
||||
media = await media_source.async_browse_media(hass, "")
|
||||
assert isinstance(media, media_source.models.BrowseMedia)
|
||||
assert media.name == "media/"
|
||||
assert len(media.children) == 1
|
||||
|
||||
# Test invalid media content
|
||||
with pytest.raises(ValueError):
|
||||
await media_source.async_browse_media(hass, "invalid")
|
||||
|
||||
# Test base URI returns all domains
|
||||
media = await media_source.async_browse_media(hass, const.URI_SCHEME)
|
||||
assert isinstance(media, media_source.models.BrowseMedia)
|
||||
assert len(media.children) == 1
|
||||
assert media.children[0].name == "Local Media"
|
||||
|
||||
|
||||
async def test_async_resolve_media(hass):
|
||||
"""Test browse media."""
|
||||
assert await async_setup_component(hass, const.DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test no media content
|
||||
media = await media_source.async_resolve_media(hass, "")
|
||||
assert isinstance(media, media_source.models.PlayMedia)
|
||||
|
||||
|
||||
async def test_websocket_browse_media(hass, hass_ws_client):
|
||||
"""Test browse media websocket."""
|
||||
assert await async_setup_component(hass, const.DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
media = media_source.models.BrowseMedia(const.DOMAIN, "/media", False, True)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.media_source.async_browse_media",
|
||||
return_value=media,
|
||||
):
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "media_source/browse_media",
|
||||
}
|
||||
)
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
assert msg["id"] == 1
|
||||
assert media.to_media_player_item() == msg["result"]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.media_source.async_browse_media",
|
||||
side_effect=BrowseError("test"),
|
||||
):
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 2,
|
||||
"type": "media_source/browse_media",
|
||||
"media_content_id": "invalid",
|
||||
}
|
||||
)
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == "browse_media_failed"
|
||||
assert msg["error"]["message"] == "test"
|
||||
|
||||
|
||||
async def test_websocket_resolve_media(hass, hass_ws_client):
|
||||
"""Test browse media websocket."""
|
||||
assert await async_setup_component(hass, const.DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
media = media_source.models.PlayMedia("/media/test.mp3", "audio/mpeg")
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.media_source.async_resolve_media",
|
||||
return_value=media,
|
||||
):
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "media_source/resolve_media",
|
||||
"media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/media/test.mp3",
|
||||
}
|
||||
)
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
assert msg["id"] == 1
|
||||
assert msg["result"]["url"].startswith(media.url)
|
||||
assert msg["result"]["mime_type"] == media.mime_type
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.media_source.async_resolve_media",
|
||||
side_effect=media_source.Unresolvable("test"),
|
||||
):
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 2,
|
||||
"type": "media_source/resolve_media",
|
||||
"media_content_id": "invalid",
|
||||
}
|
||||
)
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == "resolve_media_failed"
|
||||
assert msg["error"]["message"] == "test"
|
66
tests/components/media_source/test_local_source.py
Normal file
66
tests/components/media_source/test_local_source.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
"""Test Local Media Source."""
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_source import const
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
async def test_async_browse_media(hass):
|
||||
"""Test browse media."""
|
||||
assert await async_setup_component(hass, const.DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test path not exists
|
||||
with pytest.raises(media_source.BrowseError) as excinfo:
|
||||
await media_source.async_browse_media(
|
||||
hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/test/not/exist"
|
||||
)
|
||||
assert str(excinfo.value) == "Path does not exist."
|
||||
|
||||
# Test browse file
|
||||
with pytest.raises(media_source.BrowseError) as excinfo:
|
||||
await media_source.async_browse_media(
|
||||
hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/test.mp3"
|
||||
)
|
||||
assert str(excinfo.value) == "Path is not a directory."
|
||||
|
||||
# Test invalid base
|
||||
with pytest.raises(media_source.BrowseError) as excinfo:
|
||||
await media_source.async_browse_media(
|
||||
hass, f"{const.URI_SCHEME}{const.DOMAIN}/invalid/base"
|
||||
)
|
||||
assert str(excinfo.value) == "Unknown source directory."
|
||||
|
||||
# Test directory traversal
|
||||
with pytest.raises(media_source.BrowseError) as excinfo:
|
||||
await media_source.async_browse_media(
|
||||
hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/../configuration.yaml"
|
||||
)
|
||||
assert str(excinfo.value) == "Invalid path."
|
||||
|
||||
# Test successful listing
|
||||
media = await media_source.async_browse_media(
|
||||
hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/."
|
||||
)
|
||||
assert media
|
||||
|
||||
|
||||
async def test_media_view(hass, hass_client):
|
||||
"""Test media view."""
|
||||
assert await async_setup_component(hass, const.DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
# Protects against non-existent files
|
||||
resp = await client.get("/media/invalid.txt")
|
||||
assert resp.status == 404
|
||||
|
||||
# Protects against non-media files
|
||||
resp = await client.get("/media/not_media.txt")
|
||||
assert resp.status == 404
|
||||
|
||||
# Fetch available media
|
||||
resp = await client.get("/media/test.mp3")
|
||||
assert resp.status == 200
|
27
tests/components/media_source/test_models.py
Normal file
27
tests/components/media_source/test_models.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
"""Test Media Source model methods."""
|
||||
from homeassistant.components.media_source import const, models
|
||||
|
||||
|
||||
async def test_browse_media_to_media_player_item():
|
||||
"""Test BrowseMedia conversion to media player item dict."""
|
||||
base = models.BrowseMedia(const.DOMAIN, "media", "media/", False, True)
|
||||
base.children = [
|
||||
models.BrowseMedia(
|
||||
const.DOMAIN, "media/test.mp3", "test.mp3", True, False, "audio/mp3"
|
||||
)
|
||||
]
|
||||
|
||||
item = base.to_media_player_item()
|
||||
assert item["title"] == "media/"
|
||||
assert item["media_content_type"] == "folder"
|
||||
assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media"
|
||||
assert not item["can_play"]
|
||||
assert item["can_expand"]
|
||||
assert len(item["children"]) == 1
|
||||
assert item["children"][0]["title"] == "test.mp3"
|
||||
|
||||
|
||||
async def test_media_source_default_name():
|
||||
"""Test MediaSource uses domain as default name."""
|
||||
source = models.MediaSource(const.DOMAIN)
|
||||
assert source.name == const.DOMAIN
|
Loading…
Add table
Add a link
Reference in a new issue