Add Kodi media browser support (#39729)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
cgtobi 2020-09-07 18:22:20 +02:00 committed by GitHub
parent c11b88b4c2
commit ef8cdf0405
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 283 additions and 9 deletions

View file

@ -446,6 +446,7 @@ omit =
homeassistant/components/knx/climate.py
homeassistant/components/knx/cover.py
homeassistant/components/kodi/__init__.py
homeassistant/components/kodi/browse_media.py
homeassistant/components/kodi/const.py
homeassistant/components/kodi/media_player.py
homeassistant/components/kodi/notify.py

View file

@ -0,0 +1,216 @@
"""Support for media browsing."""
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import (
MEDIA_TYPE_ALBUM,
MEDIA_TYPE_ARTIST,
MEDIA_TYPE_EPISODE,
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_PLAYLIST,
MEDIA_TYPE_SEASON,
MEDIA_TYPE_TRACK,
MEDIA_TYPE_TVSHOW,
)
PLAYABLE_MEDIA_TYPES = [
MEDIA_TYPE_ALBUM,
MEDIA_TYPE_ARTIST,
MEDIA_TYPE_TRACK,
]
EXPANDABLE_MEDIA_TYPES = [
MEDIA_TYPE_ALBUM,
MEDIA_TYPE_ARTIST,
MEDIA_TYPE_PLAYLIST,
MEDIA_TYPE_TVSHOW,
MEDIA_TYPE_SEASON,
]
async def build_item_response(media_library, payload):
"""Create response payload for the provided media query."""
search_id = payload["search_id"]
search_type = payload["search_type"]
thumbnail = None
title = None
media = None
query = {"properties": ["thumbnail"]}
# pylint: disable=protected-access
if search_type == MEDIA_TYPE_ALBUM:
if search_id:
query.update({"filter": {"albumid": int(search_id)}})
query["properties"].extend(
["albumid", "artist", "duration", "album", "track"]
)
album = await media_library._server.AudioLibrary.GetAlbumDetails(
{"albumid": int(search_id), "properties": ["thumbnail"]}
)
thumbnail = media_library.thumbnail_url(
album["albumdetails"].get("thumbnail")
)
title = album["albumdetails"]["label"]
media = await media_library._server.AudioLibrary.GetSongs(query)
media = media.get("songs")
else:
media = await media_library._server.AudioLibrary.GetAlbums(query)
media = media.get("albums")
title = "Albums"
elif search_type == MEDIA_TYPE_ARTIST:
if search_id:
query.update({"filter": {"artistid": int(search_id)}})
media = await media_library._server.AudioLibrary.GetAlbums(query)
media = media.get("albums")
artist = await media_library._server.AudioLibrary.GetArtistDetails(
{"artistid": int(search_id), "properties": ["thumbnail"]}
)
thumbnail = media_library.thumbnail_url(
artist["artistdetails"].get("thumbnail")
)
title = artist["artistdetails"]["label"]
else:
media = await media_library._server.AudioLibrary.GetArtists(query)
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._server.VideoLibrary.GetMovies(query)
media = media.get("movies")
title = "Movies"
elif search_type == MEDIA_TYPE_TVSHOW:
if search_id:
media = await media_library._server.VideoLibrary.GetSeasons(
{
"tvshowid": int(search_id),
"properties": ["thumbnail", "season", "tvshowid"],
}
)
media = media.get("seasons")
tvshow = await media_library._server.VideoLibrary.GetTVShowDetails(
{"tvshowid": int(search_id), "properties": ["thumbnail"]}
)
thumbnail = media_library.thumbnail_url(
tvshow["tvshowdetails"].get("thumbnail")
)
title = tvshow["tvshowdetails"]["label"]
else:
media = await media_library._server.VideoLibrary.GetTVShows(query)
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._server.VideoLibrary.GetEpisodes(
{
"tvshowid": int(tv_show_id),
"season": int(season_id),
"properties": ["thumbnail", "tvshowid", "seasonid"],
}
)
media = media.get("episodes")
if media:
season = await media_library._server.VideoLibrary.GetSeasonDetails(
{"seasonid": int(media[0]["seasonid"]), "properties": ["thumbnail"]}
)
thumbnail = media_library.thumbnail_url(
season["seasondetails"].get("thumbnail")
)
title = season["seasondetails"]["label"]
if media is None:
return
return BrowseMedia(
media_content_id=payload["search_id"],
media_content_type=search_type,
title=title,
can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id,
can_expand=True,
children=[item_payload(item, media_library) for item in media],
thumbnail=thumbnail,
)
def item_payload(item, media_library):
"""
Create response payload for a single media item.
Used by async_browse_media.
"""
if "songid" in item:
media_content_type = MEDIA_TYPE_TRACK
media_content_id = f"{item['songid']}"
elif "albumid" in item:
media_content_type = MEDIA_TYPE_ALBUM
media_content_id = f"{item['albumid']}"
elif "artistid" in item:
media_content_type = MEDIA_TYPE_ARTIST
media_content_id = f"{item['artistid']}"
elif "movieid" in item:
media_content_type = MEDIA_TYPE_MOVIE
media_content_id = f"{item['movieid']}"
elif "episodeid" in item:
media_content_type = MEDIA_TYPE_EPISODE
media_content_id = f"{item['episodeid']}"
elif "seasonid" in item:
media_content_type = MEDIA_TYPE_SEASON
media_content_id = f"{item['tvshowid']}/{item['season']}"
elif "tvshowid" in item:
media_content_type = MEDIA_TYPE_TVSHOW
media_content_id = f"{item['tvshowid']}"
else:
# this case is for the top folder of each type
# possible content types: album, artist, movie, library_music, tvshow
media_content_type = item.get("type")
media_content_id = ""
title = item["label"]
can_play = media_content_type in PLAYABLE_MEDIA_TYPES and bool(media_content_id)
can_expand = media_content_type in EXPANDABLE_MEDIA_TYPES
thumbnail = item.get("thumbnail")
if thumbnail:
thumbnail = media_library.thumbnail_url(thumbnail)
return BrowseMedia(
title=title,
media_content_type=media_content_type,
media_content_id=media_content_id,
can_play=can_play,
can_expand=can_expand,
thumbnail=thumbnail,
)
def library_payload(media_library):
"""
Create response payload to describe contents of a specific library.
Used by async_browse_media.
"""
library_info = BrowseMedia(
media_content_id="library",
media_content_type="library",
title="Media Library",
can_play=False,
can_expand=True,
children=[],
)
library = {
"library_music": "Music",
MEDIA_TYPE_MOVIE: "Movies",
MEDIA_TYPE_TVSHOW: "TV shows",
}
for item in [{"label": name, "type": type_} for type_, name in library.items()]:
library_info.children.append(
item_payload(
{"label": item["label"], "type": item["type"], "uri": item["type"]},
media_library,
)
)
return library_info

View file

@ -9,12 +9,18 @@ import voluptuous as vol
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
from homeassistant.components.media_player.const import (
MEDIA_TYPE_ALBUM,
MEDIA_TYPE_ARTIST,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_EPISODE,
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_PLAYLIST,
MEDIA_TYPE_SEASON,
MEDIA_TYPE_TRACK,
MEDIA_TYPE_TVSHOW,
MEDIA_TYPE_VIDEO,
SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
@ -29,6 +35,7 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP,
)
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_ENTITY_ID,
@ -50,6 +57,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.event import async_track_time_interval
import homeassistant.util.dt as dt_util
from .browse_media import build_item_response, library_payload
from .const import (
CONF_WS_PORT,
DATA_CONNECTION,
@ -103,20 +111,28 @@ MEDIA_TYPES = {
"audio": MEDIA_TYPE_MUSIC,
}
MAP_KODI_MEDIA_TYPES = {
MEDIA_TYPE_MOVIE: "movieid",
MEDIA_TYPE_EPISODE: "episodeid",
MEDIA_TYPE_SEASON: "seasonid",
MEDIA_TYPE_TVSHOW: "tvshowid",
}
SUPPORT_KODI = (
SUPPORT_PAUSE
| SUPPORT_VOLUME_SET
| SUPPORT_VOLUME_MUTE
| SUPPORT_PREVIOUS_TRACK
SUPPORT_BROWSE_MEDIA
| SUPPORT_NEXT_TRACK
| SUPPORT_SEEK
| SUPPORT_PLAY_MEDIA
| SUPPORT_STOP
| SUPPORT_SHUFFLE_SET
| SUPPORT_PAUSE
| SUPPORT_PLAY
| SUPPORT_VOLUME_STEP
| SUPPORT_PLAY_MEDIA
| SUPPORT_PREVIOUS_TRACK
| SUPPORT_SEEK
| SUPPORT_SHUFFLE_SET
| SUPPORT_STOP
| SUPPORT_TURN_OFF
| SUPPORT_TURN_ON
| SUPPORT_VOLUME_MUTE
| SUPPORT_VOLUME_SET
| SUPPORT_VOLUME_STEP
)
@ -644,6 +660,31 @@ class KodiEntity(MediaPlayerEntity):
await self._kodi.play_playlist(int(media_id))
elif media_type_lower == "directory":
await self._kodi.play_directory(str(media_id))
elif media_type_lower in [
MEDIA_TYPE_ARTIST,
MEDIA_TYPE_ALBUM,
]:
await self.async_clear_playlist()
params = {"playlistid": 0, "item": {f"{media_type}id": int(media_id)}}
# pylint: disable=protected-access
await self._kodi._server.Playlist.Add(params)
await self._kodi.play_playlist(0)
elif media_type_lower == MEDIA_TYPE_TRACK:
await self._kodi.clear_playlist()
params = {"playlistid": 0, "item": {"songid": int(media_id)}}
# pylint: disable=protected-access
await self._kodi._server.Playlist.Add(params)
await self._kodi.play_playlist(0)
elif media_type_lower in [
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_EPISODE,
MEDIA_TYPE_SEASON,
MEDIA_TYPE_TVSHOW,
]:
# pylint: disable=protected-access
await self._kodi._play_item(
{MAP_KODI_MEDIA_TYPES[media_type_lower]: int(media_id)}
)
else:
await self._kodi.play_file(str(media_id))
@ -794,3 +835,19 @@ class KodiEntity(MediaPlayerEntity):
out[i][1] = rate
return sorted(out, key=lambda out: out[1], reverse=True)
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
if media_content_type in [None, "library"]:
return await self.hass.async_add_executor_job(library_payload, self._kodi)
payload = {
"search_type": media_content_type,
"search_id": media_content_id,
}
response = await build_item_response(self._kodi, payload)
if response is None:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
)
return response