diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index bd0ab02e6c27..60e31bc707d8 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -17,6 +17,8 @@ CAN_PLAY_TYPE = { MediaType.ALBUM, MediaType.GENRE, MediaType.MUSIC, + MediaType.EPISODE, + "show", # this is a spotify constant } CONF_LIBRESPOT_JAVA_PORT = "librespot_java_port" CONF_MAX_PLAYLISTS = "max_playlists" diff --git a/homeassistant/components/forked_daapd/manifest.json b/homeassistant/components/forked_daapd/manifest.json index 9a0372a193e6..14d2132a165c 100644 --- a/homeassistant/components/forked_daapd/manifest.json +++ b/homeassistant/components/forked_daapd/manifest.json @@ -4,6 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/forked_daapd", "codeowners": ["@uvjustin"], "requirements": ["pyforked-daapd==0.1.11", "pylibrespot-java==0.1.0"], + "after_dependencies": ["spotify"], "config_flow": true, "zeroconf": ["_daap._tcp.local."], "iot_class": "local_push", diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 05d417ac9d9b..9da1c1a1168a 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -21,6 +21,12 @@ from homeassistant.components.media_player import ( MediaType, async_process_play_media_url, ) +from homeassistant.components.spotify import ( + async_browse_media as spotify_async_browse_media, + is_spotify_media_type, + resolve_spotify_media_type, + spotify_uri_from_media_browser_url, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant, callback @@ -677,6 +683,9 @@ class ForkedDaapdMaster(MediaPlayerEntity): media_id = play_item.url elif is_owntone_media_content_id(media_id): media_id = convert_to_owntone_uri(media_id) + elif is_spotify_media_type(media_type): + media_type = resolve_spotify_media_type(media_type) + media_id = spotify_uri_from_media_browser_url(media_id) if media_type not in CAN_PLAY_TYPE: _LOGGER.warning("Media type '%s' not supported", media_type) @@ -836,10 +845,27 @@ class ForkedDaapdMaster(MediaPlayerEntity): media_content_id, content_filter=lambda bm: bm.media_content_type in CAN_PLAY_TYPE, ) - if media_content_type is None: - # This is the base level, so we combine our library with the media source - return library(ms_result.children) - return ms_result + if media_content_type is not None: + return ms_result + other_sources: list[BrowseMedia] = ( + list(ms_result.children) if ms_result.children else [] + ) + if "spotify" in self.hass.config.components and ( + media_content_type is None or is_spotify_media_type(media_content_type) + ): + spotify_result = await spotify_async_browse_media( + self.hass, media_content_type, media_content_id + ) + if media_content_type is not None: + return spotify_result + if spotify_result.children: + other_sources += spotify_result.children + + if media_content_id is None or media_content_type is None: + # This is the base level, so we combine our library with the other sources + return library(other_sources) + + # media_content_type should only be None if media_content_id is None return await get_owntone_content(self, media_content_id) async def async_get_browse_image( diff --git a/tests/components/forked_daapd/test_browse_media.py b/tests/components/forked_daapd/test_browse_media.py index e90cbbff2aa9..ff26b6f93157 100644 --- a/tests/components/forked_daapd/test_browse_media.py +++ b/tests/components/forked_daapd/test_browse_media.py @@ -3,9 +3,12 @@ from http import HTTPStatus from unittest.mock import patch -from homeassistant.components import media_source +from homeassistant.components import media_source, spotify from homeassistant.components.forked_daapd.browse_media import create_media_content_id -from homeassistant.components.media_player import MediaType +from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType +from homeassistant.components.spotify.const import ( + MEDIA_PLAYER_PREFIX as SPOTIFY_MEDIA_PLAYER_PREFIX, +) from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.setup import async_setup_component @@ -220,6 +223,56 @@ async def test_async_browse_media_not_found(hass, hass_ws_client, config_entry): msg_id += 1 +async def test_async_browse_spotify(hass, hass_ws_client, config_entry): + """Test browsing spotify.""" + + assert await async_setup_component(hass, spotify.DOMAIN, {}) + await hass.async_block_till_done() + config_entry.add_to_hass(hass) + await config_entry.async_setup(hass) + await hass.async_block_till_done() + with patch( + "homeassistant.components.forked_daapd.media_player.spotify_async_browse_media" + ) as mock_spotify_browse: + children = [ + BrowseMedia( + title="Spotify", + media_class=MediaClass.APP, + media_content_id=f"{SPOTIFY_MEDIA_PLAYER_PREFIX}some_id", + media_content_type=f"{SPOTIFY_MEDIA_PLAYER_PREFIX}track", + thumbnail="https://brands.home-assistant.io/_/spotify/logo.png", + can_play=False, + can_expand=True, + ) + ] + mock_spotify_browse.return_value = BrowseMedia( + title="Spotify", + media_class=MediaClass.APP, + media_content_id=SPOTIFY_MEDIA_PLAYER_PREFIX, + media_content_type=f"{SPOTIFY_MEDIA_PLAYER_PREFIX}library", + thumbnail="https://brands.home-assistant.io/_/spotify/logo.png", + can_play=False, + can_expand=True, + children=children, + ) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": TEST_MASTER_ENTITY_NAME, + "media_content_type": f"{SPOTIFY_MEDIA_PLAYER_PREFIX}library", + "media_content_id": SPOTIFY_MEDIA_PLAYER_PREFIX, + } + ) + msg = await client.receive_json() + # Assert WebSocket response + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + async def test_async_browse_image(hass, hass_client, config_entry): """Test browse media images.""" diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 05a51e4defa9..589f176db143 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -54,6 +54,7 @@ from homeassistant.components.media_player import ( MediaPlayerEnqueue, MediaType, ) +from homeassistant.components.media_source import PlayMedia from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, @@ -891,3 +892,55 @@ async def test_play_owntone_media(hass, mock_api_object): position=0, playback_from_position=0, ) + + +async def test_play_spotify_media(hass, mock_api_object): + """Test async play media with a spotify source.""" + initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: "spotify://track", + ATTR_MEDIA_CONTENT_ID: "spotify://open.spotify.com/spotify:track:abcdefghi", + 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="spotify:track:abcdefghi", + playback="start", + position=0, + playback_from_position=0, + ) + + +async def test_play_media_source(hass, mock_api_object): + """Test async play media with a spotify source.""" + initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) + with patch( + "homeassistant.components.media_source.async_resolve_media", + return_value=PlayMedia("http://my_hass/song.m4a", "audio/aac"), + ): + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: "audio/aac", + ATTR_MEDIA_CONTENT_ID: "media-source://media_source/test_dir/song.m4a", + 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://my_hass/song.m4a", + playback="start", + position=0, + playback_from_position=0, + )