Browse media class (#39698)
This commit is contained in:
parent
13a6aaa6ff
commit
df8daf561e
14 changed files with 342 additions and 235 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Reference in a new issue