diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 07d7d11ea461..83828ba3cea2 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -213,3 +213,6 @@ SCAN_INTERVAL = datetime.timedelta(seconds=10) DISCOVERY_INTERVAL = datetime.timedelta(seconds=60) SUBSCRIPTION_TIMEOUT = 1200 LONG_SERVICE_TIMEOUT = 30.0 + +# Same as the Plex integration, which is only imported when used +PLEX_URI_SCHEME = "plex://" diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index a7d7404afaad..078ba041ebba 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -11,7 +11,7 @@ from soco.data_structures import DidlContainer, DidlObject from soco.ms_data_structures import MusicServiceItem from soco.music_library import MusicLibrary -from homeassistant.components import media_source, plex, spotify +from homeassistant.components import media_source from homeassistant.components.media_player import ( BrowseError, BrowseMedia, @@ -152,18 +152,25 @@ async def async_browse_media( hass, media_content_id, content_filter=media_source_filter ) - if plex.is_plex_media_id(media_content_id): - return await plex.async_browse_media( - hass, media_content_type, media_content_id, platform=DOMAIN - ) + # Plex and Spotify are only imported once set up, as they are heavy to load + if "plex" in hass.config.components: + from homeassistant.components import plex # noqa: PLC0415 - if media_content_type == "plex": - return await plex.async_browse_media(hass, None, None, platform=DOMAIN) + if plex.is_plex_media_id(media_content_id): + return await plex.async_browse_media( + hass, media_content_type, media_content_id, platform=DOMAIN + ) - if spotify.is_spotify_media_type(media_content_type): - return await spotify.async_browse_media( - hass, media_content_type, media_content_id, can_play_artist=False - ) + if media_content_type == "plex": + return await plex.async_browse_media(hass, None, None, platform=DOMAIN) + + if "spotify" in hass.config.components: + from homeassistant.components import spotify # noqa: PLC0415 + + if spotify.is_spotify_media_type(media_content_type): + return await spotify.async_browse_media( + hass, media_content_type, media_content_id, can_play_artist=False + ) if media_content_type == "library": return await hass.async_add_executor_job( @@ -420,6 +427,8 @@ async def root_payload( ) if "spotify" in hass.config.components: + from homeassistant.components import spotify # noqa: PLC0415 + result = await spotify.async_browse_media(hass, None, None) if result.children: children.extend(result.children) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 11ad112bce8d..f41990318dd3 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -20,7 +20,7 @@ from soco.ms_data_structures import MusicServiceItem from sonos_websocket import CLIP_ID_KEY from sonos_websocket.exception import SonosWebsocketError -from homeassistant.components import media_source, spotify +from homeassistant.components import media_source from homeassistant.components.media_player import ( ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ANNOUNCE, @@ -41,10 +41,6 @@ from homeassistant.components.media_player import ( SearchMediaQuery, async_process_play_media_url, ) -from homeassistant.components.plex import PLEX_URI_SCHEME -from homeassistant.components.plex.services import ( # pylint: disable=home-assistant-component-root-import - process_plex_payload, -) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er @@ -63,6 +59,7 @@ from .const import ( MODELS_LINEIN_ONLY, MODELS_TV_ONLY, PLAYABLE_MEDIA_TYPES, + PLEX_URI_SCHEME, SONOS_CREATE_MEDIA_PLAYER, SONOS_FAVORITES_UPDATED, SONOS_MEDIA_UPDATED, @@ -561,9 +558,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): }, ) - if spotify.is_spotify_media_type(media_type): - media_type = spotify.resolve_spotify_media_type(media_type) - media_id = spotify.spotify_uri_from_media_browser_url(media_id) + # Spotify is only imported once set up, as it is heavy to load + if "spotify" in self.hass.config.components: + from homeassistant.components import spotify # noqa: PLC0415 + + if spotify.is_spotify_media_type(media_type): + media_type = spotify.resolve_spotify_media_type(media_type) + media_id = spotify.spotify_uri_from_media_browser_url(media_id) await self.hass.async_add_executor_job( partial(self._play_media, media_type, media_id, is_radio, **kwargs) @@ -586,6 +587,11 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco = self.coordinator.soco if media_id and media_id.startswith(PLEX_URI_SCHEME): + # Runs in the executor, only load Plex when playing Plex media + from homeassistant.components.plex.services import ( # noqa: PLC0415 # pylint: disable=home-assistant-component-root-import + process_plex_payload, + ) + plex_plugin = self.speaker.plex_plugin result = process_plex_payload( self.hass, media_type, media_id, supports_playqueues=False diff --git a/tests/components/sonos/test_media_browser.py b/tests/components/sonos/test_media_browser.py index 27f420c818de..40f81529f6cf 100644 --- a/tests/components/sonos/test_media_browser.py +++ b/tests/components/sonos/test_media_browser.py @@ -1,7 +1,7 @@ """Tests for the Sonos Media Browser.""" from functools import partial -from unittest.mock import MagicMock, Mock +from unittest.mock import MagicMock, Mock, patch import pytest from syrupy.assertion import SnapshotAssertion @@ -269,6 +269,118 @@ async def test_browse_media_root( assert response["result"]["children"] == snapshot +@pytest.mark.parametrize( + ("media_content_type", "media_content_id", "target", "expected_args", "kwargs"), + [ + pytest.param( + "music", + "plex://1/2", + "homeassistant.components.plex.async_browse_media", + ("music", "plex://1/2"), + {"platform": "sonos"}, + id="plex_item", + ), + pytest.param( + "plex", + "", + "homeassistant.components.plex.async_browse_media", + (None, None), + {"platform": "sonos"}, + id="plex_root", + ), + pytest.param( + "spotify://library", + "spotify://entry_id", + "homeassistant.components.spotify.async_browse_media", + ("spotify://library", "spotify://entry_id"), + {"can_play_artist": False}, + id="spotify", + ), + ], +) +@pytest.mark.usefixtures("soco_factory", "async_autosetup_sonos", "soco") +async def test_browse_media_plex_spotify( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + media_content_type: str, + media_content_id: str, + target: str, + expected_args: tuple[str | None, str | None], + kwargs: dict[str, str | bool], +) -> None: + """Test browsing is passed to Plex and Spotify when they are set up.""" + hass.config.components.update({"plex", "spotify"}) + result = BrowseMedia( + title="Result", + media_class=MediaClass.DIRECTORY, + media_content_id="result", + media_content_type="result", + can_play=False, + can_expand=True, + ) + + client = await hass_ws_client() + with patch(target, return_value=result) as mock_browse: + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": "media_player.zone_a", + "media_content_id": media_content_id, + "media_content_type": media_content_type, + } + ) + response = await client.receive_json() + + assert response["success"] + assert response["result"]["title"] == "Result" + mock_browse.assert_called_once_with(hass, *expected_args, **kwargs) + + +@pytest.mark.usefixtures("soco_factory", "async_autosetup_sonos", "soco") +async def test_browse_media_root_spotify( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test the root includes Spotify when it is set up.""" + hass.config.components.add("spotify") + spotify_item = BrowseMedia( + title="Spotify", + media_class=MediaClass.APP, + media_content_id="spotify://entry_id", + media_content_type="spotify://library", + can_play=False, + can_expand=True, + ) + spotify_root = BrowseMedia( + title="Spotify", + media_class=MediaClass.APP, + media_content_id="spotify://", + media_content_type="spotify", + can_play=False, + can_expand=True, + children=[spotify_item], + ) + + client = await hass_ws_client() + with patch( + "homeassistant.components.spotify.async_browse_media", + return_value=spotify_root, + ): + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": "media_player.zone_a", + } + ) + response = await client.receive_json() + + assert response["success"] + assert "spotify://entry_id" in [ + child["media_content_id"] for child in response["result"]["children"] + ] + + async def test_browse_media_library( hass: HomeAssistant, soco_factory: SoCoMockFactory, diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 15c1299a6cd7..601dcc3e0a1b 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -537,6 +537,28 @@ async def test_play_media_share_link_add( ) +@pytest.mark.usefixtures("soco_factory", "async_autosetup_sonos") +async def test_play_media_spotify_media_browser_url( + hass: HomeAssistant, soco_sharelink: MagicMock +) -> None: + """Tests playing a Spotify media browser URL when Spotify is set up.""" + hass.config.components.add("spotify") + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "spotify://playlist", + ATTR_MEDIA_CONTENT_ID: f"spotify://01J5TX5A0FF6G5V0QJX6HBC94T/{_share_link}", + }, + blocking=True, + ) + assert soco_sharelink.add_share_link_to_queue.call_count == 1 + assert ( + soco_sharelink.add_share_link_to_queue.call_args_list[0].args[0] == _share_link + ) + + async def test_play_media_share_link_next( hass: HomeAssistant, soco_factory: SoCoMockFactory,