diff --git a/.coveragerc b/.coveragerc index 4ad141168d8..5bfcf083c51 100644 --- a/.coveragerc +++ b/.coveragerc @@ -822,6 +822,7 @@ omit = homeassistant/components/spotify/__init__.py homeassistant/components/spotify/media_player.py homeassistant/components/squeezebox/__init__.py + homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/media_player.py homeassistant/components/starline/* homeassistant/components/starlingbank/sensor.py diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py new file mode 100644 index 00000000000..3e5523e8475 --- /dev/null +++ b/homeassistant/components/squeezebox/browse_media.py @@ -0,0 +1,171 @@ +"""Support for media browsing.""" +from homeassistant.components.media_player import BrowseError, BrowseMedia +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_GENRE, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_TRACK, + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_GENRE, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_TRACK, +) + +LIBRARY = ["Artists", "Albums", "Tracks", "Playlists", "Genres"] + +MEDIA_TYPE_TO_SQUEEZEBOX = { + "Artists": "artists", + "Albums": "albums", + "Tracks": "titles", + "Playlists": "playlists", + "Genres": "genres", + MEDIA_TYPE_ALBUM: "album", + MEDIA_TYPE_ARTIST: "artist", + MEDIA_TYPE_TRACK: "title", + MEDIA_TYPE_PLAYLIST: "playlist", + MEDIA_TYPE_GENRE: "genre", +} + +SQUEEZEBOX_ID_BY_TYPE = { + MEDIA_TYPE_ALBUM: "album_id", + MEDIA_TYPE_ARTIST: "artist_id", + MEDIA_TYPE_TRACK: "track_id", + MEDIA_TYPE_PLAYLIST: "playlist_id", + MEDIA_TYPE_GENRE: "genre_id", +} + +CONTENT_TYPE_MEDIA_CLASS = { + "Artists": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_ARTIST}, + "Albums": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_ALBUM}, + "Tracks": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_TRACK}, + "Playlists": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_PLAYLIST}, + "Genres": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_GENRE}, + MEDIA_TYPE_ALBUM: {"item": MEDIA_CLASS_ALBUM, "children": MEDIA_CLASS_TRACK}, + MEDIA_TYPE_ARTIST: {"item": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ALBUM}, + MEDIA_TYPE_TRACK: {"item": MEDIA_CLASS_TRACK, "children": None}, + MEDIA_TYPE_GENRE: {"item": MEDIA_CLASS_GENRE, "children": MEDIA_CLASS_ARTIST}, + MEDIA_TYPE_PLAYLIST: { + "item": MEDIA_CLASS_PLAYLIST, + "children": MEDIA_CLASS_TRACK, + }, +} + +CONTENT_TYPE_TO_CHILD_TYPE = { + MEDIA_TYPE_ALBUM: MEDIA_TYPE_TRACK, + MEDIA_TYPE_PLAYLIST: MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_ARTIST: MEDIA_TYPE_ALBUM, + MEDIA_TYPE_GENRE: MEDIA_TYPE_ARTIST, + "Artists": MEDIA_TYPE_ARTIST, + "Albums": MEDIA_TYPE_ALBUM, + "Tracks": MEDIA_TYPE_TRACK, + "Playlists": MEDIA_TYPE_PLAYLIST, + "Genres": MEDIA_TYPE_GENRE, +} + +BROWSE_LIMIT = 500 + + +async def build_item_response(player, payload): + """Create response payload for search described by payload.""" + search_id = payload["search_id"] + search_type = payload["search_type"] + + media_class = CONTENT_TYPE_MEDIA_CLASS[search_type] + + if search_id and search_id != search_type: + browse_id = (SQUEEZEBOX_ID_BY_TYPE[search_type], search_id) + else: + browse_id = None + + result = await player.async_browse( + MEDIA_TYPE_TO_SQUEEZEBOX[search_type], + limit=BROWSE_LIMIT, + browse_id=browse_id, + ) + + children = None + + if result is not None and result.get("items"): + item_type = CONTENT_TYPE_TO_CHILD_TYPE[search_type] + child_media_class = CONTENT_TYPE_MEDIA_CLASS[item_type] + + children = [] + for item in result["items"]: + children.append( + BrowseMedia( + title=item["title"], + media_class=child_media_class["item"], + media_content_id=str(item["id"]), + media_content_type=item_type, + can_play=True, + can_expand=child_media_class["children"] is not None, + thumbnail=item.get("image_url"), + ) + ) + + if children is None: + raise BrowseError(f"Media not found: {search_type} / {search_id}") + + return BrowseMedia( + title=result.get("title"), + media_class=media_class["item"], + children_media_class=media_class["children"], + media_content_id=search_id, + media_content_type=search_type, + can_play=True, + children=children, + can_expand=True, + ) + + +async def library_payload(player): + """Create response payload to describe contents of library.""" + + library_info = { + "title": "Music Library", + "media_class": MEDIA_CLASS_DIRECTORY, + "media_content_id": "library", + "media_content_type": "library", + "can_play": False, + "can_expand": True, + "children": [], + } + + for item in LIBRARY: + media_class = CONTENT_TYPE_MEDIA_CLASS[item] + result = await player.async_browse( + MEDIA_TYPE_TO_SQUEEZEBOX[item], + limit=1, + ) + if result is not None and result.get("items") is not None: + library_info["children"].append( + BrowseMedia( + title=item, + media_class=media_class["children"], + media_content_id=item, + media_content_type=item, + can_play=True, + can_expand=True, + ) + ) + + response = BrowseMedia(**library_info) + return response + + +async def generate_playlist(player, payload): + """Generate playlist from browsing payload.""" + media_type = payload["search_type"] + media_id = payload["search_id"] + + if media_type not in SQUEEZEBOX_ID_BY_TYPE: + return None + + browse_id = (SQUEEZEBOX_ID_BY_TYPE[media_type], media_id) + result = await player.async_browse( + "titles", limit=BROWSE_LIMIT, browse_id=browse_id + ) + return result.get("items") diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index 2ebde216130..af358b832f7 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -6,7 +6,7 @@ "@rajlaud" ], "requirements": [ - "pysqueezebox==0.3.1" + "pysqueezebox==0.5.1" ], "config_flow": true } diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index a4b62d33f39..9a7baffb3f8 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -12,6 +12,7 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, + SUPPORT_BROWSE_MEDIA, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -48,6 +49,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.util.dt import utcnow +from .browse_media import build_item_response, generate_playlist, library_payload from .const import ( DEFAULT_PORT, DISCOVERY_TASK, @@ -71,7 +73,8 @@ _LOGGER = logging.getLogger(__name__) DISCOVERY_INTERVAL = 60 SUPPORT_SQUEEZEBOX = ( - SUPPORT_PAUSE + SUPPORT_BROWSE_MEDIA + | SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK @@ -476,15 +479,40 @@ class SqueezeBoxEntity(MediaPlayerEntity): If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the current playlist. """ cmd = "play" + index = None + if kwargs.get(ATTR_MEDIA_ENQUEUE): cmd = "add" - if media_type == MEDIA_TYPE_PLAYLIST: - content = json.loads(media_id) - await self._player.async_load_playlist(content["urls"], cmd) - await self._player.async_index(content["index"]) - else: + if media_type == MEDIA_TYPE_MUSIC: await self._player.async_load_url(media_id, cmd) + return + + if media_type == MEDIA_TYPE_PLAYLIST: + try: + # a saved playlist by number + payload = { + "search_id": int(media_id), + "search_type": MEDIA_TYPE_PLAYLIST, + } + playlist = await generate_playlist(self._player, payload) + except ValueError: + # a list of urls + content = json.loads(media_id) + playlist = content["urls"] + index = content["index"] + else: + payload = { + "search_id": media_id, + "search_type": media_type, + } + playlist = await generate_playlist(self._player, payload) + + _LOGGER.debug("Generated playlist: %s", playlist) + + await self._player.async_load_playlist(playlist, cmd) + if index is not None: + await self._player.async_index(index) async def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" @@ -541,3 +569,22 @@ class SqueezeBoxEntity(MediaPlayerEntity): async def async_unsync(self): """Unsync this Squeezebox player.""" await self._player.async_unsync() + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + + _LOGGER.debug( + "Reached async_browse_media with content_type %s and content_id %s", + media_content_type, + media_content_id, + ) + + if media_content_type in [None, "library"]: + return await library_payload(self._player) + + payload = { + "search_type": media_content_type, + "search_id": media_content_id, + } + + return await build_item_response(self._player, payload) diff --git a/requirements_all.txt b/requirements_all.txt index 19641cae45c..173a1210183 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1691,7 +1691,7 @@ pysonos==0.0.35 pyspcwebgw==0.4.0 # homeassistant.components.squeezebox -pysqueezebox==0.3.1 +pysqueezebox==0.5.1 # homeassistant.components.stiebel_eltron pystiebeleltron==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c02bb72fe86..78be9e667c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -829,7 +829,7 @@ pysonos==0.0.35 pyspcwebgw==0.4.0 # homeassistant.components.squeezebox -pysqueezebox==0.3.1 +pysqueezebox==0.5.1 # homeassistant.components.syncthru pysyncthru==0.7.0