diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 17607cd3f8c..7e6c34a8324 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -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 diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 886f0170a06..16cabe5edb9 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -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 diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 4c8fdace9d7..74c65fcd780 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -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" diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 6dc4eecc6dc..22f633d21d0 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -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)) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 2374edf0c33..34b526171ea 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -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 diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index b93cb961449..cd8e44f4a24 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -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 diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index a862252f02e..d5dcb2e6059 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -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 diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 9137a61d835..0e2d3f97c49 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -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.""" diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index e8d5d32c66e..39ad44f5ff1 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -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): diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index c25145ae2d7..c8f1b6d999d 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -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( diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 3ad22de9151..3da46b07e9e 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -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): diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 1b3140327ed..7f7659061e8 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -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(): diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index bc1d901e03f..eb387fcc6a3 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -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", diff --git a/tests/components/media_source/test_models.py b/tests/components/media_source/test_models.py index e7bac5acc9a..f951fcfb0c0 100644 --- a/tests/components/media_source/test_models.py +++ b/tests/components/media_source/test_models.py @@ -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)