From d431e3ffee15ad7cea04e7b9e7e03bbca78ae3aa Mon Sep 17 00:00:00 2001 From: peteS-UK Date: Tue, 7 Jan 2025 16:17:18 +0000 Subject: [PATCH] Recreate PR134142 after rebase conflicts --- .../components/squeezebox/browse_media.py | 74 ++++++++++++++++++- homeassistant/components/squeezebox/const.py | 10 ++- tests/components/squeezebox/conftest.py | 12 +++ .../squeezebox/test_media_browser.py | 2 + 4 files changed, 92 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index c0458067a23..55c8d4fb3ce 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -18,6 +18,8 @@ from homeassistant.components.media_player import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.network import is_internal_request +from .const import UNPLAYABLE_TYPES + LIBRARY = [ "Favorites", "Artists", @@ -26,6 +28,8 @@ LIBRARY = [ "Playlists", "Genres", "New Music", + "Apps", + "Radios", ] MEDIA_TYPE_TO_SQUEEZEBOX = { @@ -41,6 +45,9 @@ MEDIA_TYPE_TO_SQUEEZEBOX = { MediaType.TRACK: "title", MediaType.PLAYLIST: "playlist", MediaType.GENRE: "genre", + MediaType.APPS: "apps", + "Apps": "apps", + "Radios": "radios", } SQUEEZEBOX_ID_BY_TYPE = { @@ -50,10 +57,14 @@ SQUEEZEBOX_ID_BY_TYPE = { MediaType.PLAYLIST: "playlist_id", MediaType.GENRE: "genre_id", "Favorites": "item_id", + MediaType.APPS: "item_id", } CONTENT_TYPE_MEDIA_CLASS: dict[str | MediaType, dict[str, MediaClass | None]] = { "Favorites": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK}, + "Apps": {"item": MediaClass.DIRECTORY, "children": MediaClass.APP}, + "Radios": {"item": MediaClass.DIRECTORY, "children": MediaClass.APP}, + "App": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK}, "Artists": {"item": MediaClass.DIRECTORY, "children": MediaClass.ARTIST}, "Albums": {"item": MediaClass.DIRECTORY, "children": MediaClass.ALBUM}, "Tracks": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK}, @@ -65,6 +76,8 @@ CONTENT_TYPE_MEDIA_CLASS: dict[str | MediaType, dict[str, MediaClass | None]] = MediaType.TRACK: {"item": MediaClass.TRACK, "children": None}, MediaType.GENRE: {"item": MediaClass.GENRE, "children": MediaClass.ARTIST}, MediaType.PLAYLIST: {"item": MediaClass.PLAYLIST, "children": MediaClass.TRACK}, + MediaType.APP: {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK}, + MediaType.APPS: {"item": MediaClass.DIRECTORY, "children": MediaClass.APP}, } CONTENT_TYPE_TO_CHILD_TYPE = { @@ -78,9 +91,16 @@ CONTENT_TYPE_TO_CHILD_TYPE = { "Playlists": MediaType.PLAYLIST, "Genres": MediaType.GENRE, "Favorites": None, # can only be determined after inspecting the item + "Apps": MediaClass.APP, + "Radios": MediaClass.APP, + "App": None, # can only be determined after inspecting the item "New Music": MediaType.ALBUM, + MediaType.APPS: MediaType.APP, + MediaType.APP: MediaType.TRACK, } +KNOWN_APPS: set[str | None] = set() + async def build_item_response( entity: MediaPlayerEntity, @@ -118,7 +138,7 @@ async def build_item_response( children = [] list_playable = [] for item in result["items"]: - item_id = str(item["id"]) + item_id = str(item.get("id", "")) item_thumbnail: str | None = None if item_type: child_item_type: MediaType | str = item_type @@ -144,6 +164,46 @@ async def build_item_response( can_expand = item["hasitems"] can_play = item["isaudio"] and item.get("url") + if search_type in ["Apps", "Radios"]: + # item["cmd"] contains the name of the command to use with the cli for the app + # add the command to the dictionaries + if item["title"] == "Search" or item.get("type") in UNPLAYABLE_TYPES: + # Skip searches in apps as they'd need UI or if the link isn't to audio + continue + _cmd = "app-" + item["cmd"] + MEDIA_TYPE_TO_SQUEEZEBOX.update({_cmd: _cmd}) + SQUEEZEBOX_ID_BY_TYPE.update({_cmd: "item_id"}) + CONTENT_TYPE_MEDIA_CLASS.update( + { + _cmd: { + "item": MediaClass.DIRECTORY, + "children": MediaClass.TRACK, + } + } + ) + CONTENT_TYPE_TO_CHILD_TYPE.update({_cmd: MediaType.TRACK}) + + if _cmd not in KNOWN_APPS: + KNOWN_APPS.add(_cmd) + + child_item_type = _cmd + child_media_class = CONTENT_TYPE_MEDIA_CLASS[_cmd] + can_expand = True + can_play = False + + if search_type in KNOWN_APPS: + if ( + item.get("title") in ["Search", None] + or item.get("type") in UNPLAYABLE_TYPES + ): + # Skip searches in apps as they'd need UI + continue + + child_item_type = search_type + child_media_class = CONTENT_TYPE_MEDIA_CLASS[search_type] + can_play = item["isaudio"] and item.get("url") + can_expand = item["hasitems"] + if artwork_track_id := item.get("artwork_track_id"): if internal_request: item_thumbnail = player.generate_image_url_from_track_id( @@ -153,6 +213,8 @@ async def build_item_response( item_thumbnail = entity.get_browse_image_url( item_type, item_id, artwork_track_id ) + elif search_type in ["Apps", "Radios"]: + item_thumbnail = player.generate_image_url(item["icon"]) else: item_thumbnail = item.get("image_url") # will not be proxied by HA @@ -176,6 +238,7 @@ async def build_item_response( assert media_class["item"] is not None if not search_id: search_id = search_type + return BrowseMedia( title=result.get("title"), media_class=media_class["item"], @@ -215,7 +278,7 @@ async def library_payload(hass: HomeAssistant, player: Player) -> BrowseMedia: media_class=media_class["children"], media_content_id=item, media_content_type=item, - can_play=item != "Favorites", + can_play=item not in ["Favorites", "Apps", "Radios"], can_expand=True, ) ) @@ -251,8 +314,13 @@ async def generate_playlist( raise BrowseError(f"Media type not supported: {media_type}") browse_id = (SQUEEZEBOX_ID_BY_TYPE[media_type], media_id) + if media_type[:4] == "app-": + category = media_type + else: + category = "titles" + result = await player.async_browse( - "titles", limit=browse_limit, browse_id=browse_id + category, limit=browse_limit, browse_id=browse_id ) if result and "items" in result: items: list = result["items"] diff --git a/homeassistant/components/squeezebox/const.py b/homeassistant/components/squeezebox/const.py index 61ec3cac2fa..e4c6be6213c 100644 --- a/homeassistant/components/squeezebox/const.py +++ b/homeassistant/components/squeezebox/const.py @@ -27,7 +27,12 @@ STATUS_QUERY_LIBRARYNAME = "libraryname" STATUS_QUERY_MAC = "mac" STATUS_QUERY_UUID = "uuid" STATUS_QUERY_VERSION = "version" -SQUEEZEBOX_SOURCE_STRINGS = ("source:", "wavin:", "spotify:") +SQUEEZEBOX_SOURCE_STRINGS = ( + "source:", + "wavin:", + "spotify:", + "loop:", +) SIGNAL_PLAYER_DISCOVERED = "squeezebox_player_discovered" SIGNAL_PLAYER_REDISCOVERED = "squeezebox_player_rediscovered" DISCOVERY_INTERVAL = 60 @@ -36,5 +41,4 @@ CONF_BROWSE_LIMIT = "browse_limit" CONF_VOLUME_STEP = "volume_step" DEFAULT_BROWSE_LIMIT = 1000 DEFAULT_VOLUME_STEP = 5 -ATTR_ANNOUNCE_VOLUME = "announce_volume" -ATTR_ANNOUNCE_TIMEOUT = "announce_timeout" +UNPLAYABLE_TYPES = ("text", "actions") diff --git a/tests/components/squeezebox/conftest.py b/tests/components/squeezebox/conftest.py index 9224334a716..3e76562d6fd 100644 --- a/tests/components/squeezebox/conftest.py +++ b/tests/components/squeezebox/conftest.py @@ -142,6 +142,9 @@ async def mock_async_browse( "title": "title", "playlists": "playlist", "playlist": "title", + "apps": "app", + "radios": "app", + "app-fakecommand": "track", } fake_items = [ { @@ -152,6 +155,8 @@ async def mock_async_browse( "item_type": child_types[media_type], "artwork_track_id": "b35bb9e9", "url": "file:///var/lib/squeezeboxserver/music/track_1.mp3", + "cmd": "fakecommand", + "icon": "plugins/Qobuz/html/images/qobuz.png", }, { "title": "Fake Item 2", @@ -161,6 +166,8 @@ async def mock_async_browse( "item_type": child_types[media_type], "image_url": "http://lms.internal:9000/html/images/favorites.png", "url": "file:///var/lib/squeezeboxserver/music/track_2.mp3", + "cmd": "fakecommand", + "icon": "plugins/Qobuz/html/images/qobuz.png", }, { "title": "Fake Item 3", @@ -169,6 +176,8 @@ async def mock_async_browse( "isaudio": True, "album_id": FAKE_VALID_ITEM_ID if media_type == "favorites" else None, "url": "file:///var/lib/squeezeboxserver/music/track_3.mp3", + "cmd": "fakecommand", + "icon": "plugins/Qobuz/html/images/qobuz.png", }, ] @@ -232,6 +241,9 @@ def mock_pysqueezebox_player(uuid: str) -> MagicMock: mock_player.async_play_announcement = AsyncMock( side_effect=mock_async_play_announcement ) + mock_player.generate_image_url = MagicMock( + return_value="http://lms.internal:9000/html/images/favorites.png" + ) mock_player.name = TEST_PLAYER_NAME mock_player.player_id = uuid mock_player.mode = "stop" diff --git a/tests/components/squeezebox/test_media_browser.py b/tests/components/squeezebox/test_media_browser.py index c03c1b6344d..e1c8a7be777 100644 --- a/tests/components/squeezebox/test_media_browser.py +++ b/tests/components/squeezebox/test_media_browser.py @@ -79,6 +79,8 @@ async def test_async_browse_media_with_subitems( "Playlists", "Genres", "New Music", + "Apps", + "Radios", ): with patch( "homeassistant.components.squeezebox.browse_media.is_internal_request",