1
0
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:
Pete Sage
2026-05-18 08:39:12 -04:00
committed by GitHub
parent 27b161bf7c
commit dfd241dd1a
7 changed files with 195 additions and 28 deletions
@@ -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}"
},
+11 -2
View File
@@ -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>,
+84 -24
View File
@@ -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"
}