mirror of
https://github.com/home-assistant/core
synced 2024-09-09 12:51:22 +02:00
Add spotify support to forked-daapd (#79136)
This commit is contained in:
parent
b043a6ba88
commit
a561b608bf
@ -17,6 +17,8 @@ CAN_PLAY_TYPE = {
|
|||||||
MediaType.ALBUM,
|
MediaType.ALBUM,
|
||||||
MediaType.GENRE,
|
MediaType.GENRE,
|
||||||
MediaType.MUSIC,
|
MediaType.MUSIC,
|
||||||
|
MediaType.EPISODE,
|
||||||
|
"show", # this is a spotify constant
|
||||||
}
|
}
|
||||||
CONF_LIBRESPOT_JAVA_PORT = "librespot_java_port"
|
CONF_LIBRESPOT_JAVA_PORT = "librespot_java_port"
|
||||||
CONF_MAX_PLAYLISTS = "max_playlists"
|
CONF_MAX_PLAYLISTS = "max_playlists"
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/forked_daapd",
|
"documentation": "https://www.home-assistant.io/integrations/forked_daapd",
|
||||||
"codeowners": ["@uvjustin"],
|
"codeowners": ["@uvjustin"],
|
||||||
"requirements": ["pyforked-daapd==0.1.11", "pylibrespot-java==0.1.0"],
|
"requirements": ["pyforked-daapd==0.1.11", "pylibrespot-java==0.1.0"],
|
||||||
|
"after_dependencies": ["spotify"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"zeroconf": ["_daap._tcp.local."],
|
"zeroconf": ["_daap._tcp.local."],
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
|
@ -21,6 +21,12 @@ from homeassistant.components.media_player import (
|
|||||||
MediaType,
|
MediaType,
|
||||||
async_process_play_media_url,
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@ -677,6 +683,9 @@ class ForkedDaapdMaster(MediaPlayerEntity):
|
|||||||
media_id = play_item.url
|
media_id = play_item.url
|
||||||
elif is_owntone_media_content_id(media_id):
|
elif is_owntone_media_content_id(media_id):
|
||||||
media_id = convert_to_owntone_uri(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:
|
if media_type not in CAN_PLAY_TYPE:
|
||||||
_LOGGER.warning("Media type '%s' not supported", media_type)
|
_LOGGER.warning("Media type '%s' not supported", media_type)
|
||||||
@ -836,10 +845,27 @@ class ForkedDaapdMaster(MediaPlayerEntity):
|
|||||||
media_content_id,
|
media_content_id,
|
||||||
content_filter=lambda bm: bm.media_content_type in CAN_PLAY_TYPE,
|
content_filter=lambda bm: bm.media_content_type in CAN_PLAY_TYPE,
|
||||||
)
|
)
|
||||||
if media_content_type is None:
|
if media_content_type is not None:
|
||||||
# This is the base level, so we combine our library with the media source
|
return ms_result
|
||||||
return library(ms_result.children)
|
other_sources: list[BrowseMedia] = (
|
||||||
return ms_result
|
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)
|
return await get_owntone_content(self, media_content_id)
|
||||||
|
|
||||||
async def async_get_browse_image(
|
async def async_get_browse_image(
|
||||||
|
@ -3,9 +3,12 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from unittest.mock import patch
|
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.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.components.websocket_api.const import TYPE_RESULT
|
||||||
from homeassistant.setup import async_setup_component
|
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
|
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):
|
async def test_async_browse_image(hass, hass_client, config_entry):
|
||||||
"""Test browse media images."""
|
"""Test browse media images."""
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@ from homeassistant.components.media_player import (
|
|||||||
MediaPlayerEnqueue,
|
MediaPlayerEnqueue,
|
||||||
MediaType,
|
MediaType,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.media_source import PlayMedia
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
ATTR_FRIENDLY_NAME,
|
ATTR_FRIENDLY_NAME,
|
||||||
@ -891,3 +892,55 @@ async def test_play_owntone_media(hass, mock_api_object):
|
|||||||
position=0,
|
position=0,
|
||||||
playback_from_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,
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user