Match track titles in a Squeezebox search inside a genre (#183423)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Paulus Schoutsen
2026-10-05 11:00:23 +00:00
committed by Franck Nijhof
co-authored by Claude
parent 75cc467a6a
commit 460d0e7f50
3 changed files with 47 additions and 14 deletions
@@ -47,6 +47,7 @@ MEDIA_TYPE_TO_SQUEEZEBOX: dict[str | MediaType, str] = {
"new music": "new music",
"album artists": "album artists",
"artist tracks": "titles",
"genre tracks": "titles",
MediaType.ALBUM: "album",
MediaType.ARTIST: "artist",
MediaType.TRACK: "title",
@@ -63,6 +64,7 @@ SQUEEZEBOX_ID_BY_TYPE: dict[str | MediaType, str] = {
MediaType.ARTIST: "artist_id",
"artists": "artist_id",
"artist tracks": "artist_id",
"genre tracks": "genre_id",
MediaType.TRACK: "track_id",
"tracks": "track_id",
MediaType.PLAYLIST: "playlist_id",
@@ -90,6 +92,7 @@ CONTENT_TYPE_MEDIA_CLASS: dict[str | MediaType, dict[str, MediaClass | str]] = {
"new music": {"item": MediaClass.DIRECTORY, "children": MediaClass.ALBUM},
"album artists": {"item": MediaClass.DIRECTORY, "children": MediaClass.ARTIST},
"artist tracks": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK},
"genre 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": ""},
@@ -117,6 +120,7 @@ CONTENT_TYPE_TO_CHILD_TYPE: dict[
"new music": MediaType.ALBUM,
"album artists": MediaType.ARTIST,
"artist tracks": MediaType.TRACK,
"genre tracks": MediaType.TRACK,
MediaType.APPS: MediaType.APP,
MediaType.APP: MediaType.TRACK,
"favorite": None,
@@ -665,7 +665,8 @@ 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", "artist tracks"]
if key
not in ["apps", "app", "radios", "radio", "artist tracks", "genre tracks"]
]
_media_content_type_list = (
@@ -687,9 +688,13 @@ 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")
if query.media_content_id:
# Without full text search, LMS matches only album titles inside an
# artist and only artist names inside a genre
if MediaType.ARTIST in _media_content_type_list:
_media_content_type_list.append("artist tracks")
if MediaType.GENRE in _media_content_type_list:
_media_content_type_list.append("genre tracks")
search_response_list: list[BrowseMedia] = []
@@ -260,12 +260,35 @@ async def test_async_search_media(
assert category_level[0]["title"] == "Fake Item 1"
async def test_async_search_media_in_artist(
@pytest.mark.parametrize(
("media_content_type", "category", "id_key", "first_item"),
[
pytest.param(
MediaType.ARTIST,
"artist",
"artist_id",
("Love Album", MediaClass.ALBUM, MediaType.ALBUM, "1", True),
id="artist",
),
pytest.param(
MediaType.GENRE,
"genre",
"genre_id",
("Love Band", MediaClass.ARTIST, MediaType.ARTIST, "1", True),
id="genre",
),
],
)
async def test_async_search_media_in_container(
hass: HomeAssistant,
lms: MagicMock,
hass_ws_client: WebSocketGenerator,
media_content_type: MediaType,
category: str,
id_key: str,
first_item: tuple[str, MediaClass, MediaType, str, bool],
) -> None:
"""Test a search inside an artist returns matching albums and tracks."""
"""Test a search inside an artist or genre also matches track titles."""
async def mock_browse(
category: str,
@@ -274,10 +297,9 @@ async def test_async_search_media_in_artist(
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]}
}.get(category, [{"id": "1", "title": first_item[0]}])
return {"title": category, "items": items}
player = (await lms.async_get_players())[0]
player.async_browse.side_effect = mock_browse
@@ -289,7 +311,7 @@ async def test_async_search_media_in_artist(
"type": "media_player/search_media",
"entity_id": "media_player.test_player",
"media_content_id": "42",
"media_content_type": MediaType.ARTIST,
"media_content_type": media_content_type,
"search_query": "love",
}
)
@@ -305,12 +327,12 @@ async def test_async_search_media_in_artist(
)
for item in response["result"]["result"]
] == [
("Love Album", MediaClass.ALBUM, MediaType.ALBUM, "1", True),
first_item,
("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"),
call(category, limit=ANY, browse_id=(id_key, "42"), search_query="love"),
call("titles", limit=ANY, browse_id=(id_key, "42"), search_query="love"),
]
@@ -345,7 +367,8 @@ async def test_async_search_media_invalid_filter(
"media_content_type",
[
pytest.param("Fake Type", id="unknown"),
pytest.param("artist tracks", id="internal"),
pytest.param("artist tracks", id="internal_artist"),
pytest.param("genre tracks", id="internal_genre"),
],
)
async def test_async_search_media_invalid_type(
@@ -375,6 +398,7 @@ async def test_async_search_media_invalid_type(
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"]
assert "genre tracks" not in response["error"]["message"]
async def test_async_search_media_not_found(