mirror of
https://github.com/home-assistant/core.git
synced 2026-10-07 23:09:19 -04:00
Improve search inside Squeezebox browse results (#183394)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
d5e4532b30
commit
03e5fcd6ce
@@ -46,6 +46,7 @@ MEDIA_TYPE_TO_SQUEEZEBOX: dict[str | MediaType, str] = {
|
||||
"genres": "genres",
|
||||
"new music": "new music",
|
||||
"album artists": "album artists",
|
||||
"artist tracks": "titles",
|
||||
MediaType.ALBUM: "album",
|
||||
MediaType.ARTIST: "artist",
|
||||
MediaType.TRACK: "title",
|
||||
@@ -61,6 +62,7 @@ SQUEEZEBOX_ID_BY_TYPE: dict[str | MediaType, str] = {
|
||||
"albums": "album_id",
|
||||
MediaType.ARTIST: "artist_id",
|
||||
"artists": "artist_id",
|
||||
"artist tracks": "artist_id",
|
||||
MediaType.TRACK: "track_id",
|
||||
"tracks": "track_id",
|
||||
MediaType.PLAYLIST: "playlist_id",
|
||||
@@ -87,6 +89,7 @@ CONTENT_TYPE_MEDIA_CLASS: dict[str | MediaType, dict[str, MediaClass | str]] = {
|
||||
"genres": {"item": MediaClass.DIRECTORY, "children": MediaClass.GENRE},
|
||||
"new music": {"item": MediaClass.DIRECTORY, "children": MediaClass.ALBUM},
|
||||
"album artists": {"item": MediaClass.DIRECTORY, "children": MediaClass.ARTIST},
|
||||
"artist tracks": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK},
|
||||
MediaType.ALBUM: {"item": MediaClass.ALBUM, "children": MediaClass.TRACK},
|
||||
MediaType.ARTIST: {"item": MediaClass.ARTIST, "children": MediaClass.ALBUM},
|
||||
MediaType.TRACK: {"item": MediaClass.TRACK, "children": ""},
|
||||
@@ -101,7 +104,7 @@ CONTENT_TYPE_TO_CHILD_TYPE: dict[
|
||||
str | MediaType | None,
|
||||
] = {
|
||||
MediaType.ALBUM: MediaType.TRACK,
|
||||
MediaType.PLAYLIST: MediaType.PLAYLIST,
|
||||
MediaType.PLAYLIST: MediaType.TRACK,
|
||||
MediaType.ARTIST: MediaType.ALBUM,
|
||||
MediaType.GENRE: MediaType.ARTIST,
|
||||
"artists": MediaType.ARTIST,
|
||||
@@ -113,12 +116,20 @@ CONTENT_TYPE_TO_CHILD_TYPE: dict[
|
||||
"radios": MediaClass.APP,
|
||||
"new music": MediaType.ALBUM,
|
||||
"album artists": MediaType.ARTIST,
|
||||
"artist tracks": MediaType.TRACK,
|
||||
MediaType.APPS: MediaType.APP,
|
||||
MediaType.APP: MediaType.TRACK,
|
||||
"favorite": None,
|
||||
"track": MediaType.TRACK,
|
||||
}
|
||||
|
||||
# LMS ignores the search query when it lists the tracks of a playlist
|
||||
SEARCHABLE_TYPES: set[str | MediaType] = {
|
||||
MediaType.ALBUM,
|
||||
MediaType.ARTIST,
|
||||
MediaType.GENRE,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class BrowseData:
|
||||
@@ -358,6 +369,7 @@ async def build_item_response(
|
||||
media_class=CONTENT_TYPE_MEDIA_CLASS[item_type]["item"],
|
||||
can_expand=bool(CONTENT_TYPE_MEDIA_CLASS[item_type]["children"]),
|
||||
can_play=True,
|
||||
can_search=item_type in SEARCHABLE_TYPES,
|
||||
)
|
||||
|
||||
assert child_media.media_class is not None
|
||||
@@ -397,6 +409,7 @@ async def build_item_response(
|
||||
can_play=any(child.can_play for child in children),
|
||||
children=children,
|
||||
can_expand=True,
|
||||
can_search=search_type in SEARCHABLE_TYPES,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -665,7 +665,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
_valid_type_list = [
|
||||
key
|
||||
for key in self._browse_data.content_type_media_class
|
||||
if key not in ["apps", "app", "radios", "radio"]
|
||||
if key not in ["apps", "app", "radios", "radio", "artist tracks"]
|
||||
]
|
||||
|
||||
_media_content_type_list = (
|
||||
@@ -687,6 +687,10 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
},
|
||||
)
|
||||
|
||||
if query.media_content_id and MediaType.ARTIST in _media_content_type_list:
|
||||
# LMS matches the albums of an artist by album title only
|
||||
_media_content_type_list.append("artist tracks")
|
||||
|
||||
search_response_list: list[BrowseMedia] = []
|
||||
|
||||
for _content_type in _media_content_type_list:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Test the media browser interface."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, MagicMock, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -70,17 +71,17 @@ async def test_async_browse_media_root(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("category", "child_count"),
|
||||
("category", "child_count", "can_search"),
|
||||
[
|
||||
("favorites", 4),
|
||||
("artists", 4),
|
||||
("albums", 4),
|
||||
("playlists", 4),
|
||||
("genres", 4),
|
||||
("new music", 4),
|
||||
("album artists", 4),
|
||||
("apps", 3),
|
||||
("radios", 3),
|
||||
("favorites", 4, False),
|
||||
("artists", 4, True),
|
||||
("albums", 4, True),
|
||||
("playlists", 4, False),
|
||||
("genres", 4, True),
|
||||
("new music", 4, True),
|
||||
("album artists", 4, True),
|
||||
("apps", 3, False),
|
||||
("radios", 3, False),
|
||||
],
|
||||
)
|
||||
async def test_async_browse_media_with_subitems(
|
||||
@@ -89,6 +90,7 @@ async def test_async_browse_media_with_subitems(
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
category: str,
|
||||
child_count: int,
|
||||
can_search: bool,
|
||||
) -> None:
|
||||
"""Test each category with subitems."""
|
||||
with patch(
|
||||
@@ -110,6 +112,7 @@ async def test_async_browse_media_with_subitems(
|
||||
category_level = response["result"]
|
||||
assert category_level["title"] == MEDIA_TYPE_TO_SQUEEZEBOX[category]
|
||||
assert category_level["children"][0]["title"] == "Fake Item 1"
|
||||
assert category_level["children"][0]["can_search"] is can_search
|
||||
assert len(category_level["children"]) == child_count
|
||||
|
||||
# Look up a subitem
|
||||
@@ -128,6 +131,49 @@ async def test_async_browse_media_with_subitems(
|
||||
assert response["success"]
|
||||
search = response["result"]
|
||||
assert search["title"] == "Fake Item 1"
|
||||
assert search["can_search"] is can_search
|
||||
|
||||
|
||||
async def test_async_browse_playlist(
|
||||
hass: HomeAssistant,
|
||||
lms: MagicMock,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test the children of a playlist are tracks that can be played."""
|
||||
client = await hass_ws_client()
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "media_player/browse_media",
|
||||
"entity_id": "media_player.test_player",
|
||||
"media_content_id": FAKE_VALID_ITEM_ID,
|
||||
"media_content_type": MediaType.PLAYLIST,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
child = response["result"]["children"][0]
|
||||
assert child["media_class"] == MediaClass.TRACK
|
||||
assert child["media_content_type"] == MediaType.TRACK
|
||||
assert child["media_content_id"] == FAKE_VALID_ITEM_ID
|
||||
assert not child["can_expand"]
|
||||
assert child["can_play"]
|
||||
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_MEDIA_CONTENT_TYPE: child["media_content_type"],
|
||||
ATTR_MEDIA_CONTENT_ID: child["media_content_id"],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
player = (await lms.async_get_players())[0]
|
||||
player.async_browse.assert_called_with(
|
||||
"titles", limit=ANY, browse_id=("track_id", FAKE_VALID_ITEM_ID)
|
||||
)
|
||||
player.async_load_playlist.assert_called_once()
|
||||
|
||||
|
||||
async def test_async_browse_media_for_apps(
|
||||
@@ -214,6 +260,60 @@ async def test_async_search_media(
|
||||
assert category_level[0]["title"] == "Fake Item 1"
|
||||
|
||||
|
||||
async def test_async_search_media_in_artist(
|
||||
hass: HomeAssistant,
|
||||
lms: MagicMock,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test a search inside an artist returns matching albums and tracks."""
|
||||
|
||||
async def mock_browse(
|
||||
category: str,
|
||||
limit: int,
|
||||
browse_id: tuple[str, str] | None = None,
|
||||
search_query: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
items = {
|
||||
"artist": [{"id": "1", "title": "Love Album"}],
|
||||
"titles": [{"id": "2", "title": "Love Song"}],
|
||||
}
|
||||
return {"title": category, "items": items[category]}
|
||||
|
||||
player = (await lms.async_get_players())[0]
|
||||
player.async_browse.side_effect = mock_browse
|
||||
|
||||
client = await hass_ws_client()
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "media_player/search_media",
|
||||
"entity_id": "media_player.test_player",
|
||||
"media_content_id": "42",
|
||||
"media_content_type": MediaType.ARTIST,
|
||||
"search_query": "love",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert [
|
||||
(
|
||||
item["title"],
|
||||
item["media_class"],
|
||||
item["media_content_type"],
|
||||
item["media_content_id"],
|
||||
item["can_search"],
|
||||
)
|
||||
for item in response["result"]["result"]
|
||||
] == [
|
||||
("Love Album", MediaClass.ALBUM, MediaType.ALBUM, "1", True),
|
||||
("Love Song", MediaClass.TRACK, MediaType.TRACK, "2", False),
|
||||
]
|
||||
assert player.async_browse.call_args_list == [
|
||||
call("artist", limit=ANY, browse_id=("artist_id", "42"), search_query="love"),
|
||||
call("titles", limit=ANY, browse_id=("artist_id", "42"), search_query="love"),
|
||||
]
|
||||
|
||||
|
||||
async def test_async_search_media_invalid_filter(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@@ -241,10 +341,18 @@ async def test_async_search_media_invalid_filter(
|
||||
assert len(response["result"]["result"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"media_content_type",
|
||||
[
|
||||
pytest.param("Fake Type", id="unknown"),
|
||||
pytest.param("artist tracks", id="internal"),
|
||||
],
|
||||
)
|
||||
async def test_async_search_media_invalid_type(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
media_content_type: str,
|
||||
) -> None:
|
||||
"""Test search_media action with invalid media_content_type."""
|
||||
with patch(
|
||||
@@ -258,7 +366,7 @@ async def test_async_search_media_invalid_type(
|
||||
"type": "media_player/search_media",
|
||||
"entity_id": "media_player.test_player",
|
||||
"media_content_id": "",
|
||||
"media_content_type": "Fake Type",
|
||||
"media_content_type": media_content_type,
|
||||
"search_query": "Fake Item 1",
|
||||
},
|
||||
)
|
||||
@@ -266,6 +374,7 @@ async def test_async_search_media_invalid_type(
|
||||
assert not response["success"]
|
||||
err_message = "If specified, Media content type must be one of"
|
||||
assert err_message in response["error"]["message"]
|
||||
assert "artist tracks" not in response["error"]["message"]
|
||||
|
||||
|
||||
async def test_async_search_media_not_found(
|
||||
|
||||
Reference in New Issue
Block a user