Add Kodi media browser support (#39729)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
c11b88b4c2
commit
ef8cdf0405
3 changed files with 283 additions and 9 deletions
|
@ -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
|
||||
|
|
216
homeassistant/components/kodi/browse_media.py
Normal file
216
homeassistant/components/kodi/browse_media.py
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue