mirror of
https://github.com/home-assistant/core.git
synced 2026-05-19 23:10:15 +01:00
Add search to Sonos (#170891)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -17,8 +17,11 @@ from homeassistant.components.media_player import (
|
||||
BrowseMedia,
|
||||
MediaClass,
|
||||
MediaType,
|
||||
SearchMedia,
|
||||
SearchMediaQuery,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.network import is_internal_request
|
||||
|
||||
from .const import (
|
||||
@@ -209,6 +212,49 @@ async def async_browse_media(
|
||||
return response
|
||||
|
||||
|
||||
async def async_search_media(
|
||||
hass: HomeAssistant,
|
||||
media: SonosMedia,
|
||||
get_browse_image_url: GetBrowseImageUrlType,
|
||||
query: SearchMediaQuery,
|
||||
) -> SearchMedia:
|
||||
"""Search media."""
|
||||
media_content_type = query.media_content_type or MediaType.TRACK
|
||||
search_type = MEDIA_TYPES_TO_SONOS.get(media_content_type)
|
||||
if search_type is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_media_content_type",
|
||||
translation_placeholders={
|
||||
"media_content_type": media_content_type,
|
||||
},
|
||||
)
|
||||
items = await hass.async_add_executor_job(
|
||||
partial(
|
||||
media.library.get_music_library_information,
|
||||
search_type,
|
||||
search_term=query.search_query,
|
||||
full_album_art_uri=True,
|
||||
complete_result=True,
|
||||
)
|
||||
)
|
||||
result = []
|
||||
for item in items:
|
||||
with suppress(UnknownMediaType):
|
||||
result.append(
|
||||
item_payload(
|
||||
item,
|
||||
get_thumbnail_url=partial(
|
||||
get_thumbnail_url_full,
|
||||
media,
|
||||
is_internal_request(hass),
|
||||
get_browse_image_url,
|
||||
),
|
||||
)
|
||||
)
|
||||
return SearchMedia(result=result)
|
||||
|
||||
|
||||
def build_item_response(
|
||||
media_library: MusicLibrary, payload: dict[str, str], get_thumbnail_url=None
|
||||
) -> BrowseMedia | None:
|
||||
|
||||
@@ -35,6 +35,8 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerState,
|
||||
MediaType,
|
||||
RepeatMode,
|
||||
SearchMedia,
|
||||
SearchMediaQuery,
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.components.plex import PLEX_URI_SCHEME
|
||||
@@ -124,6 +126,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||
| MediaPlayerEntityFeature.REPEAT_SET
|
||||
| MediaPlayerEntityFeature.SEARCH_MEDIA
|
||||
| MediaPlayerEntityFeature.SEEK
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.SHUFFLE_SET
|
||||
@@ -806,6 +809,18 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
media_content_type,
|
||||
)
|
||||
|
||||
async def async_search_media(
|
||||
self,
|
||||
query: SearchMediaQuery,
|
||||
) -> SearchMedia:
|
||||
"""Search the music library for media matching the query."""
|
||||
return await media_browser.async_search_media(
|
||||
self.hass,
|
||||
self.media,
|
||||
self.get_browse_image_url,
|
||||
query,
|
||||
)
|
||||
|
||||
async def async_join_players(self, group_members: list[str]) -> None:
|
||||
"""Join `group_members` as a player group with the current player."""
|
||||
speakers = []
|
||||
|
||||
@@ -124,6 +124,9 @@
|
||||
"invalid_media": {
|
||||
"message": "Could not find media in library: {media_id}"
|
||||
},
|
||||
"invalid_media_content_type": {
|
||||
"message": "Media content type {media_content_type} is not supported"
|
||||
},
|
||||
"invalid_sonos_playlist": {
|
||||
"message": "Could not find Sonos playlist: {name}"
|
||||
},
|
||||
|
||||
@@ -499,6 +499,10 @@ class MockMusicServiceItem:
|
||||
self.parent_id = parent_id
|
||||
self.album_art_uri: None | str = album_art_uri
|
||||
|
||||
def get_uri(self) -> str:
|
||||
"""Return URI."""
|
||||
return self.item_id.replace("S://", "x-file-cifs://")
|
||||
|
||||
|
||||
def list_from_json_fixture(file_name: str) -> list[MockMusicServiceItem]:
|
||||
"""Create a list of music service items from a json fixture file."""
|
||||
@@ -636,7 +640,10 @@ def mock_browse_by_idstring(
|
||||
|
||||
|
||||
def mock_get_music_library_information(
|
||||
search_type: str, search_term: str | None = None, full_album_art_uri: bool = True
|
||||
search_type: str,
|
||||
search_term: str | None = None,
|
||||
full_album_art_uri: bool = True,
|
||||
complete_result: bool = False,
|
||||
) -> list[MockMusicServiceItem]:
|
||||
"""Mock the call to get music library information."""
|
||||
if search_type == "albums" and search_term == "Abbey Road":
|
||||
@@ -670,7 +677,9 @@ def music_library_fixture(
|
||||
music_library = MagicMock()
|
||||
music_library.get_sonos_favorites.return_value = sonos_favorites
|
||||
music_library.browse_by_idstring = Mock(side_effect=mock_browse_by_idstring)
|
||||
music_library.get_music_library_information = mock_get_music_library_information
|
||||
music_library.get_music_library_information = Mock(
|
||||
side_effect=mock_get_music_library_information
|
||||
)
|
||||
music_library.browse = Mock(return_value=music_library_browse_categories)
|
||||
music_library.build_album_art_full_uri = Mock(
|
||||
return_value="build_album_art_full_uri.jpg"
|
||||
|
||||
@@ -382,3 +382,37 @@
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_search_media
|
||||
dict({
|
||||
'result': list([
|
||||
dict({
|
||||
'can_expand': False,
|
||||
'can_play': True,
|
||||
'can_search': False,
|
||||
'children': list([
|
||||
]),
|
||||
'children_media_class': None,
|
||||
'media_class': 'track',
|
||||
'media_content_id': 'x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3',
|
||||
'media_content_type': 'track',
|
||||
'not_shown': 0,
|
||||
'thumbnail': 'http://example.com/abbey_road.jpg',
|
||||
'title': 'Come Together',
|
||||
}),
|
||||
dict({
|
||||
'can_expand': False,
|
||||
'can_play': True,
|
||||
'can_search': False,
|
||||
'children': list([
|
||||
]),
|
||||
'children_media_class': None,
|
||||
'media_class': 'track',
|
||||
'media_content_id': 'x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3',
|
||||
'media_content_type': 'track',
|
||||
'not_shown': 0,
|
||||
'thumbnail': 'http://example.com/abbey_road.jpg',
|
||||
'title': 'Something',
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
'platform': 'sonos',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <MediaPlayerEntityFeature: 4127295>,
|
||||
'supported_features': <MediaPlayerEntityFeature: 8321599>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'RINCON_test',
|
||||
'unit_of_measurement': None,
|
||||
@@ -49,7 +49,7 @@
|
||||
'media_content_type': <MediaType.MUSIC: 'music'>,
|
||||
'repeat': <RepeatMode.OFF: 'off'>,
|
||||
'shuffle': False,
|
||||
'supported_features': <MediaPlayerEntityFeature: 4127295>,
|
||||
'supported_features': <MediaPlayerEntityFeature: 8321599>,
|
||||
'volume_level': 0.19,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for the Sonos Media Browser."""
|
||||
|
||||
from functools import partial
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
@@ -13,7 +13,7 @@ from homeassistant.components.media_player import (
|
||||
MediaClass,
|
||||
MediaType,
|
||||
)
|
||||
from homeassistant.components.sonos.const import MEDIA_TYPE_DIRECTORY
|
||||
from homeassistant.components.sonos.const import MEDIA_TYPE_DIRECTORY, SONOS_TRACKS
|
||||
from homeassistant.components.sonos.media_browser import (
|
||||
build_item_response,
|
||||
get_media,
|
||||
@@ -22,32 +22,11 @@ from homeassistant.components.sonos.media_browser import (
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import SoCoMockFactory
|
||||
from .conftest import MockMusicServiceItem, SoCoMockFactory
|
||||
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
class MockMusicServiceItem:
|
||||
"""Mocks a Soco MusicServiceItem."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
item_id: str,
|
||||
parent_id: str,
|
||||
item_class: str,
|
||||
) -> None:
|
||||
"""Initialize the mock item."""
|
||||
self.title = title
|
||||
self.item_id = item_id
|
||||
self.item_class = item_class
|
||||
self.parent_id = parent_id
|
||||
|
||||
def get_uri(self) -> str:
|
||||
"""Return URI."""
|
||||
return self.item_id.replace("S://", "x-file-cifs://")
|
||||
|
||||
|
||||
def mock_browse_by_idstring(
|
||||
search_type: str, idstring: str, start=0, max_items=100, full_album_art_uri=False
|
||||
) -> list[MockMusicServiceItem]:
|
||||
@@ -340,3 +319,84 @@ async def test_browse_media_library_folders(
|
||||
assert response["success"]
|
||||
assert response["result"] == snapshot
|
||||
assert soco_mock.music_library.browse_by_idstring.call_count == 1
|
||||
|
||||
|
||||
async def test_search_media(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
async_autosetup_sonos,
|
||||
soco,
|
||||
discover,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the async_search_media method returns tracks matching the query."""
|
||||
soco_mock = soco_factory.mock_list.get("192.168.42.2")
|
||||
mock_items = [
|
||||
MockMusicServiceItem(
|
||||
"Come Together",
|
||||
"S://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3",
|
||||
"A:ALBUM/Abbey%20Road",
|
||||
"object.item.audioItem.musicTrack",
|
||||
album_art_uri="http://example.com/abbey_road.jpg",
|
||||
),
|
||||
MockMusicServiceItem(
|
||||
"Something",
|
||||
"S://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3",
|
||||
"A:ALBUM/Abbey%20Road",
|
||||
"object.item.audioItem.musicTrack",
|
||||
album_art_uri="http://example.com/abbey_road.jpg",
|
||||
),
|
||||
]
|
||||
soco_mock.music_library.get_music_library_information = Mock(
|
||||
return_value=mock_items
|
||||
)
|
||||
|
||||
client = await hass_ws_client()
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "media_player/search_media",
|
||||
"entity_id": "media_player.zone_a",
|
||||
"search_query": "Come Together",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
|
||||
assert response["result"] == snapshot
|
||||
|
||||
assert soco_mock.music_library.get_music_library_information.call_args.args == (
|
||||
SONOS_TRACKS,
|
||||
)
|
||||
assert soco_mock.music_library.get_music_library_information.call_args.kwargs == {
|
||||
"search_term": "Come Together",
|
||||
"full_album_art_uri": True,
|
||||
"complete_result": True,
|
||||
}
|
||||
|
||||
|
||||
async def test_search_media_invalid_media_content_type(
|
||||
hass: HomeAssistant,
|
||||
async_autosetup_sonos,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test that async_search_media raises on an unsupported media_content_type."""
|
||||
client = await hass_ws_client()
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "media_player/search_media",
|
||||
"entity_id": "media_player.zone_a",
|
||||
"media_content_type": "movie",
|
||||
"media_content_id": "some_id",
|
||||
"search_query": "test",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert not response["success"]
|
||||
assert response["error"]["code"] == "home_assistant_error"
|
||||
assert response["error"]["translation_key"] == "invalid_media_content_type"
|
||||
assert response["error"]["translation_placeholders"] == {
|
||||
"media_content_type": "movie"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user