diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 9c06b8124b9..a7e87a6ae27 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -1,5 +1,5 @@ """Support for media browsing.""" -from contextlib import suppress +import asyncio import logging from homeassistant.components.media_player import BrowseError, BrowseMedia @@ -59,121 +59,20 @@ class UnknownMediaType(BrowseError): """Unknown media type.""" -async def build_item_response(media_library, payload): +async def build_item_response(media_library, payload, get_thumbnail_url=None): """Create response payload for the provided media query.""" search_id = payload["search_id"] search_type = payload["search_type"] - thumbnail = None - title = None - media = None - - properties = ["thumbnail"] - if search_type == MEDIA_TYPE_ALBUM: - if search_id: - album = await media_library.get_album_details( - album_id=int(search_id), properties=properties - ) - thumbnail = media_library.thumbnail_url( - album["albumdetails"].get("thumbnail") - ) - title = album["albumdetails"]["label"] - media = await media_library.get_songs( - album_id=int(search_id), - properties=[ - "albumid", - "artist", - "duration", - "album", - "thumbnail", - "track", - ], - ) - media = media.get("songs") - else: - media = await media_library.get_albums(properties=properties) - media = media.get("albums") - title = "Albums" - - elif search_type == MEDIA_TYPE_ARTIST: - if search_id: - media = await media_library.get_albums( - artist_id=int(search_id), properties=properties - ) - media = media.get("albums") - artist = await media_library.get_artist_details( - artist_id=int(search_id), properties=properties - ) - thumbnail = media_library.thumbnail_url( - artist["artistdetails"].get("thumbnail") - ) - title = artist["artistdetails"]["label"] - else: - media = await media_library.get_artists(properties) - media = media.get("artists") - title = "Artists" - - elif search_type == "library_music": - library = {MEDIA_TYPE_ALBUM: "Albums", MEDIA_TYPE_ARTIST: "Artists"} - media = [{"label": name, "type": type_} for type_, name in library.items()] - title = "Music Library" - - elif search_type == MEDIA_TYPE_MOVIE: - media = await media_library.get_movies(properties) - media = media.get("movies") - title = "Movies" - - elif search_type == MEDIA_TYPE_TVSHOW: - if search_id: - media = await media_library.get_seasons( - tv_show_id=int(search_id), - properties=["thumbnail", "season", "tvshowid"], - ) - media = media.get("seasons") - tvshow = await media_library.get_tv_show_details( - tv_show_id=int(search_id), properties=properties - ) - thumbnail = media_library.thumbnail_url( - tvshow["tvshowdetails"].get("thumbnail") - ) - title = tvshow["tvshowdetails"]["label"] - else: - media = await media_library.get_tv_shows(properties) - media = media.get("tvshows") - title = "TV Shows" - - elif search_type == MEDIA_TYPE_SEASON: - tv_show_id, season_id = search_id.split("/", 1) - media = await media_library.get_episodes( - tv_show_id=int(tv_show_id), - season_id=int(season_id), - properties=["thumbnail", "tvshowid", "seasonid"], - ) - media = media.get("episodes") - if media: - season = await media_library.get_season_details( - season_id=int(media[0]["seasonid"]), properties=properties - ) - thumbnail = media_library.thumbnail_url( - season["seasondetails"].get("thumbnail") - ) - title = season["seasondetails"]["label"] - - elif search_type == MEDIA_TYPE_CHANNEL: - media = await media_library.get_channels( - channel_group_id="alltv", - properties=["thumbnail", "channeltype", "channel", "broadcastnow"], - ) - media = media.get("channels") - title = "Channels" + _, title, media = await get_media_info(media_library, search_id, search_type) + thumbnail = await get_thumbnail_url(search_type, search_id) if media is None: return None - children = [] - for item in media: - with suppress(UnknownMediaType): - children.append(item_payload(item, media_library)) + children = await asyncio.gather( + *[item_payload(item, get_thumbnail_url) for item in media] + ) if search_type in (MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE) and search_id == "": children.sort(key=lambda x: x.title.replace("The ", "", 1), reverse=False) @@ -199,16 +98,13 @@ async def build_item_response(media_library, payload): return response -def item_payload(item, media_library): +async def item_payload(item, get_thumbnail_url=None): """ Create response payload for a single media item. Used by async_browse_media. """ title = item["label"] - thumbnail = item.get("thumbnail") - if thumbnail: - thumbnail = media_library.thumbnail_url(thumbnail) media_class = None @@ -272,6 +168,12 @@ def item_payload(item, media_library): _LOGGER.debug("Unknown media type received: %s", media_content_type) raise UnknownMediaType from err + thumbnail = item.get("thumbnail") + if thumbnail is not None and get_thumbnail_url is not None: + thumbnail = await get_thumbnail_url( + media_content_type, media_content_id, thumbnail_url=thumbnail + ) + return BrowseMedia( title=title, media_class=media_class, @@ -283,7 +185,7 @@ def item_payload(item, media_library): ) -def library_payload(media_library): +async def library_payload(): """ Create response payload to describe contents of a specific library. @@ -305,12 +207,137 @@ def library_payload(media_library): MEDIA_TYPE_TVSHOW: "TV shows", MEDIA_TYPE_CHANNEL: "Channels", } - for item in [{"label": name, "type": type_} for type_, name in library.items()]: - library_info.children.append( + + library_info.children = await asyncio.gather( + *[ item_payload( - {"label": item["label"], "type": item["type"], "uri": item["type"]}, - media_library, + { + "label": item["label"], + "type": item["type"], + "uri": item["type"], + }, ) - ) + for item in [ + {"label": name, "type": type_} for type_, name in library.items() + ] + ] + ) return library_info + + +async def get_media_info(media_library, search_id, search_type): + """Fetch media/album.""" + thumbnail = None + title = None + media = None + + properties = ["thumbnail"] + if search_type == MEDIA_TYPE_ALBUM: + if search_id: + album = await media_library.get_album_details( + album_id=int(search_id), properties=properties + ) + thumbnail = media_library.thumbnail_url( + album["albumdetails"].get("thumbnail") + ) + title = album["albumdetails"]["label"] + media = await media_library.get_songs( + album_id=int(search_id), + properties=[ + "albumid", + "artist", + "duration", + "album", + "thumbnail", + "track", + ], + ) + media = media.get("songs") + else: + media = await media_library.get_albums(properties=properties) + media = media.get("albums") + title = "Albums" + + elif search_type == MEDIA_TYPE_ARTIST: + if search_id: + media = await media_library.get_albums( + artist_id=int(search_id), properties=properties + ) + media = media.get("albums") + artist = await media_library.get_artist_details( + artist_id=int(search_id), properties=properties + ) + thumbnail = media_library.thumbnail_url( + artist["artistdetails"].get("thumbnail") + ) + title = artist["artistdetails"]["label"] + else: + media = await media_library.get_artists(properties) + media = media.get("artists") + title = "Artists" + + elif search_type == "library_music": + library = {MEDIA_TYPE_ALBUM: "Albums", MEDIA_TYPE_ARTIST: "Artists"} + media = [{"label": name, "type": type_} for type_, name in library.items()] + title = "Music Library" + + elif search_type == MEDIA_TYPE_MOVIE: + if search_id: + movie = await media_library.get_movie_details( + movie_id=int(search_id), properties=properties + ) + thumbnail = media_library.thumbnail_url( + movie["moviedetails"].get("thumbnail") + ) + title = movie["moviedetails"]["label"] + else: + media = await media_library.get_movies(properties) + media = media.get("movies") + title = "Movies" + + elif search_type == MEDIA_TYPE_TVSHOW: + if search_id: + media = await media_library.get_seasons( + tv_show_id=int(search_id), + properties=["thumbnail", "season", "tvshowid"], + ) + media = media.get("seasons") + tvshow = await media_library.get_tv_show_details( + tv_show_id=int(search_id), properties=properties + ) + thumbnail = media_library.thumbnail_url( + tvshow["tvshowdetails"].get("thumbnail") + ) + title = tvshow["tvshowdetails"]["label"] + else: + media = await media_library.get_tv_shows(properties) + media = media.get("tvshows") + title = "TV Shows" + + elif search_type == MEDIA_TYPE_SEASON: + tv_show_id, season_id = search_id.split("/", 1) + media = await media_library.get_episodes( + tv_show_id=int(tv_show_id), + season_id=int(season_id), + properties=["thumbnail", "tvshowid", "seasonid"], + ) + media = media.get("episodes") + if media: + season = await media_library.get_season_details( + season_id=int(media[0]["seasonid"]), properties=properties + ) + thumbnail = media_library.thumbnail_url( + season["seasondetails"].get("thumbnail") + ) + title = season["seasondetails"]["label"] + + elif search_type == MEDIA_TYPE_CHANNEL: + media = await media_library.get_channels( + channel_group_id="alltv", + properties=["thumbnail", "channeltype", "channel", "broadcastnow"], + ) + media = media.get("channels") + title = "Channels" + + return thumbnail, title, media diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 24d3393d7c3..63282ed1a9a 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -3,7 +3,7 @@ "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", "requirements": [ - "pykodi==0.2.1" + "pykodi==0.2.3" ], "codeowners": [ "@OnFreund", diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 6b849cb711b..72197f6b8e2 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -3,8 +3,10 @@ from datetime import timedelta from functools import wraps import logging import re +import urllib.parse import jsonrpc_base +from jsonrpc_base.jsonrpc import ProtocolError, TransportError from pykodi import CannotConnectError import voluptuous as vol @@ -61,9 +63,10 @@ from homeassistant.helpers import ( entity_platform, ) from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.network import is_internal_request import homeassistant.util.dt as dt_util -from .browse_media import build_item_response, library_payload +from .browse_media import build_item_response, get_media_info, library_payload from .const import ( CONF_WS_PORT, DATA_CONNECTION, @@ -871,16 +874,50 @@ class KodiEntity(MediaPlayerEntity): async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" + is_internal = is_internal_request(self.hass) + + async def _get_thumbnail_url( + media_content_type, + media_content_id, + media_image_id=None, + thumbnail_url=None, + ): + if is_internal: + return self._kodi.thumbnail_url(thumbnail_url) + + return self.get_browse_image_url( + media_content_type, + urllib.parse.quote_plus(media_content_id), + media_image_id, + ) + if media_content_type in [None, "library"]: - return await self.hass.async_add_executor_job(library_payload, self._kodi) + return await library_payload() payload = { "search_type": media_content_type, "search_id": media_content_id, } - response = await build_item_response(self._kodi, payload) + + response = await build_item_response(self._kodi, payload, _get_thumbnail_url) if response is None: raise BrowseError( f"Media not found: {media_content_type} / {media_content_id}" ) return response + + async def async_get_browse_image( + self, media_content_type, media_content_id, media_image_id=None + ): + """Get media image from kodi server.""" + try: + image_url, _, _ = await get_media_info( + self._kodi, media_content_id, media_content_type + ) + except (ProtocolError, TransportError): + return (None, None) + + if image_url: + return await self._async_fetch_image(image_url) + + return (None, None) diff --git a/requirements_all.txt b/requirements_all.txt index 9f5c0667f93..aa05c77f9a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1476,7 +1476,7 @@ pykira==0.1.1 pykmtronic==0.0.3 # homeassistant.components.kodi -pykodi==0.2.1 +pykodi==0.2.3 # homeassistant.components.kulersky pykulersky==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f17b09d5029..b45b2528ac9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -784,7 +784,7 @@ pykira==0.1.1 pykmtronic==0.0.3 # homeassistant.components.kodi -pykodi==0.2.1 +pykodi==0.2.3 # homeassistant.components.kulersky pykulersky==0.5.2