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 import media_source
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
ATTR_MEDIA_ANNOUNCE,
ATTR_MEDIA_ENQUEUE,
BrowseMedia, BrowseMedia,
MediaPlayerEnqueue,
MediaPlayerEntity, MediaPlayerEntity,
MediaPlayerState, MediaPlayerState,
MediaType, MediaType,
@ -349,11 +352,8 @@ class ForkedDaapdMaster(MediaPlayerEntity):
@callback @callback
def _update_queue(self, queue, event): def _update_queue(self, queue, event):
self._queue = queue self._queue = queue
if ( if self._tts_requested:
self._tts_requested # Assume the change was due to the request
and self._queue["count"] == 1
and self._queue["items"][0]["uri"].find("tts_proxy") != -1
):
self._tts_requested = False self._tts_requested = False
self._tts_queued = True self._tts_queued = True
@ -669,10 +669,48 @@ class ForkedDaapdMaster(MediaPlayerEntity):
if media_type == MediaType.MUSIC: if media_type == MediaType.MUSIC:
media_id = async_process_play_media_url(self.hass, media_id) 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) if kwargs.get(ATTR_MEDIA_ANNOUNCE):
else: return await self._async_announce(media_id)
_LOGGER.debug("Media type '%s' not supported", media_type)
# 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: async def _async_announce(self, media_id: str) -> None:
"""Play a URI.""" """Play a URI."""

View File

@ -22,10 +22,12 @@ from homeassistant.components.media_player import (
ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE,
ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_ARTIST,
ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ALBUM_NAME,
ATTR_MEDIA_ANNOUNCE,
ATTR_MEDIA_ARTIST, ATTR_MEDIA_ARTIST,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_DURATION, ATTR_MEDIA_DURATION,
ATTR_MEDIA_ENQUEUE,
ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION,
ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SEEK_POSITION,
ATTR_MEDIA_SHUFFLE, ATTR_MEDIA_SHUFFLE,
@ -49,6 +51,7 @@ from homeassistant.components.media_player import (
SERVICE_TURN_ON, SERVICE_TURN_ON,
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET, SERVICE_VOLUME_SET,
MediaPlayerEnqueue,
MediaType, MediaType,
) )
from homeassistant.config_entries import SOURCE_USER from homeassistant.config_entries import SOURCE_USER
@ -112,6 +115,28 @@ SAMPLE_PLAYER_STOPPED = {
"item_progress_ms": 5, "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 = { SAMPLE_QUEUE_TTS = {
"version": 833, "version": 833,
"count": 1, "count": 1,
@ -291,7 +316,7 @@ async def get_request_return_values_fixture():
"config": SAMPLE_CONFIG, "config": SAMPLE_CONFIG,
"outputs": SAMPLE_OUTPUTS_ON, "outputs": SAMPLE_OUTPUTS_ON,
"player": SAMPLE_PLAYER_PAUSED, "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() await hass.async_block_till_done()
async def add_to_queue_side_effect( 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"]) await updater_update(["queue", "player"])
mock_api.return_value.add_to_queue.side_effect = ( # for play_media testing
add_to_queue_side_effect # for play_media testing mock_api.return_value.add_to_queue.side_effect = add_to_queue_side_effect
)
async def pause_side_effect(): async def pause_side_effect():
await updater_update(["player"]) 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_DURATION] == 0.05
assert state.attributes[ATTR_MEDIA_POSITION] == 0.005 assert state.attributes[ATTR_MEDIA_POSITION] == 0.005
assert state.attributes[ATTR_MEDIA_TITLE] == "No album" # reversed for url assert state.attributes[ATTR_MEDIA_TITLE] == "No album" # reversed for url
assert state.attributes[ATTR_MEDIA_ARTIST] == "Google" assert state.attributes[ATTR_MEDIA_ARTIST] == "Some artist"
assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "Short TTS file" # reversed assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "Some song" # reversed
assert state.attributes[ATTR_MEDIA_ALBUM_ARTIST] == "The xx" assert state.attributes[ATTR_MEDIA_ALBUM_ARTIST] == "The xx"
assert state.attributes[ATTR_MEDIA_TRACK] == 1 assert state.attributes[ATTR_MEDIA_TRACK] == 1
assert not state.attributes[ATTR_MEDIA_SHUFFLE] 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 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 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] updater_update = mock_api_object.start_websocket_handler.call_args[0][2]
get_request_return_values["player"] = SAMPLE_PLAYER_STOPPED get_request_return_values["player"] = SAMPLE_PLAYER_STOPPED
await updater_update(["player"]) await updater_update(["player"])
await hass.async_block_till_done() await hass.async_block_till_done()
initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME)
get_request_return_values["queue"] = SAMPLE_QUEUE_TTS
await _service_call( await _service_call(
hass, hass,
TEST_MASTER_ENTITY_NAME, 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_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3",
ATTR_MEDIA_ANNOUNCE: True,
}, },
) )
state = hass.states.get(TEST_MASTER_ENTITY_NAME) 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 assert state.last_updated == initial_state.last_updated
async def test_async_play_media_tts_timeout(hass, mock_api_object): async def test_async_play_media_announcement_tts_timeout(hass, mock_api_object):
"""Test async play media with TTS timeout.""" """Test async play media announcement with TTS timeout."""
mock_api_object.add_to_queue.side_effect = None mock_api_object.add_to_queue.side_effect = None
with patch("homeassistant.components.forked_daapd.media_player.TTS_TIMEOUT", 0): with patch("homeassistant.components.forked_daapd.media_player.TTS_TIMEOUT", 0):
initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) 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_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3",
ATTR_MEDIA_ANNOUNCE: True,
}, },
) )
state = hass.states.get(TEST_MASTER_ENTITY_NAME) 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" assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "some album"
async def test_librespot_java_play_media(hass, pipe_control_api_object): async def test_librespot_java_play_announcement(hass, pipe_control_api_object):
"""Test play media with librespot-java pipe.""" """Test play announcement with librespot-java pipe."""
initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME)
await _service_call( await _service_call(
hass, 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_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3",
ATTR_MEDIA_ANNOUNCE: True,
}, },
) )
state = hass.states.get(TEST_MASTER_ENTITY_NAME) 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() await hass.async_block_till_done()
assert hass.states.get(TEST_MASTER_ENTITY_NAME).state == STATE_UNAVAILABLE assert hass.states.get(TEST_MASTER_ENTITY_NAME).state == STATE_UNAVAILABLE
assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]).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
)