diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index cb33c78fc52..389942a9f59 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -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.""" diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 8035ec99777..307eb8deea6 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -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 + )