From 2a879afc7ab70a91259ed3eb078884e48e240fa7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 8 Sep 2020 16:42:01 +0200 Subject: [PATCH] Add media class browse media attribute (#39770) --- .../components/arcam_fmj/media_player.py | 4 + homeassistant/components/kodi/browse_media.py | 21 +++++ .../components/media_player/__init__.py | 4 + .../components/media_player/const.py | 23 ++++++ .../components/media_source/local_source.py | 2 + .../components/media_source/models.py | 4 + .../components/netatmo/media_source.py | 6 +- .../components/philips_js/media_player.py | 4 + .../components/plex/media_browser.py | 65 ++++++++++++++-- homeassistant/components/roku/media_player.py | 12 +++ .../components/sonos/media_player.py | 30 ++++++++ .../components/spotify/media_player.py | 77 ++++++++++++++++--- tests/components/media_source/test_init.py | 2 + tests/components/media_source/test_models.py | 12 ++- 14 files changed, 248 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 7e6c34a8324..1f0f564c59b 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -7,6 +7,8 @@ from arcam.fmj.state import State from homeassistant import config_entries from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_MUSIC, MEDIA_TYPE_MUSIC, SUPPORT_BROWSE_MEDIA, SUPPORT_PLAY_MEDIA, @@ -255,6 +257,7 @@ class ArcamFmj(MediaPlayerEntity): radio = [ BrowseMedia( title=preset.name, + media_class=MEDIA_CLASS_MUSIC, media_content_id=f"preset:{preset.index}", media_content_type=MEDIA_TYPE_MUSIC, can_play=True, @@ -265,6 +268,7 @@ class ArcamFmj(MediaPlayerEntity): root = BrowseMedia( title="Root", + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="root", media_content_type="library", can_play=False, diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index d308b48101b..1b1576e82da 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -2,6 +2,14 @@ from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_MOVIE, + MEDIA_CLASS_MUSIC, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_SEASON, + MEDIA_CLASS_TV_SHOW, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, MEDIA_TYPE_EPISODE, @@ -26,6 +34,16 @@ EXPANDABLE_MEDIA_TYPES = [ MEDIA_TYPE_SEASON, ] +CONTENT_TYPE_MEDIA_CLASS = { + "library_music": MEDIA_CLASS_MUSIC, + MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, + MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, + MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, + MEDIA_TYPE_MOVIE: MEDIA_CLASS_MOVIE, + MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, + MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, +} + async def build_item_response(media_library, payload): """Create response payload for the provided media query.""" @@ -124,6 +142,7 @@ async def build_item_response(media_library, payload): return return BrowseMedia( + media_class=CONTENT_TYPE_MEDIA_CLASS[search_type], media_content_id=payload["search_id"], media_content_type=search_type, title=title, @@ -177,6 +196,7 @@ def item_payload(item, media_library): return BrowseMedia( title=title, + media_class=CONTENT_TYPE_MEDIA_CLASS[item["type"]], media_content_type=media_content_type, media_content_id=media_content_id, can_play=can_play, @@ -192,6 +212,7 @@ def library_payload(media_library): Used by async_browse_media. """ library_info = BrowseMedia( + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="library", media_content_type="library", title="Media Library", diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 16cabe5edb9..718011b4a76 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -822,6 +822,7 @@ class MediaPlayerEntity(Entity): Payload should follow this format: { "title": str - Title of the item + "media_class": str - Media class "media_content_type": str - see below "media_content_id": str - see below - Can be passed back in to browse further @@ -1046,6 +1047,7 @@ class BrowseMedia: def __init__( self, *, + media_class: str, media_content_id: str, media_content_type: str, title: str, @@ -1055,6 +1057,7 @@ class BrowseMedia: thumbnail: Optional[str] = None, ): """Initialize browse media item.""" + self.media_class = media_class self.media_content_id = media_content_id self.media_content_type = media_content_type self.title = title @@ -1067,6 +1070,7 @@ class BrowseMedia: """Convert Media class to browse media dictionary.""" response = { "title": self.title, + "media_class": self.media_class, "media_content_type": self.media_content_type, "media_content_id": self.media_content_id, "can_play": self.can_play, diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 74c65fcd780..6714d03c19e 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -29,6 +29,29 @@ ATTR_SOUND_MODE_LIST = "sound_mode_list" DOMAIN = "media_player" +MEDIA_CLASS_ALBUM = "album" +MEDIA_CLASS_APP = "app" +MEDIA_CLASS_APPS = "apps" +MEDIA_CLASS_ARTIST = "artist" +MEDIA_CLASS_CHANNEL = "channel" +MEDIA_CLASS_CHANNELS = "channels" +MEDIA_CLASS_COMPOSER = "composer" +MEDIA_CLASS_CONTRIBUTING_ARTIST = "contributing_artist" +MEDIA_CLASS_DIRECTORY = "directory" +MEDIA_CLASS_EPISODE = "episode" +MEDIA_CLASS_GAME = "game" +MEDIA_CLASS_GENRE = "genre" +MEDIA_CLASS_IMAGE = "image" +MEDIA_CLASS_MOVIE = "movie" +MEDIA_CLASS_MUSIC = "music" +MEDIA_CLASS_PLAYLIST = "playlist" +MEDIA_CLASS_PODCAST = "podcast" +MEDIA_CLASS_SEASON = "season" +MEDIA_CLASS_TRACK = "track" +MEDIA_CLASS_TV_SHOW = "tv_show" +MEDIA_CLASS_URL = "url" +MEDIA_CLASS_VIDEO = "video" + MEDIA_TYPE_ALBUM = "album" MEDIA_TYPE_APP = "app" MEDIA_TYPE_APPS = "apps" diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index d670cee1676..774ee64d852 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -6,6 +6,7 @@ from typing import Tuple from aiohttp import web from homeassistant.components.http import HomeAssistantView +from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source.error import Unresolvable from homeassistant.core import HomeAssistant, callback @@ -114,6 +115,7 @@ class LocalSource(MediaSource): media = BrowseMediaSource( domain=DOMAIN, identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}", + media_class=MEDIA_CLASS_DIRECTORY, media_content_type="directory", title=title, can_play=is_file, diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index cd8e44f4a24..5d768fd79d8 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -5,6 +5,8 @@ from typing import List, Optional, Tuple from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_CHANNELS, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, ) @@ -52,6 +54,7 @@ class MediaSourceItem: base = BrowseMediaSource( domain=None, identifier=None, + media_class=MEDIA_CLASS_CHANNELS, media_content_type=MEDIA_TYPE_CHANNELS, title="Media Sources", can_play=False, @@ -61,6 +64,7 @@ class MediaSourceItem: BrowseMediaSource( domain=source.domain, identifier=None, + media_class=MEDIA_CLASS_CHANNEL, media_content_type=MEDIA_TYPE_CHANNEL, title=source.name, can_play=False, diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index 09c34ec41af..02ffd608472 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -4,7 +4,10 @@ import logging import re from typing import Optional, Tuple -from homeassistant.components.media_player.const import MEDIA_TYPE_VIDEO +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_VIDEO, + MEDIA_TYPE_VIDEO, +) from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source.const import MEDIA_MIME_TYPES from homeassistant.components.media_source.error import MediaSourceError, Unresolvable @@ -91,6 +94,7 @@ class NetatmoSource(MediaSource): media = BrowseMediaSource( domain=DOMAIN, identifier=path, + media_class=MEDIA_CLASS_VIDEO, media_content_type=MEDIA_TYPE_VIDEO, title=title, can_play=bool( diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 0e2d3f97c49..a780fe6b635 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -11,6 +11,8 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_CHANNELS, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, SUPPORT_BROWSE_MEDIA, @@ -288,6 +290,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): return BrowseMedia( title="Channels", + media_class=MEDIA_CLASS_CHANNELS, media_content_id="", media_content_type=MEDIA_TYPE_CHANNELS, can_play=False, @@ -295,6 +298,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): children=[ BrowseMedia( title=channel, + media_class=MEDIA_CLASS_CHANNEL, media_content_id=channel, media_content_type=MEDIA_TYPE_CHANNEL, can_play=True, diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 1d3f3616450..9d5572a4faa 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -2,13 +2,31 @@ import logging from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_EPISODE, + MEDIA_CLASS_MOVIE, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_SEASON, + MEDIA_CLASS_TRACK, + MEDIA_CLASS_TV_SHOW, + MEDIA_CLASS_VIDEO, +) from homeassistant.components.media_player.errors import BrowseError from .const import DOMAIN + +class UnknownMediaType(BrowseError): + """Unknown media type.""" + + EXPANDABLES = ["album", "artist", "playlist", "season", "show"] PLAYLISTS_BROWSE_PAYLOAD = { "title": "Playlists", + "media_class": MEDIA_CLASS_PLAYLIST, "media_content_id": "all", "media_content_type": "playlists", "can_play": False, @@ -19,6 +37,18 @@ SPECIAL_METHODS = { "Recently Added": "recentlyAdded", } +ITEM_TYPE_MEDIA_CLASS = { + "album": MEDIA_CLASS_ALBUM, + "artist": MEDIA_CLASS_ARTIST, + "episode": MEDIA_CLASS_EPISODE, + "movie": MEDIA_CLASS_MOVIE, + "playlist": MEDIA_CLASS_PLAYLIST, + "season": MEDIA_CLASS_SEASON, + "show": MEDIA_CLASS_TV_SHOW, + "track": MEDIA_CLASS_TRACK, + "video": MEDIA_CLASS_VIDEO, +} + _LOGGER = logging.getLogger(__name__) @@ -34,11 +64,17 @@ def browse_media( if media is None: return None - media_info = item_payload(media) + try: + media_info = item_payload(media) + except UnknownMediaType: + return None if media_info.can_expand: media_info.children = [] for item in media: - media_info.children.append(item_payload(item)) + try: + media_info.children.append(item_payload(item)) + except UnknownMediaType: + continue return media_info if media_content_id and ":" in media_content_id: @@ -65,6 +101,7 @@ def browse_media( payload = { "title": title, + "media_class": MEDIA_CLASS_DIRECTORY, "media_content_id": f"{media_content_id}:{special_folder}", "media_content_type": media_content_type, "can_play": False, @@ -75,7 +112,10 @@ def browse_media( method = SPECIAL_METHODS[special_folder] items = getattr(library_or_section, method)() for item in items: - payload["children"].append(item_payload(item)) + try: + payload["children"].append(item_payload(item)) + except UnknownMediaType: + continue return BrowseMedia(**payload) if media_content_type in ["server", None]: @@ -99,8 +139,14 @@ def browse_media( def item_payload(item): """Create response payload for a single media item.""" + try: + media_class = ITEM_TYPE_MEDIA_CLASS[item.type] + except KeyError as err: + _LOGGER.debug("Unknown type received: %s", item.type) + raise UnknownMediaType from err payload = { "title": item.title, + "media_class": media_class, "media_content_id": str(item.ratingKey), "media_content_type": item.type, "can_play": True, @@ -116,6 +162,7 @@ def library_section_payload(section): """Create response payload for a single library section.""" return BrowseMedia( title=section.title, + media_class=MEDIA_CLASS_DIRECTORY, media_content_id=section.key, media_content_type="library", can_play=False, @@ -128,6 +175,7 @@ def special_library_payload(parent_payload, special_type): title = f"{special_type} ({parent_payload.title})" return BrowseMedia( title=title, + media_class=parent_payload.media_class, media_content_id=f"{parent_payload.media_content_id}:{special_type}", media_content_type=parent_payload.media_content_type, can_play=False, @@ -139,6 +187,7 @@ def server_payload(plex_server): """Create response payload to describe libraries of the Plex server.""" server_info = BrowseMedia( title=plex_server.friendly_name, + media_class=MEDIA_CLASS_DIRECTORY, media_content_id=plex_server.machine_identifier, media_content_type="server", can_play=False, @@ -165,7 +214,10 @@ def library_payload(plex_server, library_id): special_library_payload(library_info, "Recently Added") ) for item in library.all(): - library_info.children.append(item_payload(item)) + try: + library_info.children.append(item_payload(item)) + except UnknownMediaType: + continue return library_info @@ -173,5 +225,8 @@ def playlists_payload(plex_server): """Create response payload for all available playlists.""" playlists_info = {**PLAYLISTS_BROWSE_PAYLOAD, "children": []} for playlist in plex_server.playlists(): - playlists_info["children"].append(item_payload(playlist)) + try: + playlists_info["children"].append(item_payload(playlist)) + except UnknownMediaType: + continue return BrowseMedia(**playlists_info) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index c8f1b6d999d..7935206f114 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -11,6 +11,11 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( + MEDIA_CLASS_APP, + MEDIA_CLASS_APPS, + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_CHANNELS, + MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_APP, MEDIA_TYPE_APPS, MEDIA_TYPE_CHANNEL, @@ -79,6 +84,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia: """Create response payload to describe contents of a specific library.""" library_info = BrowseMedia( title="Media Library", + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="library", media_content_type="library", can_play=False, @@ -89,6 +95,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia: library_info.children.append( BrowseMedia( title="Apps", + media_class=MEDIA_CLASS_APPS, media_content_id="apps", media_content_type=MEDIA_TYPE_APPS, can_expand=True, @@ -100,6 +107,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia: library_info.children.append( BrowseMedia( title="Channels", + media_class=MEDIA_CLASS_CHANNELS, media_content_id="channels", media_content_type=MEDIA_TYPE_CHANNELS, can_expand=True, @@ -286,6 +294,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if media_content_type == MEDIA_TYPE_APPS: response = BrowseMedia( title="Apps", + media_class=MEDIA_CLASS_APPS, media_content_id="apps", media_content_type=MEDIA_TYPE_APPS, can_expand=True, @@ -294,6 +303,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): BrowseMedia( title=app.name, thumbnail=self.coordinator.roku.app_icon_url(app.app_id), + media_class=MEDIA_CLASS_APP, media_content_id=app.app_id, media_content_type=MEDIA_TYPE_APP, can_play=True, @@ -306,6 +316,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if media_content_type == MEDIA_TYPE_CHANNELS: response = BrowseMedia( title="Channels", + media_class=MEDIA_CLASS_CHANNELS, media_content_id="channels", media_content_type=MEDIA_TYPE_CHANNELS, can_expand=True, @@ -313,6 +324,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): children=[ BrowseMedia( title=channel.name, + media_class=MEDIA_CLASS_CHANNEL, media_content_id=channel.number, media_content_type=MEDIA_TYPE_CHANNEL, can_play=True, diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 47610f242eb..9a245f8d4d7 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -17,6 +17,14 @@ import voluptuous as vol from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_COMPOSER, + MEDIA_CLASS_CONTRIBUTING_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_GENRE, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_TRACK, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, MEDIA_TYPE_COMPOSER, @@ -103,6 +111,23 @@ EXPANDABLE_MEDIA_TYPES = [ SONOS_PLAYLISTS, ] +SONOS_TO_MEDIA_CLASSES = { + SONOS_ALBUM: MEDIA_CLASS_ALBUM, + SONOS_ALBUM_ARTIST: MEDIA_CLASS_ARTIST, + SONOS_ARTIST: MEDIA_CLASS_CONTRIBUTING_ARTIST, + SONOS_COMPOSER: MEDIA_CLASS_COMPOSER, + SONOS_GENRE: MEDIA_CLASS_GENRE, + SONOS_PLAYLISTS: MEDIA_CLASS_PLAYLIST, + SONOS_TRACKS: MEDIA_CLASS_TRACK, + "object.container.album.musicAlbum": MEDIA_CLASS_ALBUM, + "object.container.genre.musicGenre": MEDIA_CLASS_PLAYLIST, + "object.container.person.composer": MEDIA_CLASS_PLAYLIST, + "object.container.person.musicArtist": MEDIA_CLASS_ARTIST, + "object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST, + "object.container.playlistContainer": MEDIA_CLASS_PLAYLIST, + "object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK, +} + SONOS_TO_MEDIA_TYPES = { SONOS_ALBUM: MEDIA_TYPE_ALBUM, SONOS_ALBUM_ARTIST: MEDIA_TYPE_ARTIST, @@ -1462,9 +1487,12 @@ def build_item_response(media_library, payload): except IndexError: title = LIBRARY_TITLES_MAPPING[payload["idstring"]] + media_class = SONOS_TO_MEDIA_CLASSES[MEDIA_TYPES_TO_SONOS[payload["search_type"]]] + return BrowseMedia( title=title, thumbnail=thumbnail, + media_class=media_class, media_content_id=payload["idstring"], media_content_type=payload["search_type"], children=[item_payload(item) for item in media], @@ -1482,6 +1510,7 @@ def item_payload(item): return BrowseMedia( title=item.title, thumbnail=getattr(item, "album_art_uri", None), + media_class=SONOS_TO_MEDIA_CLASSES[get_media_type(item)], media_content_id=get_content_id(item), media_content_type=SONOS_TO_MEDIA_TYPES[get_media_type(item)], can_play=can_play(item.item_class), @@ -1497,6 +1526,7 @@ def library_payload(media_library): """ return BrowseMedia( title="Music Library", + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="library", media_content_type="library", can_play=False, diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index cc62cb9e276..91e0c85aa3c 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -11,6 +11,12 @@ from yarl import URL from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_PODCAST, + MEDIA_CLASS_TRACK, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, MEDIA_TYPE_EPISODE, @@ -96,6 +102,29 @@ LIBRARY_MAP = { "new_releases": "New Releases", } +CONTENT_TYPE_MEDIA_CLASS = { + "current_user_playlists": MEDIA_CLASS_PLAYLIST, + "current_user_followed_artists": MEDIA_CLASS_ARTIST, + "current_user_saved_albums": MEDIA_CLASS_ALBUM, + "current_user_saved_tracks": MEDIA_CLASS_TRACK, + "current_user_saved_shows": MEDIA_CLASS_PODCAST, + "current_user_recently_played": MEDIA_CLASS_TRACK, + "current_user_top_artists": MEDIA_CLASS_ARTIST, + "current_user_top_tracks": MEDIA_CLASS_TRACK, + "featured_playlists": MEDIA_CLASS_PLAYLIST, + "categories": MEDIA_CLASS_DIRECTORY, + "category_playlists": MEDIA_CLASS_PLAYLIST, + "new_releases": MEDIA_CLASS_ALBUM, + MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, + MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, + MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, + MEDIA_TYPE_SHOW: MEDIA_CLASS_PODCAST, +} + + +class MissingMediaInformation(BrowseError): + """Missing media required information.""" + async def async_setup_entry( hass: HomeAssistant, @@ -498,24 +527,32 @@ def build_item_response(spotify, user, payload): return None if media_content_type == "categories": - return BrowseMedia( + media_item = BrowseMedia( title=LIBRARY_MAP.get(media_content_id), + media_class=CONTENT_TYPE_MEDIA_CLASS[media_content_type], media_content_id=media_content_id, media_content_type=media_content_type, can_play=False, can_expand=True, - children=[ + children=[], + ) + for item in items: + try: + item_id = item["id"] + except KeyError: + _LOGGER.debug("Missing id for media item: %s", item) + continue + media_item.children.append( BrowseMedia( title=item.get("name"), - media_content_id=item["id"], + media_class=MEDIA_CLASS_PLAYLIST, + media_content_id=item_id, media_content_type="category_playlists", thumbnail=fetch_image_url(item, key="icons"), can_play=False, can_expand=True, ) - for item in items - ], - ) + ) if title is None: if "name" in media: @@ -525,12 +562,18 @@ def build_item_response(spotify, user, payload): response = { "title": title, + "media_class": CONTENT_TYPE_MEDIA_CLASS[media_content_type], "media_content_id": media_content_id, "media_content_type": media_content_type, "can_play": media_content_type in PLAYABLE_MEDIA_TYPES, - "children": [item_payload(item) for item in items], + "children": [], "can_expand": True, } + for item in items: + try: + response["children"].append(item_payload(item)) + except MissingMediaInformation: + continue if "images" in media: response["thumbnail"] = fetch_image_url(media) @@ -546,20 +589,31 @@ def item_payload(item): Used by async_browse_media. """ + try: + media_type = item["type"] + media_id = item["uri"] + except KeyError as err: + _LOGGER.debug("Missing type or uri for media item: %s", item) + raise MissingMediaInformation from err - can_expand = item["type"] not in [ + can_expand = media_type not in [ MEDIA_TYPE_TRACK, MEDIA_TYPE_EPISODE, ] payload = { "title": item.get("name"), - "media_content_id": item["uri"], - "media_content_type": item["type"], - "can_play": item["type"] in PLAYABLE_MEDIA_TYPES, + "media_content_id": media_id, + "media_content_type": media_type, + "can_play": media_type in PLAYABLE_MEDIA_TYPES, "can_expand": can_expand, } + payload = { + **payload, + "media_class": CONTENT_TYPE_MEDIA_CLASS[media_type], + } + if "images" in item: payload["thumbnail"] = fetch_image_url(item) elif MEDIA_TYPE_ALBUM in item: @@ -576,6 +630,7 @@ def library_payload(): """ library_info = { "title": "Media Library", + "media_class": MEDIA_CLASS_DIRECTORY, "media_content_id": "library", "media_content_type": "library", "can_play": False, diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index eb387fcc6a3..68e0fcda1d8 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components import media_source +from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source import const from homeassistant.setup import async_setup_component @@ -77,6 +78,7 @@ async def test_websocket_browse_media(hass, hass_ws_client): domain=const.DOMAIN, identifier="/media", title="Local Media", + media_class=MEDIA_CLASS_DIRECTORY, media_content_type="listing", can_play=False, can_expand=True, diff --git a/tests/components/media_source/test_models.py b/tests/components/media_source/test_models.py index f951fcfb0c0..3d19edd722d 100644 --- a/tests/components/media_source/test_models.py +++ b/tests/components/media_source/test_models.py @@ -1,5 +1,9 @@ """Test Media Source model methods.""" -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_MUSIC, + MEDIA_TYPE_MUSIC, +) from homeassistant.components.media_source import const, models @@ -8,6 +12,7 @@ async def test_browse_media_as_dict(): base = models.BrowseMediaSource( domain=const.DOMAIN, identifier="media", + media_class=MEDIA_CLASS_DIRECTORY, media_content_type="folder", title="media/", can_play=False, @@ -17,6 +22,7 @@ async def test_browse_media_as_dict(): models.BrowseMediaSource( domain=const.DOMAIN, identifier="media/test.mp3", + media_class=MEDIA_CLASS_MUSIC, media_content_type=MEDIA_TYPE_MUSIC, title="test.mp3", can_play=True, @@ -26,12 +32,14 @@ async def test_browse_media_as_dict(): item = base.as_dict() assert item["title"] == "media/" + assert item["media_class"] == MEDIA_CLASS_DIRECTORY assert item["media_content_type"] == "folder" assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" assert not item["can_play"] assert item["can_expand"] assert len(item["children"]) == 1 assert item["children"][0]["title"] == "test.mp3" + assert item["children"][0]["media_class"] == MEDIA_CLASS_MUSIC async def test_browse_media_parent_no_children(): @@ -39,6 +47,7 @@ async def test_browse_media_parent_no_children(): base = models.BrowseMediaSource( domain=const.DOMAIN, identifier="media", + media_class=MEDIA_CLASS_DIRECTORY, media_content_type="folder", title="media/", can_play=False, @@ -47,6 +56,7 @@ async def test_browse_media_parent_no_children(): item = base.as_dict() assert item["title"] == "media/" + assert item["media_class"] == MEDIA_CLASS_DIRECTORY assert item["media_content_type"] == "folder" assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" assert not item["can_play"]