Browse media class (#39698)

This commit is contained in:
Paulus Schoutsen 2020-09-06 15:52:59 +02:00 committed by GitHub
parent 13a6aaa6ff
commit df8daf561e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 342 additions and 235 deletions

View file

@ -5,7 +5,7 @@ from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceC
from arcam.fmj.state import State from arcam.fmj.state import State
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_MUSIC,
SUPPORT_BROWSE_MEDIA, SUPPORT_BROWSE_MEDIA,
@ -253,22 +253,24 @@ class ArcamFmj(MediaPlayerEntity):
presets = self._state.get_preset_details() presets = self._state.get_preset_details()
radio = [ radio = [
{ BrowseMedia(
"title": preset.name, title=preset.name,
"media_content_id": f"preset:{preset.index}", media_content_id=f"preset:{preset.index}",
"media_content_type": MEDIA_TYPE_MUSIC, media_content_type=MEDIA_TYPE_MUSIC,
"can_play": True, can_play=True,
} can_expand=False,
)
for preset in presets.values() for preset in presets.values()
] ]
root = { root = BrowseMedia(
"title": "Root", title="Root",
"media_content_id": "root", media_content_id="root",
"media_content_type": "library", media_content_type="library",
"can_play": False, can_play=False,
"children": radio, can_expand=True,
} children=radio,
)
return root return root

View file

@ -7,7 +7,7 @@ import functools as ft
import hashlib import hashlib
import logging import logging
from random import SystemRandom from random import SystemRandom
from typing import Optional from typing import List, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
from aiohttp import web from aiohttp import web
@ -811,7 +811,11 @@ class MediaPlayerEntity(Entity):
return state_attr return state_attr
async def async_browse_media(self, media_content_type=None, media_content_id=None): async def async_browse_media(
self,
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 payload for the "media_player/browse_media" websocket command.
@ -976,7 +980,7 @@ async def websocket_browse_media(hass, connection, msg):
To use, media_player integrations can implement MediaPlayerEntity.async_browse_media() To use, media_player integrations can implement MediaPlayerEntity.async_browse_media()
""" """
component = hass.data[DOMAIN] component = hass.data[DOMAIN]
player = component.get_entity(msg["entity_id"]) player: Optional[MediaPlayerDevice] = component.get_entity(msg["entity_id"])
if player is None: if player is None:
connection.send_error(msg["id"], "entity_not_found", "Entity not found") connection.send_error(msg["id"], "entity_not_found", "Entity not found")
@ -1015,6 +1019,12 @@ async def websocket_browse_media(hass, connection, msg):
) )
return return
# For backwards compat
if isinstance(payload, BrowseMedia):
payload = payload.as_dict()
else:
_LOGGER.warning("Browse Media should use new BrowseMedia class")
connection.send_result(msg["id"], payload) connection.send_result(msg["id"], payload)
@ -1028,3 +1038,50 @@ class MediaPlayerDevice(MediaPlayerEntity):
"MediaPlayerDevice is deprecated, modify %s to extend MediaPlayerEntity", "MediaPlayerDevice is deprecated, modify %s to extend MediaPlayerEntity",
cls.__name__, cls.__name__,
) )
class BrowseMedia:
"""Represent a browsable media file."""
def __init__(
self,
*,
media_content_id: str,
media_content_type: str,
title: str,
can_play: bool,
can_expand: bool,
children: Optional[List["BrowseMedia"]] = None,
thumbnail: Optional[str] = None,
):
"""Initialize browse media item."""
self.media_content_id = media_content_id
self.media_content_type = media_content_type
self.title = title
self.can_play = can_play
self.can_expand = can_expand
self.children = children
self.thumbnail = thumbnail
def as_dict(self, *, parent: bool = True) -> dict:
"""Convert Media class to browse media dictionary."""
response = {
"title": self.title,
"media_content_type": self.media_content_type,
"media_content_id": self.media_content_id,
"can_play": self.can_play,
"can_expand": self.can_expand,
"thumbnail": self.thumbnail,
}
if not parent:
return response
if self.children:
response["children"] = [
child.as_dict(parent=False) for child in self.children
]
else:
response["children"] = []
return response

View file

@ -29,27 +29,27 @@ ATTR_SOUND_MODE_LIST = "sound_mode_list"
DOMAIN = "media_player" DOMAIN = "media_player"
MEDIA_TYPE_MUSIC = "music" MEDIA_TYPE_ALBUM = "album"
MEDIA_TYPE_TVSHOW = "tvshow"
MEDIA_TYPE_MOVIE = "movie"
MEDIA_TYPE_VIDEO = "video"
MEDIA_TYPE_EPISODE = "episode"
MEDIA_TYPE_CHANNEL = "channel"
MEDIA_TYPE_CHANNELS = "channels"
MEDIA_TYPE_PLAYLIST = "playlist"
MEDIA_TYPE_IMAGE = "image"
MEDIA_TYPE_URL = "url"
MEDIA_TYPE_GAME = "game"
MEDIA_TYPE_APP = "app" MEDIA_TYPE_APP = "app"
MEDIA_TYPE_APPS = "apps" MEDIA_TYPE_APPS = "apps"
MEDIA_TYPE_ALBUM = "album"
MEDIA_TYPE_TRACK = "track"
MEDIA_TYPE_ARTIST = "artist" MEDIA_TYPE_ARTIST = "artist"
MEDIA_TYPE_CHANNEL = "channel"
MEDIA_TYPE_CHANNELS = "channels"
MEDIA_TYPE_COMPOSER = "composer"
MEDIA_TYPE_CONTRIBUTING_ARTIST = "contributing_artist" MEDIA_TYPE_CONTRIBUTING_ARTIST = "contributing_artist"
MEDIA_TYPE_EPISODE = "episode"
MEDIA_TYPE_GAME = "game"
MEDIA_TYPE_GENRE = "genre"
MEDIA_TYPE_IMAGE = "image"
MEDIA_TYPE_MOVIE = "movie"
MEDIA_TYPE_MUSIC = "music"
MEDIA_TYPE_PLAYLIST = "playlist"
MEDIA_TYPE_PODCAST = "podcast" MEDIA_TYPE_PODCAST = "podcast"
MEDIA_TYPE_SEASON = "season" MEDIA_TYPE_SEASON = "season"
MEDIA_TYPE_GENRE = "genre" MEDIA_TYPE_TRACK = "track"
MEDIA_TYPE_COMPOSER = "composer" MEDIA_TYPE_TVSHOW = "tvshow"
MEDIA_TYPE_URL = "url"
MEDIA_TYPE_VIDEO = "video"
SERVICE_CLEAR_PLAYLIST = "clear_playlist" SERVICE_CLEAR_PLAYLIST = "clear_playlist"
SERVICE_PLAY_MEDIA = "play_media" SERVICE_PLAY_MEDIA = "play_media"

View file

@ -68,7 +68,7 @@ def _get_media_item(
@bind_hass @bind_hass
async def async_browse_media( async def async_browse_media(
hass: HomeAssistant, media_content_id: str hass: HomeAssistant, media_content_id: str
) -> models.BrowseMedia: ) -> models.BrowseMediaSource:
"""Return media player browse media results.""" """Return media player browse media results."""
return await _get_media_item(hass, media_content_id).async_browse() return await _get_media_item(hass, media_content_id).async_browse()
@ -94,7 +94,7 @@ async def websocket_browse_media(hass, connection, msg):
media = await async_browse_media(hass, msg.get("media_content_id")) media = await async_browse_media(hass, msg.get("media_content_id"))
connection.send_result( connection.send_result(
msg["id"], msg["id"],
media.to_media_player_item(), media.as_dict(),
) )
except BrowseError as err: except BrowseError as err:
connection.send_error(msg["id"], "browse_media_failed", str(err)) connection.send_error(msg["id"], "browse_media_failed", str(err))

View file

@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.util import sanitize_path from homeassistant.util import sanitize_path
from .const import DOMAIN, MEDIA_MIME_TYPES from .const import DOMAIN, MEDIA_MIME_TYPES
from .models import BrowseMedia, MediaSource, MediaSourceItem, PlayMedia from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia
@callback @callback
@ -67,7 +67,7 @@ class LocalSource(MediaSource):
async def async_browse_media( async def async_browse_media(
self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES
) -> BrowseMedia: ) -> BrowseMediaSource:
"""Return media.""" """Return media."""
try: try:
source_dir_id, location = async_parse_identifier(item) source_dir_id, location = async_parse_identifier(item)
@ -92,37 +92,41 @@ class LocalSource(MediaSource):
def _build_item_response(self, source_dir_id: str, path: Path, is_child=False): def _build_item_response(self, source_dir_id: str, path: Path, is_child=False):
mime_type, _ = mimetypes.guess_type(str(path)) mime_type, _ = mimetypes.guess_type(str(path))
media = BrowseMedia( is_file = path.is_file()
DOMAIN, is_dir = path.is_dir()
f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}",
path.name,
path.is_file(),
path.is_dir(),
mime_type,
)
# Make sure it's a file or directory # Make sure it's a file or directory
if not media.can_play and not media.can_expand: if not is_file and not is_dir:
return None return None
# Check that it's a media file # Check that it's a media file
if media.can_play and ( if is_file and (
not mime_type or mime_type.split("/")[0] not in MEDIA_MIME_TYPES not mime_type or mime_type.split("/")[0] not in MEDIA_MIME_TYPES
): ):
return None return None
if not media.can_expand: title = path.name
if is_dir:
title += "/"
media = BrowseMediaSource(
domain=DOMAIN,
identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}",
media_content_type="directory",
title=title,
can_play=is_file,
can_expand=is_dir,
)
if is_file or is_child:
return media return media
media.name += "/"
# Append first level children # Append first level children
if not is_child: media.children = []
media.children = [] for child_path in path.iterdir():
for child_path in path.iterdir(): child = self._build_item_response(source_dir_id, child_path, True)
child = self._build_item_response(source_dir_id, child_path, True) if child:
if child: media.children.append(child)
media.children.append(child)
return media return media

View file

@ -3,6 +3,11 @@ from abc import ABC
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import (
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
)
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX
@ -16,49 +21,21 @@ class PlayMedia:
mime_type: str mime_type: str
@dataclass class BrowseMediaSource(BrowseMedia):
class BrowseMedia:
"""Represent a browsable media file.""" """Represent a browsable media file."""
domain: str children: Optional[List["BrowseMediaSource"]]
identifier: str
name: str def __init__(self, *, domain: Optional[str], identifier: Optional[str], **kwargs):
can_play: bool = False """Initialize media source browse media."""
can_expand: bool = False media_content_id = f"{URI_SCHEME}{domain or ''}"
media_content_type: str = None if identifier:
children: List = None media_content_id += f"/{identifier}"
thumbnail: str = None
def to_uri(self): super().__init__(media_content_id=media_content_id, **kwargs)
"""Return URI of media."""
uri = f"{URI_SCHEME}{self.domain or ''}"
if self.identifier:
uri += f"/{self.identifier}"
return uri
def to_media_player_item(self): self.domain = domain
"""Convert Media class to browse media dictionary.""" self.identifier = identifier
content_type = self.media_content_type
if content_type is None:
content_type = "folder" if self.can_expand else "file"
response = {
"title": self.name,
"media_content_type": content_type,
"media_content_id": self.to_uri(),
"can_play": self.can_play,
"can_expand": self.can_expand,
"thumbnail": self.thumbnail,
}
if self.children:
response["children"] = [
child.to_media_player_item() for child in self.children
]
return response
@dataclass @dataclass
@ -69,12 +46,26 @@ class MediaSourceItem:
domain: Optional[str] domain: Optional[str]
identifier: str identifier: str
async def async_browse(self) -> BrowseMedia: async def async_browse(self) -> BrowseMediaSource:
"""Browse this item.""" """Browse this item."""
if self.domain is None: if self.domain is None:
base = BrowseMedia(None, None, "Media Sources", False, True) base = BrowseMediaSource(
domain=None,
identifier=None,
media_content_type=MEDIA_TYPE_CHANNELS,
title="Media Sources",
can_play=False,
can_expand=True,
)
base.children = [ base.children = [
BrowseMedia(source.domain, None, source.name, False, True) BrowseMediaSource(
domain=source.domain,
identifier=None,
media_content_type=MEDIA_TYPE_CHANNEL,
title=source.name,
can_play=False,
can_expand=True,
)
for source in self.hass.data[DOMAIN].values() for source in self.hass.data[DOMAIN].values()
] ]
return base return base
@ -121,6 +112,6 @@ class MediaSource(ABC):
async def async_browse_media( async def async_browse_media(
self, item: MediaSourceItem, media_types: Tuple[str] self, item: MediaSourceItem, media_types: Tuple[str]
) -> BrowseMedia: ) -> BrowseMediaSource:
"""Browse media.""" """Browse media."""
raise NotImplementedError raise NotImplementedError

View file

@ -3,11 +3,12 @@ import datetime as dt
import re import re
from typing import Optional, Tuple from typing import Optional, Tuple
from homeassistant.components.media_player.const import MEDIA_TYPE_VIDEO
from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.media_source.const import MEDIA_MIME_TYPES from homeassistant.components.media_source.const import MEDIA_MIME_TYPES
from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.error import Unresolvable
from homeassistant.components.media_source.models import ( from homeassistant.components.media_source.models import (
BrowseMedia, BrowseMediaSource,
MediaSource, MediaSource,
MediaSourceItem, MediaSourceItem,
PlayMedia, PlayMedia,
@ -43,7 +44,7 @@ class NetatmoSource(MediaSource):
async def async_browse_media( async def async_browse_media(
self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES
) -> Optional[BrowseMedia]: ) -> Optional[BrowseMediaSource]:
"""Return media.""" """Return media."""
try: try:
source, camera_id, event_id = async_parse_identifier(item) source, camera_id, event_id = async_parse_identifier(item)
@ -54,7 +55,7 @@ class NetatmoSource(MediaSource):
def _browse_media( def _browse_media(
self, source: str, camera_id: str, event_id: int self, source: str, camera_id: str, event_id: int
) -> Optional[BrowseMedia]: ) -> Optional[BrowseMediaSource]:
"""Browse media.""" """Browse media."""
if camera_id and camera_id not in self.events: if camera_id and camera_id not in self.events:
raise BrowseError("Camera does not exist.") raise BrowseError("Camera does not exist.")
@ -66,7 +67,7 @@ class NetatmoSource(MediaSource):
def _build_item_response( def _build_item_response(
self, source: str, camera_id: str, event_id: int = None self, source: str, camera_id: str, event_id: int = None
) -> Optional[BrowseMedia]: ) -> Optional[BrowseMediaSource]:
if event_id and event_id in self.events[camera_id]: if event_id and event_id in self.events[camera_id]:
created = dt.datetime.fromtimestamp(event_id) created = dt.datetime.fromtimestamp(event_id)
thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url") thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url")
@ -81,18 +82,18 @@ class NetatmoSource(MediaSource):
else: else:
path = f"{source}/{camera_id}" path = f"{source}/{camera_id}"
media = BrowseMedia( media = BrowseMediaSource(
DOMAIN, domain=DOMAIN,
path, identifier=path,
title, media_content_type=MEDIA_TYPE_VIDEO,
title=title,
can_play=bool(
event_id and self.events[camera_id][event_id].get("media_url")
),
can_expand=event_id is None,
thumbnail=thumbnail,
) )
media.can_play = bool(
event_id and self.events[camera_id][event_id].get("media_url")
)
media.can_expand = event_id is None
media.thumbnail = thumbnail
if not media.can_play and not media.can_expand: if not media.can_play and not media.can_expand:
return None return None

View file

@ -5,9 +5,14 @@ import logging
from haphilipsjs import PhilipsTV from haphilipsjs import PhilipsTV
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player import (
PLATFORM_SCHEMA,
BrowseMedia,
MediaPlayerEntity,
)
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
SUPPORT_BROWSE_MEDIA, SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK, SUPPORT_NEXT_TRACK,
SUPPORT_PLAY_MEDIA, SUPPORT_PLAY_MEDIA,
@ -281,21 +286,23 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
f"Media not found: {media_content_type} / {media_content_id}" f"Media not found: {media_content_type} / {media_content_id}"
) )
return { return BrowseMedia(
"title": "Channels", title="Channels",
"media_content_id": "", media_content_id="",
"media_content_type": "library", media_content_type=MEDIA_TYPE_CHANNELS,
"can_play": False, can_play=False,
"children": [ can_expand=True,
{ children=[
"title": channel, BrowseMedia(
"media_content_id": channel, title=channel,
"media_content_type": MEDIA_TYPE_CHANNEL, media_content_id=channel,
"can_play": True, media_content_type=MEDIA_TYPE_CHANNEL,
} can_play=True,
can_expand=False,
)
for channel in self._channels.values() for channel in self._channels.values()
], ],
} )
def update(self): def update(self):
"""Get the latest data and update device state.""" """Get the latest data and update device state."""

View file

@ -1,6 +1,7 @@
"""Support to interface with the Plex API.""" """Support to interface with the Plex API."""
import logging import logging
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_player.errors import BrowseError
from .const import DOMAIN from .const import DOMAIN
@ -34,10 +35,10 @@ def browse_media(
return None return None
media_info = item_payload(media) media_info = item_payload(media)
if media_info.get("can_expand"): if media_info.can_expand:
media_info["children"] = [] media_info.children = []
for item in media: for item in media:
media_info["children"].append(item_payload(item)) media_info.children.append(item_payload(item))
return media_info return media_info
if media_content_id and ":" in media_content_id: if media_content_id and ":" in media_content_id:
@ -103,12 +104,12 @@ def item_payload(item):
"media_content_id": str(item.ratingKey), "media_content_id": str(item.ratingKey),
"media_content_type": item.type, "media_content_type": item.type,
"can_play": True, "can_play": True,
"can_expand": item.type in EXPANDABLES,
} }
if hasattr(item, "thumbUrl"): if hasattr(item, "thumbUrl"):
payload["thumbnail"] = item.thumbUrl payload["thumbnail"] = item.thumbUrl
if item.type in EXPANDABLES:
payload["can_expand"] = True return BrowseMedia(**payload)
return payload
def library_section_payload(section): def library_section_payload(section):

View file

@ -7,6 +7,7 @@ import voluptuous as vol
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
DEVICE_CLASS_RECEIVER, DEVICE_CLASS_RECEIVER,
DEVICE_CLASS_TV, DEVICE_CLASS_TV,
BrowseMedia,
MediaPlayerEntity, MediaPlayerEntity,
) )
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
@ -74,36 +75,36 @@ async def async_setup_entry(hass, entry, async_add_entities):
) )
def browse_media_library(channels: bool = False) -> dict: def browse_media_library(channels: bool = False) -> BrowseMedia:
"""Create response payload to describe contents of a specific library.""" """Create response payload to describe contents of a specific library."""
library_info = { library_info = BrowseMedia(
"title": "Media Library", title="Media Library",
"media_content_id": "library", media_content_id="library",
"media_content_type": "library", media_content_type="library",
"can_play": False, can_play=False,
"can_expand": True, can_expand=True,
"children": [], children=[],
} )
library_info["children"].append( library_info.children.append(
{ BrowseMedia(
"title": "Apps", title="Apps",
"media_content_id": "apps", media_content_id="apps",
"media_content_type": MEDIA_TYPE_APPS, media_content_type=MEDIA_TYPE_APPS,
"can_expand": True, can_expand=True,
"can_play": False, can_play=False,
} )
) )
if channels: if channels:
library_info["children"].append( library_info.children.append(
{ BrowseMedia(
"title": "Channels", title="Channels",
"media_content_id": "channels", media_content_id="channels",
"media_content_type": MEDIA_TYPE_CHANNELS, media_content_type=MEDIA_TYPE_CHANNELS,
"can_expand": True, can_expand=True,
"can_play": False, can_play=False,
} )
) )
return library_info return library_info
@ -283,41 +284,43 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
response = None response = None
if media_content_type == MEDIA_TYPE_APPS: if media_content_type == MEDIA_TYPE_APPS:
response = { response = BrowseMedia(
"title": "Apps", title="Apps",
"media_content_id": "apps", media_content_id="apps",
"media_content_type": MEDIA_TYPE_APPS, media_content_type=MEDIA_TYPE_APPS,
"can_expand": True, can_expand=True,
"can_play": False, can_play=False,
"children": [ children=[
{ BrowseMedia(
"title": app.name, title=app.name,
"thumbnail": self.coordinator.roku.app_icon_url(app.app_id), thumbnail=self.coordinator.roku.app_icon_url(app.app_id),
"media_content_id": app.app_id, media_content_id=app.app_id,
"media_content_type": MEDIA_TYPE_APP, media_content_type=MEDIA_TYPE_APP,
"can_play": True, can_play=True,
} can_expand=False,
)
for app in self.coordinator.data.apps for app in self.coordinator.data.apps
], ],
} )
if media_content_type == MEDIA_TYPE_CHANNELS: if media_content_type == MEDIA_TYPE_CHANNELS:
response = { response = BrowseMedia(
"title": "Channels", title="Channels",
"media_content_id": "channels", media_content_id="channels",
"media_content_type": MEDIA_TYPE_CHANNELS, media_content_type=MEDIA_TYPE_CHANNELS,
"can_expand": True, can_expand=True,
"can_play": False, can_play=False,
"children": [ children=[
{ BrowseMedia(
"title": channel.name, title=channel.name,
"media_content_id": channel.number, media_content_id=channel.number,
"media_content_type": MEDIA_TYPE_CHANNEL, media_content_type=MEDIA_TYPE_CHANNEL,
"can_play": True, can_play=True,
} can_expand=False,
)
for channel in self.coordinator.data.channels for channel in self.coordinator.data.channels
], ],
} )
if response is None: if response is None:
raise BrowseError( raise BrowseError(

View file

@ -14,7 +14,7 @@ import pysonos.music_library
import pysonos.snapshot import pysonos.snapshot
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_ENQUEUE,
MEDIA_TYPE_ALBUM, MEDIA_TYPE_ALBUM,
@ -1462,15 +1462,15 @@ def build_item_response(media_library, payload):
except IndexError: except IndexError:
title = LIBRARY_TITLES_MAPPING[payload["idstring"]] title = LIBRARY_TITLES_MAPPING[payload["idstring"]]
return { return BrowseMedia(
"title": title, title=title,
"thumbnail": thumbnail, thumbnail=thumbnail,
"media_content_id": payload["idstring"], media_content_id=payload["idstring"],
"media_content_type": payload["search_type"], media_content_type=payload["search_type"],
"children": [item_payload(item) for item in media], children=[item_payload(item) for item in media],
"can_play": can_play(payload["search_type"]), can_play=can_play(payload["search_type"]),
"can_expand": can_expand(payload["search_type"]), can_expand=can_expand(payload["search_type"]),
} )
def item_payload(item): def item_payload(item):

View file

@ -9,7 +9,7 @@ from aiohttp import ClientError
from spotipy import Spotify, SpotifyException from spotipy import Spotify, SpotifyException
from yarl import URL from yarl import URL
from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
MEDIA_TYPE_ALBUM, MEDIA_TYPE_ALBUM,
MEDIA_TYPE_ARTIST, MEDIA_TYPE_ARTIST,
@ -439,28 +439,30 @@ def build_item_response(spotify, payload):
items = media.get("items", []) items = media.get("items", [])
else: else:
media = None media = None
items = []
if media is None: if media is None:
return None return None
if title is None:
if "name" in media:
title = media.get("name")
else:
title = LIBRARY_MAP.get(payload["media_content_id"])
response = { response = {
"title": title,
"media_content_id": payload.get("media_content_id"), "media_content_id": payload.get("media_content_id"),
"media_content_type": payload.get("media_content_type"), "media_content_type": payload.get("media_content_type"),
"can_play": payload.get("media_content_type") in PLAYABLE_MEDIA_TYPES, "can_play": payload.get("media_content_type") in PLAYABLE_MEDIA_TYPES,
"children": [item_payload(item) for item in items], "children": [item_payload(item) for item in items],
"can_expand": True,
} }
if "name" in media:
response["title"] = media.get("name")
elif title:
response["title"] = title
else:
response["title"] = LIBRARY_MAP.get(payload["media_content_id"])
if "images" in media: if "images" in media:
response["thumbnail"] = fetch_image_url(media) response["thumbnail"] = fetch_image_url(media)
return response return BrowseMedia(**response)
def item_payload(item): def item_payload(item):
@ -469,32 +471,31 @@ def item_payload(item):
Used by async_browse_media. Used by async_browse_media.
""" """
can_expand = item.get("type") not in [None, MEDIA_TYPE_TRACK]
if ( if (
MEDIA_TYPE_TRACK in item MEDIA_TYPE_TRACK in item
or item.get("type") != MEDIA_TYPE_ALBUM or item.get("type") != MEDIA_TYPE_ALBUM
and "playlists" in item and "playlists" in item
): ):
track = item.get(MEDIA_TYPE_TRACK) track = item.get(MEDIA_TYPE_TRACK)
payload = { return BrowseMedia(
"title": track.get("name"), title=track.get("name"),
"thumbnail": fetch_image_url(track.get(MEDIA_TYPE_ALBUM, {})), thumbnail=fetch_image_url(track.get(MEDIA_TYPE_ALBUM, {})),
"media_content_id": track.get("uri"), media_content_id=track.get("uri"),
"media_content_type": MEDIA_TYPE_TRACK, media_content_type=MEDIA_TYPE_TRACK,
"can_play": True, can_play=True,
} can_expand=can_expand,
else: )
payload = {
"title": item.get("name"),
"thumbnail": fetch_image_url(item),
"media_content_id": item.get("uri"),
"media_content_type": item.get("type"),
"can_play": item.get("type") in PLAYABLE_MEDIA_TYPES,
}
if item.get("type") not in [None, MEDIA_TYPE_TRACK]: return BrowseMedia(
payload["can_expand"] = True title=item.get("name"),
thumbnail=fetch_image_url(item),
return payload media_content_id=item.get("uri"),
media_content_type=item.get("type"),
can_play=item.get("type") in PLAYABLE_MEDIA_TYPES,
can_expand=can_expand,
)
def library_payload(): def library_payload():

View file

@ -41,8 +41,8 @@ async def test_async_browse_media(hass):
# Test non-media ignored (/media has test.mp3 and not_media.txt) # Test non-media ignored (/media has test.mp3 and not_media.txt)
media = await media_source.async_browse_media(hass, "") media = await media_source.async_browse_media(hass, "")
assert isinstance(media, media_source.models.BrowseMedia) assert isinstance(media, media_source.models.BrowseMediaSource)
assert media.name == "media/" assert media.title == "media/"
assert len(media.children) == 1 assert len(media.children) == 1
# Test invalid media content # Test invalid media content
@ -51,9 +51,9 @@ async def test_async_browse_media(hass):
# Test base URI returns all domains # Test base URI returns all domains
media = await media_source.async_browse_media(hass, const.URI_SCHEME) media = await media_source.async_browse_media(hass, const.URI_SCHEME)
assert isinstance(media, media_source.models.BrowseMedia) assert isinstance(media, media_source.models.BrowseMediaSource)
assert len(media.children) == 1 assert len(media.children) == 1
assert media.children[0].name == "Local Media" assert media.children[0].title == "Local Media"
async def test_async_resolve_media(hass): async def test_async_resolve_media(hass):
@ -73,7 +73,14 @@ async def test_websocket_browse_media(hass, hass_ws_client):
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
media = media_source.models.BrowseMedia(const.DOMAIN, "/media", False, True) media = media_source.models.BrowseMediaSource(
domain=const.DOMAIN,
identifier="/media",
title="Local Media",
media_content_type="listing",
can_play=False,
can_expand=True,
)
with patch( with patch(
"homeassistant.components.media_source.async_browse_media", "homeassistant.components.media_source.async_browse_media",
@ -90,7 +97,7 @@ async def test_websocket_browse_media(hass, hass_ws_client):
assert msg["success"] assert msg["success"]
assert msg["id"] == 1 assert msg["id"] == 1
assert media.to_media_player_item() == msg["result"] assert media.as_dict() == msg["result"]
with patch( with patch(
"homeassistant.components.media_source.async_browse_media", "homeassistant.components.media_source.async_browse_media",

View file

@ -1,17 +1,30 @@
"""Test Media Source model methods.""" """Test Media Source model methods."""
from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC
from homeassistant.components.media_source import const, models from homeassistant.components.media_source import const, models
async def test_browse_media_to_media_player_item(): async def test_browse_media_as_dict():
"""Test BrowseMedia conversion to media player item dict.""" """Test BrowseMediaSource conversion to media player item dict."""
base = models.BrowseMedia(const.DOMAIN, "media", "media/", False, True) base = models.BrowseMediaSource(
domain=const.DOMAIN,
identifier="media",
media_content_type="folder",
title="media/",
can_play=False,
can_expand=True,
)
base.children = [ base.children = [
models.BrowseMedia( models.BrowseMediaSource(
const.DOMAIN, "media/test.mp3", "test.mp3", True, False, "audio/mp3" domain=const.DOMAIN,
identifier="media/test.mp3",
media_content_type=MEDIA_TYPE_MUSIC,
title="test.mp3",
can_play=True,
can_expand=False,
) )
] ]
item = base.to_media_player_item() item = base.as_dict()
assert item["title"] == "media/" assert item["title"] == "media/"
assert item["media_content_type"] == "folder" assert item["media_content_type"] == "folder"
assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media"
@ -21,6 +34,26 @@ async def test_browse_media_to_media_player_item():
assert item["children"][0]["title"] == "test.mp3" assert item["children"][0]["title"] == "test.mp3"
async def test_browse_media_parent_no_children():
"""Test BrowseMediaSource conversion to media player item dict."""
base = models.BrowseMediaSource(
domain=const.DOMAIN,
identifier="media",
media_content_type="folder",
title="media/",
can_play=False,
can_expand=True,
)
item = base.as_dict()
assert item["title"] == "media/"
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"]) == 0
async def test_media_source_default_name(): async def test_media_source_default_name():
"""Test MediaSource uses domain as default name.""" """Test MediaSource uses domain as default name."""
source = models.MediaSource(const.DOMAIN) source = models.MediaSource(const.DOMAIN)