1
mirror of https://github.com/home-assistant/core synced 2024-08-15 18:25:44 +02:00

Add spotify support to forked-daapd (#79136)

This commit is contained in:
uvjustin 2022-09-27 11:32:05 -07:00 committed by GitHub
parent b043a6ba88
commit a561b608bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 141 additions and 6 deletions

View File

@ -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"

View File

@ -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",

View File

@ -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(

View File

@ -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."""

View File

@ -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,
)