Add children media class ()

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
pull/39974/head
Martin Hjelmare 2020-09-11 13:08:13 +02:00 committed by GitHub
parent e208aac834
commit e96fed20c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 181 additions and 67 deletions
homeassistant/components
tests/components

View File

@ -29,8 +29,15 @@ PLAYABLE_MEDIA_TYPES = [
MEDIA_TYPE_TRACK,
]
CONTENT_TYPE_MEDIA_CLASS = {
"library_music": MEDIA_CLASS_MUSIC,
CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = {
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW,
}
CHILD_TYPE_MEDIA_CLASS = {
MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
@ -151,8 +158,10 @@ async def build_item_response(media_library, payload):
except UnknownMediaType:
pass
return BrowseMedia(
media_class=CONTENT_TYPE_MEDIA_CLASS[search_type],
response = BrowseMedia(
media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get(
search_type, MEDIA_CLASS_DIRECTORY
),
media_content_id=search_id,
media_content_type=search_type,
title=title,
@ -162,6 +171,13 @@ async def build_item_response(media_library, payload):
thumbnail=thumbnail,
)
if search_type == "library_music":
response.children_media_class = MEDIA_CLASS_MUSIC
else:
response.calculate_children_class()
return response
def item_payload(item, media_library):
"""
@ -170,11 +186,12 @@ def item_payload(item, media_library):
Used by async_browse_media.
"""
title = item["label"]
thumbnail = item.get("thumbnail")
if thumbnail:
thumbnail = media_library.thumbnail_url(thumbnail)
media_class = None
if "songid" in item:
media_content_type = MEDIA_TYPE_TRACK
media_content_id = f"{item['songid']}"
@ -213,16 +230,18 @@ def item_payload(item, media_library):
else:
# this case is for the top folder of each type
# possible content types: album, artist, movie, library_music, tvshow
media_class = MEDIA_CLASS_DIRECTORY
media_content_type = item["type"]
media_content_id = ""
can_play = False
can_expand = True
try:
media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type]
except KeyError as err:
_LOGGER.debug("Unknown media type received: %s", media_content_type)
raise UnknownMediaType from err
if media_class is None:
try:
media_class = CHILD_TYPE_MEDIA_CLASS[media_content_type]
except KeyError as err:
_LOGGER.debug("Unknown media type received: %s", media_content_type)
raise UnknownMediaType from err
return BrowseMedia(
title=title,

View File

@ -85,6 +85,7 @@ from .const import (
ATTR_SOUND_MODE,
ATTR_SOUND_MODE_LIST,
DOMAIN,
MEDIA_CLASS_DIRECTORY,
SERVICE_CLEAR_PLAYLIST,
SERVICE_PLAY_MEDIA,
SERVICE_SELECT_SOUND_MODE,
@ -816,24 +817,10 @@ class MediaPlayerEntity(Entity):
media_content_type: Optional[str] = None,
media_content_id: Optional[str] = None,
) -> "BrowseMedia":
"""
Return a payload for the "media_player/browse_media" websocket command.
"""Return a BrowseMedia instance.
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
- Can be used as-is with media_player.play_media service
"can_play": bool - If item is playable
"can_expand": bool - If item contains other media
"thumbnail": str (Optional) - URL to image thumbnail for item
"children": list (Optional) - [{<item_with_keys_above>}, ...]
}
Note: Children should omit the children key.
The BrowseMedia instance will be used by the
"media_player/browse_media" websocket command.
"""
raise NotImplementedError()
@ -1054,6 +1041,7 @@ class BrowseMedia:
can_play: bool,
can_expand: bool,
children: Optional[List["BrowseMedia"]] = None,
children_media_class: Optional[str] = None,
thumbnail: Optional[str] = None,
):
"""Initialize browse media item."""
@ -1064,10 +1052,14 @@ class BrowseMedia:
self.can_play = can_play
self.can_expand = can_expand
self.children = children
self.children_media_class = children_media_class
self.thumbnail = thumbnail
def as_dict(self, *, parent: bool = True) -> dict:
"""Convert Media class to browse media dictionary."""
if self.children_media_class is None:
self.calculate_children_class()
response = {
"title": self.title,
"media_class": self.media_class,
@ -1075,6 +1067,7 @@ class BrowseMedia:
"media_content_id": self.media_content_id,
"can_play": self.can_play,
"can_expand": self.can_expand,
"children_media_class": self.children_media_class,
"thumbnail": self.thumbnail,
}
@ -1089,3 +1082,14 @@ class BrowseMedia:
response["children"] = []
return response
def calculate_children_class(self) -> None:
"""Count the children media classes and calculate the correct class."""
if self.children is None or len(self.children) == 0:
return
self.children_media_class = MEDIA_CLASS_DIRECTORY
proposed_class = self.children[0].media_class
if all(child.media_class == proposed_class for child in self.children):
self.children_media_class = proposed_class

View File

@ -6,6 +6,7 @@ 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_DIRECTORY,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
)
@ -53,11 +54,12 @@ class MediaSourceItem:
base = BrowseMediaSource(
domain=None,
identifier=None,
media_class=MEDIA_CLASS_CHANNEL,
media_class=MEDIA_CLASS_DIRECTORY,
media_content_type=MEDIA_TYPE_CHANNELS,
title="Media Sources",
can_play=False,
can_expand=True,
children_media_class=MEDIA_CLASS_CHANNEL,
)
base.children = [
BrowseMediaSource(

View File

@ -5,6 +5,7 @@ import re
from typing import Optional, Tuple
from homeassistant.components.media_player.const import (
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_VIDEO,
MEDIA_TYPE_VIDEO,
)
@ -91,10 +92,12 @@ class NetatmoSource(MediaSource):
else:
path = f"{source}/{camera_id}"
media_class = MEDIA_CLASS_DIRECTORY if event_id is None else MEDIA_CLASS_VIDEO
media = BrowseMediaSource(
domain=DOMAIN,
identifier=path,
media_class=MEDIA_CLASS_VIDEO,
media_class=media_class,
media_content_type=MEDIA_TYPE_VIDEO,
title=title,
can_play=bool(

View File

@ -12,6 +12,7 @@ from homeassistant.components.media_player import (
)
from homeassistant.components.media_player.const import (
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
SUPPORT_BROWSE_MEDIA,
@ -289,7 +290,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
return BrowseMedia(
title="Channels",
media_class=MEDIA_CLASS_CHANNEL,
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="",
media_content_type=MEDIA_TYPE_CHANNELS,
can_play=False,

View File

@ -26,7 +26,7 @@ class UnknownMediaType(BrowseError):
EXPANDABLES = ["album", "artist", "playlist", "season", "show"]
PLAYLISTS_BROWSE_PAYLOAD = {
"title": "Playlists",
"media_class": MEDIA_CLASS_PLAYLIST,
"media_class": MEDIA_CLASS_DIRECTORY,
"media_content_id": "all",
"media_content_type": "playlists",
"can_play": False,
@ -94,10 +94,21 @@ def browse_media(
if special_folder:
if media_content_type == "server":
library_or_section = plex_server.library
children_media_class = MEDIA_CLASS_DIRECTORY
title = plex_server.friendly_name
elif media_content_type == "library":
library_or_section = plex_server.library.sectionByID(media_content_id)
title = library_or_section.title
try:
children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE]
except KeyError as err:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
) from err
else:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
)
payload = {
"title": title,
@ -107,6 +118,7 @@ def browse_media(
"can_play": False,
"can_expand": True,
"children": [],
"children_media_class": children_media_class,
}
method = SPECIAL_METHODS[special_folder]
@ -116,13 +128,20 @@ def browse_media(
payload["children"].append(item_payload(item))
except UnknownMediaType:
continue
return BrowseMedia(**payload)
if media_content_type in ["server", None]:
return server_payload(plex_server)
try:
if media_content_type in ["server", None]:
return server_payload(plex_server)
if media_content_type == "library":
return library_payload(plex_server, media_content_id)
if media_content_type == "library":
return library_payload(plex_server, media_content_id)
except UnknownMediaType as err:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
) from err
if media_content_type == "playlists":
return playlists_payload(plex_server)
@ -160,6 +179,11 @@ def item_payload(item):
def library_section_payload(section):
"""Create response payload for a single library section."""
try:
children_media_class = ITEM_TYPE_MEDIA_CLASS[section.TYPE]
except KeyError as err:
_LOGGER.debug("Unknown type received: %s", section.TYPE)
raise UnknownMediaType from err
return BrowseMedia(
title=section.title,
media_class=MEDIA_CLASS_DIRECTORY,
@ -167,6 +191,7 @@ def library_section_payload(section):
media_content_type="library",
can_play=False,
can_expand=True,
children_media_class=children_media_class,
)
@ -194,6 +219,7 @@ def server_payload(plex_server):
can_expand=True,
)
server_info.children = []
server_info.children_media_class = MEDIA_CLASS_DIRECTORY
server_info.children.append(special_library_payload(server_info, "On Deck"))
server_info.children.append(special_library_payload(server_info, "Recently Added"))
for library in plex_server.library.sections():
@ -229,4 +255,6 @@ def playlists_payload(plex_server):
playlists_info["children"].append(item_payload(playlist))
except UnknownMediaType:
continue
return BrowseMedia(**playlists_info)
response = BrowseMedia(**playlists_info)
response.children_media_class = MEDIA_CLASS_PLAYLIST
return response

View File

@ -13,9 +13,9 @@ from homeassistant.components.media_player.const import (
CONTENT_TYPE_MEDIA_CLASS = {
MEDIA_TYPE_APP: MEDIA_CLASS_APP,
MEDIA_TYPE_APPS: MEDIA_CLASS_APP,
MEDIA_TYPE_APPS: MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL,
MEDIA_TYPE_CHANNELS: MEDIA_CLASS_CHANNEL,
MEDIA_TYPE_CHANNELS: MEDIA_CLASS_DIRECTORY,
}
PLAYABLE_MEDIA_TYPES = [
@ -59,7 +59,7 @@ def build_item_response(coordinator, payload):
return None
return BrowseMedia(
media_class=CONTENT_TYPE_MEDIA_CLASS[search_type],
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id=search_id,
media_content_type=search_type,
title=title,
@ -139,4 +139,16 @@ def library_payload(coordinator):
)
)
if all(
child.media_content_type == MEDIA_TYPE_APPS for child in library_info.children
):
library_info.children_media_class = MEDIA_CLASS_APP
elif all(
child.media_content_type == MEDIA_TYPE_CHANNELS
for child in library_info.children
):
library_info.children_media_class = MEDIA_CLASS_CHANNEL
else:
library_info.children_media_class = MEDIA_CLASS_DIRECTORY
return library_info

View File

@ -222,6 +222,10 @@ ATTR_STATUS_LIGHT = "status_light"
UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None}
class UnknownMediaType(BrowseError):
"""Unknown media type."""
class SonosData:
"""Storage class for platform global data."""
@ -1487,7 +1491,20 @@ 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"]]]
try:
media_class = SONOS_TO_MEDIA_CLASSES[
MEDIA_TYPES_TO_SONOS[payload["search_type"]]
]
except KeyError:
_LOGGER.debug("Unknown media type received %s", payload["search_type"])
return None
children = []
for item in media:
try:
children.append(item_payload(item))
except UnknownMediaType:
pass
return BrowseMedia(
title=title,
@ -1495,7 +1512,7 @@ def build_item_response(media_library, payload):
media_class=media_class,
media_content_id=payload["idstring"],
media_content_type=payload["search_type"],
children=[item_payload(item) for item in media],
children=children,
can_play=can_play(payload["search_type"]),
can_expand=can_expand(payload["search_type"]),
)
@ -1507,12 +1524,18 @@ def item_payload(item):
Used by async_browse_media.
"""
media_type = get_media_type(item)
try:
media_class = SONOS_TO_MEDIA_CLASSES[media_type]
except KeyError as err:
_LOGGER.debug("Unknown media type received %s", media_type)
raise UnknownMediaType from err
return BrowseMedia(
title=item.title,
thumbnail=getattr(item, "album_art_uri", None),
media_class=SONOS_TO_MEDIA_CLASSES[get_media_type(item)],
media_class=media_class,
media_content_id=get_content_id(item),
media_content_type=SONOS_TO_MEDIA_TYPES[get_media_type(item)],
media_content_type=SONOS_TO_MEDIA_TYPES[media_type],
can_play=can_play(item.item_class),
can_expand=can_expand(item),
)
@ -1524,6 +1547,13 @@ def library_payload(media_library):
Used by async_browse_media.
"""
children = []
for item in media_library.browse():
try:
children.append(item_payload(item))
except UnknownMediaType:
pass
return BrowseMedia(
title="Music Library",
media_class=MEDIA_CLASS_DIRECTORY,
@ -1531,7 +1561,7 @@ def library_payload(media_library):
media_content_type="library",
can_play=False,
can_expand=True,
children=[item_payload(item) for item in media_library.browse()],
children=children,
)

View File

@ -105,18 +105,18 @@ LIBRARY_MAP = {
}
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_GENRE,
"category_playlists": MEDIA_CLASS_PLAYLIST,
"new_releases": MEDIA_CLASS_ALBUM,
"current_user_playlists": MEDIA_CLASS_DIRECTORY,
"current_user_followed_artists": MEDIA_CLASS_DIRECTORY,
"current_user_saved_albums": MEDIA_CLASS_DIRECTORY,
"current_user_saved_tracks": MEDIA_CLASS_DIRECTORY,
"current_user_saved_shows": MEDIA_CLASS_DIRECTORY,
"current_user_recently_played": MEDIA_CLASS_DIRECTORY,
"current_user_top_artists": MEDIA_CLASS_DIRECTORY,
"current_user_top_tracks": MEDIA_CLASS_DIRECTORY,
"featured_playlists": MEDIA_CLASS_DIRECTORY,
"categories": MEDIA_CLASS_DIRECTORY,
"category_playlists": MEDIA_CLASS_DIRECTORY,
"new_releases": MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
@ -567,6 +567,7 @@ def build_item_response(spotify, user, payload):
can_expand=True,
)
)
media_item.children_media_class = MEDIA_CLASS_GENRE
return media_item
if title is None:
@ -575,7 +576,7 @@ def build_item_response(spotify, user, payload):
else:
title = LIBRARY_MAP.get(payload["media_content_id"])
response = {
params = {
"title": title,
"media_class": media_class,
"media_content_id": media_content_id,
@ -586,16 +587,16 @@ def build_item_response(spotify, user, payload):
}
for item in items:
try:
response["children"].append(item_payload(item))
params["children"].append(item_payload(item))
except (MissingMediaInformation, UnknownMediaType):
continue
if "images" in media:
response["thumbnail"] = fetch_image_url(media)
params["thumbnail"] = fetch_image_url(media)
elif image:
response["thumbnail"] = image
params["thumbnail"] = image
return BrowseMedia(**response)
return BrowseMedia(**params)
def item_payload(item):
@ -624,17 +625,13 @@ def item_payload(item):
payload = {
"title": item.get("name"),
"media_class": media_class,
"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": media_class,
}
if "images" in item:
payload["thumbnail"] = fetch_image_url(item)
elif MEDIA_TYPE_ALBUM in item:
@ -665,7 +662,9 @@ def library_payload():
{"name": item["name"], "type": item["type"], "uri": item["type"]}
)
)
return BrowseMedia(**library_info)
response = BrowseMedia(**library_info)
response.children_media_class = MEDIA_CLASS_DIRECTORY
return response
def fetch_image_url(item, key="images"):

View File

@ -17,6 +17,7 @@ async def test_browse_media_as_dict():
title="media/",
can_play=False,
can_expand=True,
children_media_class=MEDIA_CLASS_MUSIC,
)
base.children = [
models.BrowseMediaSource(
@ -37,6 +38,7 @@ async def test_browse_media_as_dict():
assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media"
assert not item["can_play"]
assert item["can_expand"]
assert item["children_media_class"] == MEDIA_CLASS_MUSIC
assert len(item["children"]) == 1
assert item["children"][0]["title"] == "test.mp3"
assert item["children"][0]["media_class"] == MEDIA_CLASS_MUSIC
@ -62,6 +64,7 @@ async def test_browse_media_parent_no_children():
assert not item["can_play"]
assert item["can_expand"]
assert len(item["children"]) == 0
assert item["children_media_class"] is None
async def test_media_source_default_name():

View File

@ -409,6 +409,11 @@ class MockPlexLibrarySection:
if self.title == "Photos":
return "photo"
@property
def TYPE(self):
"""Return the library type."""
return self.type
@property
def key(self):
"""Mock the key identifier property."""

View File

@ -16,6 +16,9 @@ from homeassistant.components.media_player.const import (
ATTR_MEDIA_TITLE,
ATTR_MEDIA_VOLUME_MUTED,
DOMAIN as MP_DOMAIN,
MEDIA_CLASS_APP,
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_APP,
MEDIA_TYPE_APPS,
MEDIA_TYPE_CHANNEL,
@ -499,6 +502,7 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client):
assert msg["result"]
assert msg["result"]["title"] == "Media Library"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == "library"
assert msg["result"]["can_expand"]
assert not msg["result"]["can_play"]
@ -523,10 +527,12 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client):
assert msg["result"]
assert msg["result"]["title"] == "Apps"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == MEDIA_TYPE_APPS
assert msg["result"]["can_expand"]
assert not msg["result"]["can_play"]
assert len(msg["result"]["children"]) == 11
assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP
assert msg["result"]["children"][0]["title"] == "Satellite TV"
assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP
@ -565,10 +571,12 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client):
assert msg["result"]
assert msg["result"]["title"] == "Channels"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == MEDIA_TYPE_CHANNELS
assert msg["result"]["can_expand"]
assert not msg["result"]["can_play"]
assert len(msg["result"]["children"]) == 2
assert msg["result"]["children_media_class"] == MEDIA_CLASS_CHANNEL
assert msg["result"]["children"][0]["title"] == "WhatsOn"
assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_CHANNEL