From 6bf6a0f7bc292d175807d31b6d80e9b55bb1a76a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 May 2022 13:57:00 -0700 Subject: [PATCH] Convert media player enqueue to an enum (#72406) --- .../components/bluesound/media_player.py | 14 +------ homeassistant/components/heos/media_player.py | 16 +++++--- .../components/media_player/__init__.py | 37 ++++++++++++++++- .../media_player/reproduce_state.py | 3 +- .../components/media_player/services.yaml | 16 ++++++++ .../components/sonos/media_player.py | 9 ++--- .../components/squeezebox/media_player.py | 16 ++++---- tests/components/media_player/test_init.py | 40 +++++++++++++++++++ .../media_player/test_reproduce_state.py | 4 -- 9 files changed, 119 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index e22606b795f..7f1c6b6553f 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -24,10 +24,7 @@ from homeassistant.components.media_player import ( from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_ENQUEUE, - MEDIA_TYPE_MUSIC, -) +from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -1023,11 +1020,7 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(f"Play?seek={float(position)}") async def async_play_media(self, media_type, media_id, **kwargs): - """ - Send the play_media command to the media player. - - If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. - """ + """Send the play_media command to the media player.""" if self.is_grouped and not self.is_master: return @@ -1041,9 +1034,6 @@ class BluesoundPlayer(MediaPlayerEntity): url = f"Play?url={media_id}" - if kwargs.get(ATTR_MEDIA_ENQUEUE): - return await self.send_bluesound_command(url) - return await self.send_bluesound_command(url) async def async_volume_up(self): diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 4cfbe5fe408..ad9225d9b21 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -12,6 +12,7 @@ from typing_extensions import ParamSpec from homeassistant.components import media_source from homeassistant.components.media_player import ( + MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, ) @@ -73,6 +74,14 @@ CONTROL_TO_SUPPORT = { heos_const.CONTROL_PLAY_NEXT: MediaPlayerEntityFeature.NEXT_TRACK, } +HA_HEOS_ENQUEUE_MAP = { + None: heos_const.ADD_QUEUE_REPLACE_AND_PLAY, + MediaPlayerEnqueue.ADD: heos_const.ADD_QUEUE_ADD_TO_END, + MediaPlayerEnqueue.REPLACE: heos_const.ADD_QUEUE_REPLACE_AND_PLAY, + MediaPlayerEnqueue.NEXT: heos_const.ADD_QUEUE_PLAY_NEXT, + MediaPlayerEnqueue.PLAY: heos_const.ADD_QUEUE_PLAY_NOW, +} + _LOGGER = logging.getLogger(__name__) @@ -224,11 +233,8 @@ class HeosMediaPlayer(MediaPlayerEntity): playlist = next((p for p in playlists if p.name == media_id), None) if not playlist: raise ValueError(f"Invalid playlist '{media_id}'") - add_queue_option = ( - heos_const.ADD_QUEUE_ADD_TO_END - if kwargs.get(ATTR_MEDIA_ENQUEUE) - else heos_const.ADD_QUEUE_REPLACE_AND_PLAY - ) + add_queue_option = HA_HEOS_ENQUEUE_MAP.get(kwargs.get(ATTR_MEDIA_ENQUEUE)) + await self._player.add_to_queue(playlist, add_queue_option) return diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index bf006e2bd4e..f71f3fc2a1f 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -147,6 +147,19 @@ ENTITY_IMAGE_CACHE = {CACHE_IMAGES: collections.OrderedDict(), CACHE_MAXSIZE: 16 SCAN_INTERVAL = dt.timedelta(seconds=10) +class MediaPlayerEnqueue(StrEnum): + """Enqueue types for playing media.""" + + # add given media item to end of the queue + ADD = "add" + # play the given media item next, keep queue + NEXT = "next" + # play the given media item now, keep queue + PLAY = "play" + # play the given media item now, clear queue + REPLACE = "replace" + + class MediaPlayerDeviceClass(StrEnum): """Device class for media players.""" @@ -169,7 +182,9 @@ DEVICE_CLASS_RECEIVER = MediaPlayerDeviceClass.RECEIVER.value MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = { vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, - vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean, + vol.Optional(ATTR_MEDIA_ENQUEUE): vol.Any( + cv.boolean, vol.Coerce(MediaPlayerEnqueue) + ), vol.Optional(ATTR_MEDIA_EXTRA, default={}): dict, } @@ -350,10 +365,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "async_select_sound_mode", [MediaPlayerEntityFeature.SELECT_SOUND_MODE], ) + + # Remove in Home Assistant 2022.9 + def _rewrite_enqueue(value): + """Rewrite the enqueue value.""" + if ATTR_MEDIA_ENQUEUE not in value: + pass + elif value[ATTR_MEDIA_ENQUEUE] is True: + value[ATTR_MEDIA_ENQUEUE] = MediaPlayerEnqueue.ADD + _LOGGER.warning( + "Playing media with enqueue set to True is deprecated. Use 'add' instead" + ) + elif value[ATTR_MEDIA_ENQUEUE] is False: + value[ATTR_MEDIA_ENQUEUE] = MediaPlayerEnqueue.PLAY + _LOGGER.warning( + "Playing media with enqueue set to False is deprecated. Use 'play' instead" + ) + + return value + component.async_register_entity_service( SERVICE_PLAY_MEDIA, vol.All( cv.make_entity_service_schema(MEDIA_PLAYER_PLAY_MEDIA_SCHEMA), + _rewrite_enqueue, _rename_keys( media_type=ATTR_MEDIA_CONTENT_TYPE, media_id=ATTR_MEDIA_CONTENT_ID, diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 586ac61b4e1..bdfc0bf3acb 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -27,7 +27,6 @@ from .const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, @@ -118,7 +117,7 @@ async def _async_reproduce_states( if features & MediaPlayerEntityFeature.PLAY_MEDIA: await call_service( SERVICE_PLAY_MEDIA, - [ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE], + [ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID], ) already_playing = True diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 2e8585d0127..b2a8ac40262 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -151,6 +151,22 @@ play_media: selector: text: + enqueue: + name: Enqueue + description: If the content should be played now or be added to the queue. + required: false + selector: + select: + options: + - label: "Play now" + value: "play" + - label: "Play next" + value: "next" + - label: "Add to queue" + value: "add" + - label: "Play now and clear queue" + value: "replace" + select_source: name: Select source description: Send the media player the command to change input source. diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 45e11a810ae..fd37e546105 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -18,6 +18,7 @@ import voluptuous as vol from homeassistant.components import media_source, spotify from homeassistant.components.media_player import ( + MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, async_process_play_media_url, @@ -537,8 +538,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): If media_type is "playlist", media_id should be a Sonos Playlist name. Otherwise, media_id should be a URI. - - If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ if spotify.is_spotify_media_type(media_type): media_type = spotify.resolve_spotify_media_type(media_type) @@ -575,7 +574,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ) if result.shuffle: self.set_shuffle(True) - if kwargs.get(ATTR_MEDIA_ENQUEUE): + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: plex_plugin.add_to_queue(result.media) else: soco.clear_queue() @@ -585,7 +584,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): share_link = self.coordinator.share_link if share_link.is_share_link(media_id): - if kwargs.get(ATTR_MEDIA_ENQUEUE): + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: share_link.add_share_link_to_queue(media_id) else: soco.clear_queue() @@ -595,7 +594,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) - if kwargs.get(ATTR_MEDIA_ENQUEUE): + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: soco.add_uri_to_queue(media_id) else: soco.play_uri(media_id, force_radio=is_radio) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index d0d1cf89739..cd628a639c5 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( + MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, ) @@ -469,16 +470,17 @@ class SqueezeBoxEntity(MediaPlayerEntity): await self._player.async_set_power(True) async def async_play_media(self, media_type, media_id, **kwargs): - """ - Send the play_media command to the media player. - - If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the current playlist. - """ - cmd = "play" + """Send the play_media command to the media player.""" index = None - if kwargs.get(ATTR_MEDIA_ENQUEUE): + enqueue: MediaPlayerEnqueue | None = kwargs.get(ATTR_MEDIA_ENQUEUE) + + if enqueue == MediaPlayerEnqueue.ADD: cmd = "add" + elif enqueue == MediaPlayerEnqueue.NEXT: + cmd = "insert" + else: + cmd = "play" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index aa5e1b164f4..cb095cbcfe0 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -4,6 +4,8 @@ import base64 from http import HTTPStatus from unittest.mock import patch +import pytest + from homeassistant.components import media_player from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.components.websocket_api.const import TYPE_RESULT @@ -251,3 +253,41 @@ async def test_group_members_available_when_off(hass): state = hass.states.get("media_player.bedroom") assert state.state == STATE_OFF assert "group_members" in state.attributes + + +@pytest.mark.parametrize( + "input,expected", + ( + (True, media_player.MediaPlayerEnqueue.ADD), + (False, media_player.MediaPlayerEnqueue.PLAY), + ("play", media_player.MediaPlayerEnqueue.PLAY), + ("next", media_player.MediaPlayerEnqueue.NEXT), + ("add", media_player.MediaPlayerEnqueue.ADD), + ("replace", media_player.MediaPlayerEnqueue.REPLACE), + ), +) +async def test_enqueue_rewrite(hass, input, expected): + """Test that group_members are still available when media_player is off.""" + await async_setup_component( + hass, "media_player", {"media_player": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + # Fake group support for DemoYoutubePlayer + with patch( + "homeassistant.components.demo.media_player.DemoYoutubePlayer.play_media", + ) as mock_play_media: + await hass.services.async_call( + "media_player", + "play_media", + { + "entity_id": "media_player.bedroom", + "media_content_type": "music", + "media_content_id": "1234", + "enqueue": input, + }, + blocking=True, + ) + + assert len(mock_play_media.mock_calls) == 1 + assert mock_play_media.mock_calls[0][2]["enqueue"] == expected diff --git a/tests/components/media_player/test_reproduce_state.py b/tests/components/media_player/test_reproduce_state.py index f1a243337e1..f880130d4bd 100644 --- a/tests/components/media_player/test_reproduce_state.py +++ b/tests/components/media_player/test_reproduce_state.py @@ -6,7 +6,6 @@ from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, @@ -253,7 +252,6 @@ async def test_play_media(hass): value_1 = "dummy_1" value_2 = "dummy_2" - value_3 = "dummy_3" await async_reproduce_states( hass, @@ -275,7 +273,6 @@ async def test_play_media(hass): { ATTR_MEDIA_CONTENT_TYPE: value_1, ATTR_MEDIA_CONTENT_ID: value_2, - ATTR_MEDIA_ENQUEUE: value_3, }, ) ], @@ -294,5 +291,4 @@ async def test_play_media(hass): "entity_id": ENTITY_1, ATTR_MEDIA_CONTENT_TYPE: value_1, ATTR_MEDIA_CONTENT_ID: value_2, - ATTR_MEDIA_ENQUEUE: value_3, }