Add browse media to forked-daapd (#79009)

* Add browse media to forked-daapd

* Use elif in async_browse_image

* Add tests

* Add tests

* Add test

* Fix test
This commit is contained in:
uvjustin
2022-09-26 20:40:23 -04:00
committed by GitHub
parent 1e3a3d32ad
commit 499c3410d1
6 changed files with 776 additions and 59 deletions
+28
View File
@@ -0,0 +1,28 @@
"""Common fixtures for forked_daapd tests."""
import pytest
from homeassistant.components.forked_daapd.const import CONF_TTS_PAUSE_TIME, DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from tests.common import MockConfigEntry
@pytest.fixture(name="config_entry")
def config_entry_fixture():
"""Create hass config_entry fixture."""
data = {
CONF_HOST: "192.168.1.1",
CONF_PORT: "2345",
CONF_PASSWORD: "",
}
return MockConfigEntry(
version=1,
domain=DOMAIN,
title="",
data=data,
options={CONF_TTS_PAUSE_TIME: 0},
source=SOURCE_USER,
entry_id=1,
)
@@ -0,0 +1,294 @@
"""Media browsing tests for the forked_daapd media player platform."""
from http import HTTPStatus
from unittest.mock import patch
from homeassistant.components import media_source
from homeassistant.components.forked_daapd.browse_media import create_media_content_id
from homeassistant.components.media_player import MediaType
from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.setup import async_setup_component
TEST_MASTER_ENTITY_NAME = "media_player.forked_daapd_server"
async def test_async_browse_media(hass, hass_ws_client, config_entry):
"""Test browse media."""
assert await async_setup_component(hass, media_source.DOMAIN, {})
await hass.async_block_till_done()
with patch(
"homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI",
autospec=True,
) as mock_api:
config_entry.add_to_hass(hass)
await config_entry.async_setup(hass)
await hass.async_block_till_done()
mock_api.return_value.full_url = lambda x: "http://owntone_instance/" + x
mock_api.return_value.get_directory.side_effect = [
{
"directories": [
{"path": "/music/srv/Audiobooks"},
{"path": "/music/srv/Music"},
{"path": "/music/srv/Playlists"},
{"path": "/music/srv/Podcasts"},
],
"tracks": {
"items": [
{
"id": 1,
"title": "input.pipe",
"artist": "Unknown artist",
"artist_sort": "Unknown artist",
"album": "Unknown album",
"album_sort": "Unknown album",
"album_id": "4201163758598356043",
"album_artist": "Unknown artist",
"album_artist_sort": "Unknown artist",
"album_artist_id": "4187901437947843388",
"genre": "Unknown genre",
"year": 0,
"track_number": 0,
"disc_number": 0,
"length_ms": 0,
"play_count": 0,
"skip_count": 0,
"time_added": "2018-11-24T08:41:35Z",
"seek_ms": 0,
"media_kind": "music",
"data_kind": "pipe",
"path": "/music/srv/input.pipe",
"uri": "library:track:1",
"artwork_url": "/artwork/item/1",
}
],
"total": 1,
"offset": 0,
"limit": -1,
},
"playlists": {
"items": [
{
"id": 8,
"name": "radio",
"path": "/music/srv/radio.m3u",
"smart_playlist": True,
"uri": "library:playlist:8",
}
],
"total": 1,
"offset": 0,
"limit": -1,
},
}
] + 4 * [
{"directories": [], "tracks": {"items": []}, "playlists": {"items": []}}
]
mock_api.return_value.get_albums.return_value = [
{
"id": "8009851123233197743",
"name": "Add Violence",
"name_sort": "Add Violence",
"artist": "Nine Inch Nails",
"artist_id": "32561671101664759",
"track_count": 5,
"length_ms": 1634961,
"uri": "library:album:8009851123233197743",
},
]
mock_api.return_value.get_artists.return_value = [
{
"id": "3815427709949443149",
"name": "ABAY",
"name_sort": "ABAY",
"album_count": 1,
"track_count": 10,
"length_ms": 2951554,
"uri": "library:artist:3815427709949443149",
},
]
mock_api.return_value.get_genres.return_value = [
{"name": "Classical"},
{"name": "Drum & Bass"},
{"name": "Pop"},
{"name": "Rock/Pop"},
{"name": "'90s Alternative"},
]
mock_api.return_value.get_playlists.return_value = [
{
"id": 1,
"name": "radio",
"path": "/music/srv/radio.m3u",
"smart_playlist": False,
"uri": "library:playlist:1",
},
]
# Request playlist through WebSocket
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 1,
"type": "media_player/browse_media",
"entity_id": TEST_MASTER_ENTITY_NAME,
}
)
msg = await client.receive_json()
# Assert WebSocket response
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
msg_id = 2
async def browse_children(children):
"""Browse the children of this BrowseMedia."""
nonlocal msg_id
for child in children:
if child["can_expand"]:
print("EXPANDING CHILD", child)
await client.send_json(
{
"id": msg_id,
"type": "media_player/browse_media",
"entity_id": TEST_MASTER_ENTITY_NAME,
"media_content_type": child["media_content_type"],
"media_content_id": child["media_content_id"],
}
)
msg = await client.receive_json()
assert msg["success"]
msg_id += 1
await browse_children(msg["result"]["children"])
await browse_children(msg["result"]["children"])
async def test_async_browse_media_not_found(hass, hass_ws_client, config_entry):
"""Test browse media not found."""
assert await async_setup_component(hass, media_source.DOMAIN, {})
await hass.async_block_till_done()
with patch(
"homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI",
autospec=True,
) as mock_api:
config_entry.add_to_hass(hass)
await config_entry.async_setup(hass)
await hass.async_block_till_done()
mock_api.return_value.get_directory.return_value = None
mock_api.return_value.get_albums.return_value = None
mock_api.return_value.get_artists.return_value = None
mock_api.return_value.get_genres.return_value = None
mock_api.return_value.get_playlists.return_value = None
# Request playlist through WebSocket
client = await hass_ws_client(hass)
msg_id = 1
for media_type in (
"directory",
MediaType.ALBUM,
MediaType.ARTIST,
MediaType.GENRE,
MediaType.PLAYLIST,
):
await client.send_json(
{
"id": msg_id,
"type": "media_player/browse_media",
"entity_id": TEST_MASTER_ENTITY_NAME,
"media_content_type": media_type,
"media_content_id": (
media_content_id := create_media_content_id(
"title", f"library:{media_type}:"
)
),
}
)
msg = await client.receive_json()
# Assert WebSocket response
assert msg["id"] == msg_id
assert msg["type"] == TYPE_RESULT
assert not msg["success"]
assert (
msg["error"]["message"]
== f"Media not found for {media_type} / {media_content_id}"
)
msg_id += 1
async def test_async_browse_image(hass, hass_client, config_entry):
"""Test browse media images."""
with patch(
"homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI",
autospec=True,
) as mock_api:
config_entry.add_to_hass(hass)
await config_entry.async_setup(hass)
await hass.async_block_till_done()
client = await hass_client()
mock_api.return_value.full_url = lambda x: "http://owntone_instance/" + x
mock_api.return_value.get_albums.return_value = [
{"id": "8009851123233197743", "artwork_url": "some_album_image"},
]
mock_api.return_value.get_artists.return_value = [
{"id": "3815427709949443149", "artwork_url": "some_artist_image"},
]
mock_api.return_value.get_track.return_value = {
"id": 456,
"artwork_url": "some_track_image",
}
media_content_id = create_media_content_id(
"title", media_type=MediaType.ALBUM, id_or_path="8009851123233197743"
)
with patch(
"homeassistant.components.media_player.async_fetch_image"
) as mock_fetch_image:
for media_type, media_id in (
(MediaType.ALBUM, "8009851123233197743"),
(MediaType.ARTIST, "3815427709949443149"),
(MediaType.TRACK, "456"),
):
mock_fetch_image.return_value = (b"image_bytes", media_type)
media_content_id = create_media_content_id(
"title", media_type=media_type, id_or_path=media_id
)
resp = await client.get(
f"/api/media_player_proxy/{TEST_MASTER_ENTITY_NAME}/browse_media/{media_type}/{media_content_id}"
)
assert (
mock_fetch_image.call_args[0][2]
== f"http://owntone_instance/some_{media_type}_image"
)
assert resp.status == HTTPStatus.OK
assert resp.content_type == media_type
assert await resp.read() == b"image_bytes"
async def test_async_browse_image_missing(hass, hass_client, config_entry, caplog):
"""Test browse media images with no image available."""
with patch(
"homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI",
autospec=True,
) as mock_api:
config_entry.add_to_hass(hass)
await config_entry.async_setup(hass)
await hass.async_block_till_done()
client = await hass_client()
mock_api.return_value.full_url = lambda x: "http://owntone_instance/" + x
mock_api.return_value.get_track.return_value = {}
media_content_id = create_media_content_id(
"title", media_type=MediaType.TRACK, id_or_path="456"
)
resp = await client.get(
f"/api/media_player_proxy/{TEST_MASTER_ENTITY_NAME}/browse_media/{MediaType.TRACK}/{media_content_id}"
)
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
@@ -4,12 +4,12 @@ from unittest.mock import patch
import pytest
from homeassistant.components.forked_daapd.browse_media import create_media_content_id
from homeassistant.components.forked_daapd.const import (
CONF_LIBRESPOT_JAVA_PORT,
CONF_MAX_PLAYLISTS,
CONF_TTS_PAUSE_TIME,
CONF_TTS_VOLUME,
DOMAIN,
SIGNAL_UPDATE_OUTPUTS,
SIGNAL_UPDATE_PLAYER,
SIGNAL_UPDATE_QUEUE,
@@ -54,25 +54,21 @@ from homeassistant.components.media_player import (
MediaPlayerEnqueue,
MediaType,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_SUPPORTED_FEATURES,
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
STATE_ON,
STATE_PAUSED,
STATE_UNAVAILABLE,
)
from tests.common import MockConfigEntry, async_mock_signal
from tests.common import async_mock_signal
TEST_MASTER_ENTITY_NAME = "media_player.forked_daapd_server"
TEST_ZONE_ENTITY_NAMES = [
"media_player.forked_daapd_output_" + x
for x in ["kitchen", "computer", "daapd_fifo"]
for x in ("kitchen", "computer", "daapd_fifo")
]
OPTIONS_DATA = {
@@ -290,25 +286,6 @@ SAMPLE_PIPES = [
SAMPLE_PLAYLISTS = [{"id": 7, "name": "test_playlist", "uri": "library:playlist:2"}]
@pytest.fixture(name="config_entry")
def config_entry_fixture():
"""Create hass config_entry fixture."""
data = {
CONF_HOST: "192.168.1.1",
CONF_PORT: "2345",
CONF_PASSWORD: "",
}
return MockConfigEntry(
version=1,
domain=DOMAIN,
title="",
data=data,
options={CONF_TTS_PAUSE_TIME: 0},
source=SOURCE_USER,
entry_id=1,
)
@pytest.fixture(name="get_request_return_values")
async def get_request_return_values_fixture():
"""Get request return values we can change later."""
@@ -888,3 +865,29 @@ async def test_async_play_media_enqueue(hass, mock_api_object):
mock_api_object.add_to_queue.assert_called_with(
uris="http://example.com/next.mp3", playback="start", position=1
)
async def test_play_owntone_media(hass, mock_api_object):
"""Test async play media with an owntone source."""
initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME)
await _service_call(
hass,
TEST_MASTER_ENTITY_NAME,
SERVICE_PLAY_MEDIA,
{
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: create_media_content_id(
"some song", "library:track:456"
),
ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.PLAY,
},
)
state = hass.states.get(TEST_MASTER_ENTITY_NAME)
assert state.state == initial_state.state
assert state.last_updated > initial_state.last_updated
mock_api_object.add_to_queue.assert_called_with(
uris="library:track:456",
playback="start",
position=0,
playback_from_position=0,
)