Support announce and enqueue in forked-daapd (#77744)

This commit is contained in:
uvjustin 2022-09-21 11:08:28 -07:00 committed by GitHub
parent e079968ef4
commit 420285f7ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 164 additions and 21 deletions

View File

@ -12,7 +12,10 @@ from pylibrespot_java import LibrespotJavaAPI
from homeassistant.components import media_source
from homeassistant.components.media_player import (
ATTR_MEDIA_ANNOUNCE,
ATTR_MEDIA_ENQUEUE,
BrowseMedia,
MediaPlayerEnqueue,
MediaPlayerEntity,
MediaPlayerState,
MediaType,
@ -349,11 +352,8 @@ class ForkedDaapdMaster(MediaPlayerEntity):
@callback
def _update_queue(self, queue, event):
self._queue = queue
if (
self._tts_requested
and self._queue["count"] == 1
and self._queue["items"][0]["uri"].find("tts_proxy") != -1
):
if self._tts_requested:
# Assume the change was due to the request
self._tts_requested = False
self._tts_queued = True
@ -669,10 +669,48 @@ class ForkedDaapdMaster(MediaPlayerEntity):
if media_type == MediaType.MUSIC:
media_id = async_process_play_media_url(self.hass, media_id)
elif media_type not in CAN_PLAY_TYPE:
_LOGGER.warning("Media type '%s' not supported", media_type)
return
await self._async_announce(media_id)
else:
_LOGGER.debug("Media type '%s' not supported", media_type)
if kwargs.get(ATTR_MEDIA_ANNOUNCE):
return await self._async_announce(media_id)
# if kwargs[ATTR_MEDIA_ENQUEUE] is None, we assume MediaPlayerEnqueue.REPLACE
# if kwargs[ATTR_MEDIA_ENQUEUE] is True, we assume MediaPlayerEnqueue.ADD
# kwargs[ATTR_MEDIA_ENQUEUE] is assumed to never be False
# See https://github.com/home-assistant/architecture/issues/765
enqueue: bool | MediaPlayerEnqueue = kwargs.get(
ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE
)
if enqueue in {True, MediaPlayerEnqueue.ADD, MediaPlayerEnqueue.REPLACE}:
return await self._api.add_to_queue(
uris=media_id,
playback="start",
clear=enqueue == MediaPlayerEnqueue.REPLACE,
)
current_position = next(
(
item["position"]
for item in self._queue["items"]
if item["id"] == self._player["item_id"]
),
0,
)
if enqueue == MediaPlayerEnqueue.NEXT:
return await self._api.add_to_queue(
uris=media_id,
playback="start",
position=current_position + 1,
)
# enqueue == MediaPlayerEnqueue.PLAY
return await self._api.add_to_queue(
uris=media_id,
playback="start",
position=current_position,
playback_from_position=current_position,
)
async def _async_announce(self, media_id: str) -> None:
"""Play a URI."""

View File

@ -22,10 +22,12 @@ from homeassistant.components.media_player import (
ATTR_INPUT_SOURCE,
ATTR_MEDIA_ALBUM_ARTIST,
ATTR_MEDIA_ALBUM_NAME,
ATTR_MEDIA_ANNOUNCE,
ATTR_MEDIA_ARTIST,
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_DURATION,
ATTR_MEDIA_ENQUEUE,
ATTR_MEDIA_POSITION,
ATTR_MEDIA_SEEK_POSITION,
ATTR_MEDIA_SHUFFLE,
@ -49,6 +51,7 @@ from homeassistant.components.media_player import (
SERVICE_TURN_ON,
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
MediaPlayerEnqueue,
MediaType,
)
from homeassistant.config_entries import SOURCE_USER
@ -112,6 +115,28 @@ SAMPLE_PLAYER_STOPPED = {
"item_progress_ms": 5,
}
SAMPLE_QUEUE = {
"version": 833,
"count": 1,
"items": [
{
"id": 12322,
"position": 0,
"track_id": 1234,
"title": "Some song",
"artist": "Some artist",
"album": "No album",
"album_artist": "The xx",
"artwork_url": "http://art",
"length_ms": 0,
"track_number": 1,
"media_kind": "music",
"data_kind": "url",
"uri": "library:track:5",
}
],
}
SAMPLE_QUEUE_TTS = {
"version": 833,
"count": 1,
@ -291,7 +316,7 @@ async def get_request_return_values_fixture():
"config": SAMPLE_CONFIG,
"outputs": SAMPLE_OUTPUTS_ON,
"player": SAMPLE_PLAYER_PAUSED,
"queue": SAMPLE_QUEUE_TTS,
"queue": SAMPLE_QUEUE,
}
@ -323,13 +348,12 @@ async def mock_api_object_fixture(hass, config_entry, get_request_return_values)
await hass.async_block_till_done()
async def add_to_queue_side_effect(
uris, playback=None, playback_from_position=None, clear=None
uris, playback=None, position=None, playback_from_position=None, clear=None
):
await updater_update(["queue", "player"])
mock_api.return_value.add_to_queue.side_effect = (
add_to_queue_side_effect # for play_media testing
)
# for play_media testing
mock_api.return_value.add_to_queue.side_effect = add_to_queue_side_effect
async def pause_side_effect():
await updater_update(["player"])
@ -361,8 +385,8 @@ def test_master_state(hass, mock_api_object):
assert state.attributes[ATTR_MEDIA_DURATION] == 0.05
assert state.attributes[ATTR_MEDIA_POSITION] == 0.005
assert state.attributes[ATTR_MEDIA_TITLE] == "No album" # reversed for url
assert state.attributes[ATTR_MEDIA_ARTIST] == "Google"
assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "Short TTS file" # reversed
assert state.attributes[ATTR_MEDIA_ARTIST] == "Some artist"
assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "Some song" # reversed
assert state.attributes[ATTR_MEDIA_ALBUM_ARTIST] == "The xx"
assert state.attributes[ATTR_MEDIA_TRACK] == 1
assert not state.attributes[ATTR_MEDIA_SHUFFLE]
@ -562,16 +586,18 @@ async def test_async_play_media_from_paused(hass, mock_api_object):
assert state.last_updated > initial_state.last_updated
async def test_async_play_media_from_stopped(
async def test_async_play_media_announcement_from_stopped(
hass, get_request_return_values, mock_api_object
):
"""Test async play media from stopped."""
"""Test async play media announcement (from stopped)."""
updater_update = mock_api_object.start_websocket_handler.call_args[0][2]
get_request_return_values["player"] = SAMPLE_PLAYER_STOPPED
await updater_update(["player"])
await hass.async_block_till_done()
initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME)
get_request_return_values["queue"] = SAMPLE_QUEUE_TTS
await _service_call(
hass,
TEST_MASTER_ENTITY_NAME,
@ -579,6 +605,7 @@ async def test_async_play_media_from_stopped(
{
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3",
ATTR_MEDIA_ANNOUNCE: True,
},
)
state = hass.states.get(TEST_MASTER_ENTITY_NAME)
@ -602,8 +629,8 @@ async def test_async_play_media_unsupported(hass, mock_api_object):
assert state.last_updated == initial_state.last_updated
async def test_async_play_media_tts_timeout(hass, mock_api_object):
"""Test async play media with TTS timeout."""
async def test_async_play_media_announcement_tts_timeout(hass, mock_api_object):
"""Test async play media announcement with TTS timeout."""
mock_api_object.add_to_queue.side_effect = None
with patch("homeassistant.components.forked_daapd.media_player.TTS_TIMEOUT", 0):
initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME)
@ -614,6 +641,7 @@ async def test_async_play_media_tts_timeout(hass, mock_api_object):
{
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3",
ATTR_MEDIA_ANNOUNCE: True,
},
)
state = hass.states.get(TEST_MASTER_ENTITY_NAME)
@ -713,8 +741,8 @@ async def test_librespot_java_stuff(
assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "some album"
async def test_librespot_java_play_media(hass, pipe_control_api_object):
"""Test play media with librespot-java pipe."""
async def test_librespot_java_play_announcement(hass, pipe_control_api_object):
"""Test play announcement with librespot-java pipe."""
initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME)
await _service_call(
hass,
@ -723,6 +751,7 @@ async def test_librespot_java_play_media(hass, pipe_control_api_object):
{
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3",
ATTR_MEDIA_ANNOUNCE: True,
},
)
state = hass.states.get(TEST_MASTER_ENTITY_NAME)
@ -783,3 +812,79 @@ async def test_websocket_disconnect(hass, mock_api_object):
await hass.async_block_till_done()
assert hass.states.get(TEST_MASTER_ENTITY_NAME).state == STATE_UNAVAILABLE
assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]).state == STATE_UNAVAILABLE
async def test_async_play_media_enqueue(hass, mock_api_object):
"""Test async play media with different enqueue options."""
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: "http://example.com/play.mp3",
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="http://example.com/play.mp3",
playback="start",
position=0,
playback_from_position=0,
)
await _service_call(
hass,
TEST_MASTER_ENTITY_NAME,
SERVICE_PLAY_MEDIA,
{
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: "http://example.com/replace.mp3",
ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.REPLACE,
},
)
mock_api_object.add_to_queue.assert_called_with(
uris="http://example.com/replace.mp3", playback="start", clear=True
)
await _service_call(
hass,
TEST_MASTER_ENTITY_NAME,
SERVICE_PLAY_MEDIA,
{
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: "http://example.com/add.mp3",
ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.ADD,
},
)
mock_api_object.add_to_queue.assert_called_with(
uris="http://example.com/add.mp3", playback="start", clear=False
)
await _service_call(
hass,
TEST_MASTER_ENTITY_NAME,
SERVICE_PLAY_MEDIA,
{
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: "http://example.com/add.mp3",
ATTR_MEDIA_ENQUEUE: True,
},
)
mock_api_object.add_to_queue.assert_called_with(
uris="http://example.com/add.mp3", playback="start", clear=False
)
await _service_call(
hass,
TEST_MASTER_ENTITY_NAME,
SERVICE_PLAY_MEDIA,
{
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: "http://example.com/next.mp3",
ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.NEXT,
},
)
mock_api_object.add_to_queue.assert_called_with(
uris="http://example.com/next.mp3", playback="start", position=1
)