Browse media class (#39698)
parent
13a6aaa6ff
commit
df8daf561e
|
@ -5,7 +5,7 @@ from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceC
|
|||
from arcam.fmj.state import State
|
||||
|
||||
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 (
|
||||
MEDIA_TYPE_MUSIC,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
|
@ -253,22 +253,24 @@ class ArcamFmj(MediaPlayerEntity):
|
|||
presets = self._state.get_preset_details()
|
||||
|
||||
radio = [
|
||||
{
|
||||
"title": preset.name,
|
||||
"media_content_id": f"preset:{preset.index}",
|
||||
"media_content_type": MEDIA_TYPE_MUSIC,
|
||||
"can_play": True,
|
||||
}
|
||||
BrowseMedia(
|
||||
title=preset.name,
|
||||
media_content_id=f"preset:{preset.index}",
|
||||
media_content_type=MEDIA_TYPE_MUSIC,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
)
|
||||
for preset in presets.values()
|
||||
]
|
||||
|
||||
root = {
|
||||
"title": "Root",
|
||||
"media_content_id": "root",
|
||||
"media_content_type": "library",
|
||||
"can_play": False,
|
||||
"children": radio,
|
||||
}
|
||||
root = BrowseMedia(
|
||||
title="Root",
|
||||
media_content_id="root",
|
||||
media_content_type="library",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=radio,
|
||||
)
|
||||
|
||||
return root
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import functools as ft
|
|||
import hashlib
|
||||
import logging
|
||||
from random import SystemRandom
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from aiohttp import web
|
||||
|
@ -811,7 +811,11 @@ class MediaPlayerEntity(Entity):
|
|||
|
||||
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.
|
||||
|
||||
|
@ -976,7 +980,7 @@ async def websocket_browse_media(hass, connection, msg):
|
|||
To use, media_player integrations can implement MediaPlayerEntity.async_browse_media()
|
||||
"""
|
||||
component = hass.data[DOMAIN]
|
||||
player = component.get_entity(msg["entity_id"])
|
||||
player: Optional[MediaPlayerDevice] = component.get_entity(msg["entity_id"])
|
||||
|
||||
if player is None:
|
||||
connection.send_error(msg["id"], "entity_not_found", "Entity not found")
|
||||
|
@ -1015,6 +1019,12 @@ async def websocket_browse_media(hass, connection, msg):
|
|||
)
|
||||
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)
|
||||
|
||||
|
||||
|
@ -1028,3 +1038,50 @@ class MediaPlayerDevice(MediaPlayerEntity):
|
|||
"MediaPlayerDevice is deprecated, modify %s to extend MediaPlayerEntity",
|
||||
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
|
||||
|
|
|
@ -29,27 +29,27 @@ ATTR_SOUND_MODE_LIST = "sound_mode_list"
|
|||
|
||||
DOMAIN = "media_player"
|
||||
|
||||
MEDIA_TYPE_MUSIC = "music"
|
||||
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_ALBUM = "album"
|
||||
MEDIA_TYPE_APP = "app"
|
||||
MEDIA_TYPE_APPS = "apps"
|
||||
MEDIA_TYPE_ALBUM = "album"
|
||||
MEDIA_TYPE_TRACK = "track"
|
||||
MEDIA_TYPE_ARTIST = "artist"
|
||||
MEDIA_TYPE_CHANNEL = "channel"
|
||||
MEDIA_TYPE_CHANNELS = "channels"
|
||||
MEDIA_TYPE_COMPOSER = "composer"
|
||||
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_SEASON = "season"
|
||||
MEDIA_TYPE_GENRE = "genre"
|
||||
MEDIA_TYPE_COMPOSER = "composer"
|
||||
MEDIA_TYPE_TRACK = "track"
|
||||
MEDIA_TYPE_TVSHOW = "tvshow"
|
||||
MEDIA_TYPE_URL = "url"
|
||||
MEDIA_TYPE_VIDEO = "video"
|
||||
|
||||
SERVICE_CLEAR_PLAYLIST = "clear_playlist"
|
||||
SERVICE_PLAY_MEDIA = "play_media"
|
||||
|
|
|
@ -68,7 +68,7 @@ def _get_media_item(
|
|||
@bind_hass
|
||||
async def async_browse_media(
|
||||
hass: HomeAssistant, media_content_id: str
|
||||
) -> models.BrowseMedia:
|
||||
) -> models.BrowseMediaSource:
|
||||
"""Return media player browse media results."""
|
||||
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"))
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
media.to_media_player_item(),
|
||||
media.as_dict(),
|
||||
)
|
||||
except BrowseError as err:
|
||||
connection.send_error(msg["id"], "browse_media_failed", str(err))
|
||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||
from homeassistant.util import sanitize_path
|
||||
|
||||
from .const import DOMAIN, MEDIA_MIME_TYPES
|
||||
from .models import BrowseMedia, MediaSource, MediaSourceItem, PlayMedia
|
||||
from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia
|
||||
|
||||
|
||||
@callback
|
||||
|
@ -67,7 +67,7 @@ class LocalSource(MediaSource):
|
|||
|
||||
async def async_browse_media(
|
||||
self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES
|
||||
) -> BrowseMedia:
|
||||
) -> BrowseMediaSource:
|
||||
"""Return media."""
|
||||
try:
|
||||
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):
|
||||
mime_type, _ = mimetypes.guess_type(str(path))
|
||||
media = BrowseMedia(
|
||||
DOMAIN,
|
||||
f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}",
|
||||
path.name,
|
||||
path.is_file(),
|
||||
path.is_dir(),
|
||||
mime_type,
|
||||
)
|
||||
is_file = path.is_file()
|
||||
is_dir = path.is_dir()
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
):
|
||||
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
|
||||
|
||||
media.name += "/"
|
||||
|
||||
# Append first level children
|
||||
if not is_child:
|
||||
media.children = []
|
||||
for child_path in path.iterdir():
|
||||
child = self._build_item_response(source_dir_id, child_path, True)
|
||||
if child:
|
||||
media.children.append(child)
|
||||
media.children = []
|
||||
for child_path in path.iterdir():
|
||||
child = self._build_item_response(source_dir_id, child_path, True)
|
||||
if child:
|
||||
media.children.append(child)
|
||||
|
||||
return media
|
||||
|
||||
|
|
|
@ -3,6 +3,11 @@ from abc import ABC
|
|||
from dataclasses import dataclass
|
||||
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 .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX
|
||||
|
@ -16,49 +21,21 @@ class PlayMedia:
|
|||
mime_type: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class BrowseMedia:
|
||||
class BrowseMediaSource(BrowseMedia):
|
||||
"""Represent a browsable media file."""
|
||||
|
||||
domain: str
|
||||
identifier: str
|
||||
children: Optional[List["BrowseMediaSource"]]
|
||||
|
||||
name: str
|
||||
can_play: bool = False
|
||||
can_expand: bool = False
|
||||
media_content_type: str = None
|
||||
children: List = None
|
||||
thumbnail: str = None
|
||||
def __init__(self, *, domain: Optional[str], identifier: Optional[str], **kwargs):
|
||||
"""Initialize media source browse media."""
|
||||
media_content_id = f"{URI_SCHEME}{domain or ''}"
|
||||
if identifier:
|
||||
media_content_id += f"/{identifier}"
|
||||
|
||||
def to_uri(self):
|
||||
"""Return URI of media."""
|
||||
uri = f"{URI_SCHEME}{self.domain or ''}"
|
||||
if self.identifier:
|
||||
uri += f"/{self.identifier}"
|
||||
return uri
|
||||
super().__init__(media_content_id=media_content_id, **kwargs)
|
||||
|
||||
def to_media_player_item(self):
|
||||
"""Convert Media class to browse media dictionary."""
|
||||
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
|
||||
self.domain = domain
|
||||
self.identifier = identifier
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -69,12 +46,26 @@ class MediaSourceItem:
|
|||
domain: Optional[str]
|
||||
identifier: str
|
||||
|
||||
async def async_browse(self) -> BrowseMedia:
|
||||
async def async_browse(self) -> BrowseMediaSource:
|
||||
"""Browse this item."""
|
||||
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 = [
|
||||
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()
|
||||
]
|
||||
return base
|
||||
|
@ -121,6 +112,6 @@ class MediaSource(ABC):
|
|||
|
||||
async def async_browse_media(
|
||||
self, item: MediaSourceItem, media_types: Tuple[str]
|
||||
) -> BrowseMedia:
|
||||
) -> BrowseMediaSource:
|
||||
"""Browse media."""
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -3,11 +3,12 @@ import datetime as dt
|
|||
import re
|
||||
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_source.const import MEDIA_MIME_TYPES
|
||||
from homeassistant.components.media_source.error import Unresolvable
|
||||
from homeassistant.components.media_source.models import (
|
||||
BrowseMedia,
|
||||
BrowseMediaSource,
|
||||
MediaSource,
|
||||
MediaSourceItem,
|
||||
PlayMedia,
|
||||
|
@ -43,7 +44,7 @@ class NetatmoSource(MediaSource):
|
|||
|
||||
async def async_browse_media(
|
||||
self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES
|
||||
) -> Optional[BrowseMedia]:
|
||||
) -> Optional[BrowseMediaSource]:
|
||||
"""Return media."""
|
||||
try:
|
||||
source, camera_id, event_id = async_parse_identifier(item)
|
||||
|
@ -54,7 +55,7 @@ class NetatmoSource(MediaSource):
|
|||
|
||||
def _browse_media(
|
||||
self, source: str, camera_id: str, event_id: int
|
||||
) -> Optional[BrowseMedia]:
|
||||
) -> Optional[BrowseMediaSource]:
|
||||
"""Browse media."""
|
||||
if camera_id and camera_id not in self.events:
|
||||
raise BrowseError("Camera does not exist.")
|
||||
|
@ -66,7 +67,7 @@ class NetatmoSource(MediaSource):
|
|||
|
||||
def _build_item_response(
|
||||
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]:
|
||||
created = dt.datetime.fromtimestamp(event_id)
|
||||
thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url")
|
||||
|
@ -81,18 +82,18 @@ class NetatmoSource(MediaSource):
|
|||
else:
|
||||
path = f"{source}/{camera_id}"
|
||||
|
||||
media = BrowseMedia(
|
||||
DOMAIN,
|
||||
path,
|
||||
title,
|
||||
media = BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=path,
|
||||
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:
|
||||
return None
|
||||
|
||||
|
|
|
@ -5,9 +5,14 @@ import logging
|
|||
from haphilipsjs import PhilipsTV
|
||||
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 (
|
||||
MEDIA_TYPE_CHANNEL,
|
||||
MEDIA_TYPE_CHANNELS,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PLAY_MEDIA,
|
||||
|
@ -281,21 +286,23 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
|
|||
f"Media not found: {media_content_type} / {media_content_id}"
|
||||
)
|
||||
|
||||
return {
|
||||
"title": "Channels",
|
||||
"media_content_id": "",
|
||||
"media_content_type": "library",
|
||||
"can_play": False,
|
||||
"children": [
|
||||
{
|
||||
"title": channel,
|
||||
"media_content_id": channel,
|
||||
"media_content_type": MEDIA_TYPE_CHANNEL,
|
||||
"can_play": True,
|
||||
}
|
||||
return BrowseMedia(
|
||||
title="Channels",
|
||||
media_content_id="",
|
||||
media_content_type=MEDIA_TYPE_CHANNELS,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=[
|
||||
BrowseMedia(
|
||||
title=channel,
|
||||
media_content_id=channel,
|
||||
media_content_type=MEDIA_TYPE_CHANNEL,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
)
|
||||
for channel in self._channels.values()
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and update device state."""
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Support to interface with the Plex API."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.media_player import BrowseMedia
|
||||
from homeassistant.components.media_player.errors import BrowseError
|
||||
|
||||
from .const import DOMAIN
|
||||
|
@ -34,10 +35,10 @@ def browse_media(
|
|||
return None
|
||||
|
||||
media_info = item_payload(media)
|
||||
if media_info.get("can_expand"):
|
||||
media_info["children"] = []
|
||||
if media_info.can_expand:
|
||||
media_info.children = []
|
||||
for item in media:
|
||||
media_info["children"].append(item_payload(item))
|
||||
media_info.children.append(item_payload(item))
|
||||
return media_info
|
||||
|
||||
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_type": item.type,
|
||||
"can_play": True,
|
||||
"can_expand": item.type in EXPANDABLES,
|
||||
}
|
||||
if hasattr(item, "thumbUrl"):
|
||||
payload["thumbnail"] = item.thumbUrl
|
||||
if item.type in EXPANDABLES:
|
||||
payload["can_expand"] = True
|
||||
return payload
|
||||
|
||||
return BrowseMedia(**payload)
|
||||
|
||||
|
||||
def library_section_payload(section):
|
||||
|
|
|
@ -7,6 +7,7 @@ import voluptuous as vol
|
|||
from homeassistant.components.media_player import (
|
||||
DEVICE_CLASS_RECEIVER,
|
||||
DEVICE_CLASS_TV,
|
||||
BrowseMedia,
|
||||
MediaPlayerEntity,
|
||||
)
|
||||
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."""
|
||||
library_info = {
|
||||
"title": "Media Library",
|
||||
"media_content_id": "library",
|
||||
"media_content_type": "library",
|
||||
"can_play": False,
|
||||
"can_expand": True,
|
||||
"children": [],
|
||||
}
|
||||
library_info = BrowseMedia(
|
||||
title="Media Library",
|
||||
media_content_id="library",
|
||||
media_content_type="library",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=[],
|
||||
)
|
||||
|
||||
library_info["children"].append(
|
||||
{
|
||||
"title": "Apps",
|
||||
"media_content_id": "apps",
|
||||
"media_content_type": MEDIA_TYPE_APPS,
|
||||
"can_expand": True,
|
||||
"can_play": False,
|
||||
}
|
||||
library_info.children.append(
|
||||
BrowseMedia(
|
||||
title="Apps",
|
||||
media_content_id="apps",
|
||||
media_content_type=MEDIA_TYPE_APPS,
|
||||
can_expand=True,
|
||||
can_play=False,
|
||||
)
|
||||
)
|
||||
|
||||
if channels:
|
||||
library_info["children"].append(
|
||||
{
|
||||
"title": "Channels",
|
||||
"media_content_id": "channels",
|
||||
"media_content_type": MEDIA_TYPE_CHANNELS,
|
||||
"can_expand": True,
|
||||
"can_play": False,
|
||||
}
|
||||
library_info.children.append(
|
||||
BrowseMedia(
|
||||
title="Channels",
|
||||
media_content_id="channels",
|
||||
media_content_type=MEDIA_TYPE_CHANNELS,
|
||||
can_expand=True,
|
||||
can_play=False,
|
||||
)
|
||||
)
|
||||
|
||||
return library_info
|
||||
|
@ -283,41 +284,43 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
|||
response = None
|
||||
|
||||
if media_content_type == MEDIA_TYPE_APPS:
|
||||
response = {
|
||||
"title": "Apps",
|
||||
"media_content_id": "apps",
|
||||
"media_content_type": MEDIA_TYPE_APPS,
|
||||
"can_expand": True,
|
||||
"can_play": False,
|
||||
"children": [
|
||||
{
|
||||
"title": app.name,
|
||||
"thumbnail": self.coordinator.roku.app_icon_url(app.app_id),
|
||||
"media_content_id": app.app_id,
|
||||
"media_content_type": MEDIA_TYPE_APP,
|
||||
"can_play": True,
|
||||
}
|
||||
response = BrowseMedia(
|
||||
title="Apps",
|
||||
media_content_id="apps",
|
||||
media_content_type=MEDIA_TYPE_APPS,
|
||||
can_expand=True,
|
||||
can_play=False,
|
||||
children=[
|
||||
BrowseMedia(
|
||||
title=app.name,
|
||||
thumbnail=self.coordinator.roku.app_icon_url(app.app_id),
|
||||
media_content_id=app.app_id,
|
||||
media_content_type=MEDIA_TYPE_APP,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
)
|
||||
for app in self.coordinator.data.apps
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
if media_content_type == MEDIA_TYPE_CHANNELS:
|
||||
response = {
|
||||
"title": "Channels",
|
||||
"media_content_id": "channels",
|
||||
"media_content_type": MEDIA_TYPE_CHANNELS,
|
||||
"can_expand": True,
|
||||
"can_play": False,
|
||||
"children": [
|
||||
{
|
||||
"title": channel.name,
|
||||
"media_content_id": channel.number,
|
||||
"media_content_type": MEDIA_TYPE_CHANNEL,
|
||||
"can_play": True,
|
||||
}
|
||||
response = BrowseMedia(
|
||||
title="Channels",
|
||||
media_content_id="channels",
|
||||
media_content_type=MEDIA_TYPE_CHANNELS,
|
||||
can_expand=True,
|
||||
can_play=False,
|
||||
children=[
|
||||
BrowseMedia(
|
||||
title=channel.name,
|
||||
media_content_id=channel.number,
|
||||
media_content_type=MEDIA_TYPE_CHANNEL,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
)
|
||||
for channel in self.coordinator.data.channels
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
if response is None:
|
||||
raise BrowseError(
|
||||
|
|
|
@ -14,7 +14,7 @@ import pysonos.music_library
|
|||
import pysonos.snapshot
|
||||
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 (
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
MEDIA_TYPE_ALBUM,
|
||||
|
@ -1462,15 +1462,15 @@ def build_item_response(media_library, payload):
|
|||
except IndexError:
|
||||
title = LIBRARY_TITLES_MAPPING[payload["idstring"]]
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
"thumbnail": thumbnail,
|
||||
"media_content_id": payload["idstring"],
|
||||
"media_content_type": payload["search_type"],
|
||||
"children": [item_payload(item) for item in media],
|
||||
"can_play": can_play(payload["search_type"]),
|
||||
"can_expand": can_expand(payload["search_type"]),
|
||||
}
|
||||
return BrowseMedia(
|
||||
title=title,
|
||||
thumbnail=thumbnail,
|
||||
media_content_id=payload["idstring"],
|
||||
media_content_type=payload["search_type"],
|
||||
children=[item_payload(item) for item in media],
|
||||
can_play=can_play(payload["search_type"]),
|
||||
can_expand=can_expand(payload["search_type"]),
|
||||
)
|
||||
|
||||
|
||||
def item_payload(item):
|
||||
|
|
|
@ -9,7 +9,7 @@ from aiohttp import ClientError
|
|||
from spotipy import Spotify, SpotifyException
|
||||
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 (
|
||||
MEDIA_TYPE_ALBUM,
|
||||
MEDIA_TYPE_ARTIST,
|
||||
|
@ -439,28 +439,30 @@ def build_item_response(spotify, payload):
|
|||
items = media.get("items", [])
|
||||
else:
|
||||
media = None
|
||||
items = []
|
||||
|
||||
if media is 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 = {
|
||||
"title": title,
|
||||
"media_content_id": payload.get("media_content_id"),
|
||||
"media_content_type": payload.get("media_content_type"),
|
||||
"can_play": payload.get("media_content_type") in PLAYABLE_MEDIA_TYPES,
|
||||
"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:
|
||||
response["thumbnail"] = fetch_image_url(media)
|
||||
|
||||
return response
|
||||
return BrowseMedia(**response)
|
||||
|
||||
|
||||
def item_payload(item):
|
||||
|
@ -469,32 +471,31 @@ def item_payload(item):
|
|||
|
||||
Used by async_browse_media.
|
||||
"""
|
||||
can_expand = item.get("type") not in [None, MEDIA_TYPE_TRACK]
|
||||
|
||||
if (
|
||||
MEDIA_TYPE_TRACK in item
|
||||
or item.get("type") != MEDIA_TYPE_ALBUM
|
||||
and "playlists" in item
|
||||
):
|
||||
track = item.get(MEDIA_TYPE_TRACK)
|
||||
payload = {
|
||||
"title": track.get("name"),
|
||||
"thumbnail": fetch_image_url(track.get(MEDIA_TYPE_ALBUM, {})),
|
||||
"media_content_id": track.get("uri"),
|
||||
"media_content_type": MEDIA_TYPE_TRACK,
|
||||
"can_play": True,
|
||||
}
|
||||
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,
|
||||
}
|
||||
return BrowseMedia(
|
||||
title=track.get("name"),
|
||||
thumbnail=fetch_image_url(track.get(MEDIA_TYPE_ALBUM, {})),
|
||||
media_content_id=track.get("uri"),
|
||||
media_content_type=MEDIA_TYPE_TRACK,
|
||||
can_play=True,
|
||||
can_expand=can_expand,
|
||||
)
|
||||
|
||||
if item.get("type") not in [None, MEDIA_TYPE_TRACK]:
|
||||
payload["can_expand"] = True
|
||||
|
||||
return payload
|
||||
return BrowseMedia(
|
||||
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,
|
||||
can_expand=can_expand,
|
||||
)
|
||||
|
||||
|
||||
def library_payload():
|
||||
|
|
|
@ -41,8 +41,8 @@ async def test_async_browse_media(hass):
|
|||
|
||||
# Test non-media ignored (/media has test.mp3 and not_media.txt)
|
||||
media = await media_source.async_browse_media(hass, "")
|
||||
assert isinstance(media, media_source.models.BrowseMedia)
|
||||
assert media.name == "media/"
|
||||
assert isinstance(media, media_source.models.BrowseMediaSource)
|
||||
assert media.title == "media/"
|
||||
assert len(media.children) == 1
|
||||
|
||||
# Test invalid media content
|
||||
|
@ -51,9 +51,9 @@ async def test_async_browse_media(hass):
|
|||
|
||||
# Test base URI returns all domains
|
||||
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 media.children[0].name == "Local Media"
|
||||
assert media.children[0].title == "Local Media"
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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(
|
||||
"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["id"] == 1
|
||||
assert media.to_media_player_item() == msg["result"]
|
||||
assert media.as_dict() == msg["result"]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.media_source.async_browse_media",
|
||||
|
|
|
@ -1,17 +1,30 @@
|
|||
"""Test Media Source model methods."""
|
||||
from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC
|
||||
from homeassistant.components.media_source import const, models
|
||||
|
||||
|
||||
async def test_browse_media_to_media_player_item():
|
||||
"""Test BrowseMedia conversion to media player item dict."""
|
||||
base = models.BrowseMedia(const.DOMAIN, "media", "media/", False, True)
|
||||
async def test_browse_media_as_dict():
|
||||
"""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,
|
||||
)
|
||||
base.children = [
|
||||
models.BrowseMedia(
|
||||
const.DOMAIN, "media/test.mp3", "test.mp3", True, False, "audio/mp3"
|
||||
models.BrowseMediaSource(
|
||||
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["media_content_type"] == "folder"
|
||||
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"
|
||||
|
||||
|
||||
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():
|
||||
"""Test MediaSource uses domain as default name."""
|
||||
source = models.MediaSource(const.DOMAIN)
|
||||
|
|
Loading…
Reference in New Issue