mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Support announce and enqueue in forked-daapd (#77744)
This commit is contained in:
parent
e079968ef4
commit
420285f7ef
@ -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."""
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user