1 """Component to interface with various media players."""
3 from __future__
import annotations
7 from collections.abc
import Callable
8 from contextlib
import suppress
10 from enum
import StrEnum
11 import functools
as ft
12 from functools
import lru_cache
14 from http
import HTTPStatus
17 from typing
import Any, Final, Required, TypedDict, final
18 from urllib.parse
import quote, urlparse
21 from aiohttp
import web
22 from aiohttp.hdrs
import CACHE_CONTROL, CONTENT_TYPE
23 from aiohttp.typedefs
import LooseHeaders
24 from propcache
import cached_property
25 import voluptuous
as vol
34 SERVICE_MEDIA_NEXT_TRACK,
37 SERVICE_MEDIA_PLAY_PAUSE,
38 SERVICE_MEDIA_PREVIOUS_TRACK,
59 DeprecatedConstantEnum,
60 all_with_deprecated_constants,
61 check_if_deprecated_constant,
62 dir_with_deprecated_constants,
71 from .browse_media
import BrowseMedia, async_process_play_media_url
73 _DEPRECATED_MEDIA_CLASS_DIRECTORY,
74 _DEPRECATED_SUPPORT_BROWSE_MEDIA,
75 _DEPRECATED_SUPPORT_CLEAR_PLAYLIST,
76 _DEPRECATED_SUPPORT_GROUPING,
77 _DEPRECATED_SUPPORT_NEXT_TRACK,
78 _DEPRECATED_SUPPORT_PAUSE,
79 _DEPRECATED_SUPPORT_PLAY,
80 _DEPRECATED_SUPPORT_PLAY_MEDIA,
81 _DEPRECATED_SUPPORT_PREVIOUS_TRACK,
82 _DEPRECATED_SUPPORT_REPEAT_SET,
83 _DEPRECATED_SUPPORT_SEEK,
84 _DEPRECATED_SUPPORT_SELECT_SOUND_MODE,
85 _DEPRECATED_SUPPORT_SELECT_SOURCE,
86 _DEPRECATED_SUPPORT_SHUFFLE_SET,
87 _DEPRECATED_SUPPORT_STOP,
88 _DEPRECATED_SUPPORT_TURN_OFF,
89 _DEPRECATED_SUPPORT_TURN_ON,
90 _DEPRECATED_SUPPORT_VOLUME_MUTE,
91 _DEPRECATED_SUPPORT_VOLUME_SET,
92 _DEPRECATED_SUPPORT_VOLUME_STEP,
95 ATTR_ENTITY_PICTURE_LOCAL,
98 ATTR_INPUT_SOURCE_LIST,
99 ATTR_MEDIA_ALBUM_ARTIST,
100 ATTR_MEDIA_ALBUM_NAME,
104 ATTR_MEDIA_CONTENT_ID,
105 ATTR_MEDIA_CONTENT_TYPE,
112 ATTR_MEDIA_POSITION_UPDATED_AT,
115 ATTR_MEDIA_SEEK_POSITION,
116 ATTR_MEDIA_SERIES_TITLE,
120 ATTR_MEDIA_VOLUME_LEVEL,
121 ATTR_MEDIA_VOLUME_MUTED,
123 ATTR_SOUND_MODE_LIST,
124 CONTENT_AUTH_EXPIRY_TIME,
127 SERVICE_CLEAR_PLAYLIST,
130 SERVICE_SELECT_SOUND_MODE,
131 SERVICE_SELECT_SOURCE,
134 MediaPlayerEntityFeature,
139 from .errors
import BrowseError
141 _LOGGER = logging.getLogger(__name__)
143 DATA_COMPONENT: HassKey[EntityComponent[MediaPlayerEntity]] = HassKey(DOMAIN)
144 ENTITY_ID_FORMAT = DOMAIN +
".{}"
145 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
146 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
147 SCAN_INTERVAL = dt.timedelta(seconds=10)
149 CACHE_IMAGES: Final =
"images"
150 CACHE_MAXSIZE: Final =
"maxsize"
151 CACHE_LOCK: Final =
"lock"
152 CACHE_URL: Final =
"url"
153 CACHE_CONTENT: Final =
"content"
157 """Enqueue types for playing media."""
170 """Device class for media players."""
174 RECEIVER =
"receiver"
177 DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(MediaPlayerDeviceClass))
182 _DEPRECATED_DEVICE_CLASS_TV = DeprecatedConstantEnum(
183 MediaPlayerDeviceClass.TV,
"2025.10"
185 _DEPRECATED_DEVICE_CLASS_SPEAKER = DeprecatedConstantEnum(
186 MediaPlayerDeviceClass.SPEAKER,
"2025.10"
188 _DEPRECATED_DEVICE_CLASS_RECEIVER = DeprecatedConstantEnum(
189 MediaPlayerDeviceClass.RECEIVER,
"2025.10"
191 DEVICE_CLASSES = [cls.value
for cls
in MediaPlayerDeviceClass]
194 MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = {
195 vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
196 vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
197 vol.Exclusive(ATTR_MEDIA_ENQUEUE,
"enqueue_announce"): vol.Any(
198 cv.boolean, vol.Coerce(MediaPlayerEnqueue)
200 vol.Exclusive(ATTR_MEDIA_ANNOUNCE,
"enqueue_announce"): cv.boolean,
201 vol.Optional(ATTR_MEDIA_EXTRA, default={}): dict,
205 ATTR_MEDIA_VOLUME_LEVEL,
206 ATTR_MEDIA_VOLUME_MUTED,
207 ATTR_MEDIA_CONTENT_ID,
208 ATTR_MEDIA_CONTENT_TYPE,
211 ATTR_MEDIA_POSITION_UPDATED_AT,
214 ATTR_MEDIA_ALBUM_NAME,
215 ATTR_MEDIA_ALBUM_ARTIST,
217 ATTR_MEDIA_SERIES_TITLE,
234 """Class to hold a cached image."""
236 lock: Required[asyncio.Lock]
237 content: tuple[bytes |
None, str |
None]
241 """Class to hold a cached image."""
243 images: collections.OrderedDict[str, _CacheImage]
247 _ENTITY_IMAGE_CACHE =
_ImageCache(images=collections.OrderedDict(), maxsize=16)
251 def is_on(hass: HomeAssistant, entity_id: str |
None =
None) -> bool:
252 """Return true if specified media player entity_id is on.
254 Check all media player if no entity_id specified.
256 entity_ids = [entity_id]
if entity_id
else hass.states.entity_ids(DOMAIN)
258 not hass.states.is_state(entity_id, MediaPlayerState.OFF)
259 for entity_id
in entity_ids
263 def _rename_keys(**keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]:
264 """Create validator that renames keys.
266 Necessary because the service schema names do not match the command parameters.
271 def rename(value: dict[str, Any]) -> dict[str, Any]:
272 for to_key, from_key
in keys.items():
273 if from_key
in value:
274 value[to_key] = value.pop(from_key)
280 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
281 """Track states and offer events for media_players."""
282 component = hass.data[DATA_COMPONENT] = EntityComponent[MediaPlayerEntity](
283 logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL
286 websocket_api.async_register_command(hass, websocket_browse_media)
289 await component.async_setup(config)
291 component.async_register_entity_service(
292 SERVICE_TURN_ON,
None,
"async_turn_on", [MediaPlayerEntityFeature.TURN_ON]
294 component.async_register_entity_service(
295 SERVICE_TURN_OFF,
None,
"async_turn_off", [MediaPlayerEntityFeature.TURN_OFF]
297 component.async_register_entity_service(
301 [MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.TURN_ON],
303 component.async_register_entity_service(
307 [MediaPlayerEntityFeature.VOLUME_SET, MediaPlayerEntityFeature.VOLUME_STEP],
309 component.async_register_entity_service(
313 [MediaPlayerEntityFeature.VOLUME_SET, MediaPlayerEntityFeature.VOLUME_STEP],
315 component.async_register_entity_service(
316 SERVICE_MEDIA_PLAY_PAUSE,
318 "async_media_play_pause",
319 [MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE],
321 component.async_register_entity_service(
322 SERVICE_MEDIA_PLAY,
None,
"async_media_play", [MediaPlayerEntityFeature.PLAY]
324 component.async_register_entity_service(
325 SERVICE_MEDIA_PAUSE,
None,
"async_media_pause", [MediaPlayerEntityFeature.PAUSE]
327 component.async_register_entity_service(
328 SERVICE_MEDIA_STOP,
None,
"async_media_stop", [MediaPlayerEntityFeature.STOP]
330 component.async_register_entity_service(
331 SERVICE_MEDIA_NEXT_TRACK,
333 "async_media_next_track",
334 [MediaPlayerEntityFeature.NEXT_TRACK],
336 component.async_register_entity_service(
337 SERVICE_MEDIA_PREVIOUS_TRACK,
339 "async_media_previous_track",
340 [MediaPlayerEntityFeature.PREVIOUS_TRACK],
342 component.async_register_entity_service(
343 SERVICE_CLEAR_PLAYLIST,
345 "async_clear_playlist",
346 [MediaPlayerEntityFeature.CLEAR_PLAYLIST],
348 component.async_register_entity_service(
351 cv.make_entity_service_schema(
352 {vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float}
356 "async_set_volume_level",
357 [MediaPlayerEntityFeature.VOLUME_SET],
359 component.async_register_entity_service(
362 cv.make_entity_service_schema(
363 {vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean}
368 [MediaPlayerEntityFeature.VOLUME_MUTE],
370 component.async_register_entity_service(
373 cv.make_entity_service_schema(
374 {vol.Required(ATTR_MEDIA_SEEK_POSITION): cv.positive_float}
379 [MediaPlayerEntityFeature.SEEK],
381 component.async_register_entity_service(
383 {vol.Required(ATTR_GROUP_MEMBERS): vol.All(cv.ensure_list, [cv.entity_id])},
384 "async_join_players",
385 [MediaPlayerEntityFeature.GROUPING],
387 component.async_register_entity_service(
388 SERVICE_SELECT_SOURCE,
389 {vol.Required(ATTR_INPUT_SOURCE): cv.string},
390 "async_select_source",
391 [MediaPlayerEntityFeature.SELECT_SOURCE],
393 component.async_register_entity_service(
394 SERVICE_SELECT_SOUND_MODE,
395 {vol.Required(ATTR_SOUND_MODE): cv.string},
396 "async_select_sound_mode",
397 [MediaPlayerEntityFeature.SELECT_SOUND_MODE],
401 def _rewrite_enqueue(value: dict[str, Any]) -> dict[str, Any]:
402 """Rewrite the enqueue value."""
403 if ATTR_MEDIA_ENQUEUE
not in value:
405 elif value[ATTR_MEDIA_ENQUEUE]
is True:
406 value[ATTR_MEDIA_ENQUEUE] = MediaPlayerEnqueue.ADD
408 "Playing media with enqueue set to True is deprecated. Use 'add'"
411 elif value[ATTR_MEDIA_ENQUEUE]
is False:
412 value[ATTR_MEDIA_ENQUEUE] = MediaPlayerEnqueue.PLAY
414 "Playing media with enqueue set to False is deprecated. Use 'play'"
420 component.async_register_entity_service(
423 cv.make_entity_service_schema(MEDIA_PLAYER_PLAY_MEDIA_SCHEMA),
426 media_type=ATTR_MEDIA_CONTENT_TYPE,
427 media_id=ATTR_MEDIA_CONTENT_ID,
428 enqueue=ATTR_MEDIA_ENQUEUE,
432 [MediaPlayerEntityFeature.PLAY_MEDIA],
434 component.async_register_entity_service(
436 {vol.Required(ATTR_MEDIA_SHUFFLE): cv.boolean},
438 [MediaPlayerEntityFeature.SHUFFLE_SET],
440 component.async_register_entity_service(
441 SERVICE_UNJOIN,
None,
"async_unjoin_player", [MediaPlayerEntityFeature.GROUPING]
444 component.async_register_entity_service(
446 {vol.Required(ATTR_MEDIA_REPEAT): vol.Coerce(RepeatMode)},
448 [MediaPlayerEntityFeature.REPEAT_SET],
455 """Set up a config entry."""
460 """Unload a config entry."""
465 """A class that describes media player entities."""
467 device_class: MediaPlayerDeviceClass |
None =
None
468 volume_step: float |
None =
None
471 CACHED_PROPERTIES_WITH_ATTR_ = {
478 "media_content_type",
481 "media_position_updated_at",
483 "media_image_remotely_accessible",
487 "media_album_artist",
489 "media_series_title",
503 "supported_features",
509 """Create hash for media image url."""
510 return hashlib.sha256(url.encode(
"utf-8")).hexdigest()[:16]
514 """ABC for media player entities."""
516 _entity_component_unrecorded_attributes = frozenset(
518 ATTR_ENTITY_PICTURE_LOCAL,
520 ATTR_INPUT_SOURCE_LIST,
521 ATTR_MEDIA_POSITION_UPDATED_AT,
523 ATTR_SOUND_MODE_LIST,
527 entity_description: MediaPlayerEntityDescription
528 _access_token: str |
None =
None
530 _attr_app_id: str |
None =
None
531 _attr_app_name: str |
None =
None
532 _attr_device_class: MediaPlayerDeviceClass |
None
533 _attr_group_members: list[str] |
None =
None
534 _attr_is_volume_muted: bool |
None =
None
535 _attr_media_album_artist: str |
None =
None
536 _attr_media_album_name: str |
None =
None
537 _attr_media_artist: str |
None =
None
538 _attr_media_channel: str |
None =
None
539 _attr_media_content_id: str |
None =
None
540 _attr_media_content_type: MediaType | str |
None =
None
541 _attr_media_duration: int |
None =
None
542 _attr_media_episode: str |
None =
None
543 _attr_media_image_hash: str |
None
544 _attr_media_image_remotely_accessible: bool =
False
545 _attr_media_image_url: str |
None =
None
546 _attr_media_playlist: str |
None =
None
547 _attr_media_position_updated_at: dt.datetime |
None =
None
548 _attr_media_position: int |
None =
None
549 _attr_media_season: str |
None =
None
550 _attr_media_series_title: str |
None =
None
551 _attr_media_title: str |
None =
None
552 _attr_media_track: int |
None =
None
553 _attr_repeat: RepeatMode | str |
None =
None
554 _attr_shuffle: bool |
None =
None
555 _attr_sound_mode_list: list[str] |
None =
None
556 _attr_sound_mode: str |
None =
None
557 _attr_source_list: list[str] |
None =
None
558 _attr_source: str |
None =
None
559 _attr_state: MediaPlayerState |
None =
None
561 _attr_volume_level: float |
None =
None
562 _attr_volume_step: float
567 """Return the class of this entity."""
568 if hasattr(self,
"_attr_device_class"):
569 return self._attr_device_class
570 if hasattr(self,
"entity_description"):
571 return self.entity_description.device_class
575 def state(self) -> MediaPlayerState | None:
576 """State of the player."""
577 return self._attr_state
581 """Access token for this media player."""
588 """Volume level of the media player (0..1)."""
589 return self._attr_volume_level
593 """Return the step to be used by the volume_up and volume_down services."""
594 if hasattr(self,
"_attr_volume_step"):
595 return self._attr_volume_step
597 hasattr(self,
"entity_description")
598 and (volume_step := self.entity_description.volume_step)
is not None
605 """Boolean if volume is currently muted."""
606 return self._attr_is_volume_muted
610 """Content ID of current playing media."""
611 return self._attr_media_content_id
615 """Content type of current playing media."""
616 return self._attr_media_content_type
620 """Duration of current playing media in seconds."""
621 return self._attr_media_duration
625 """Position of current playing media in seconds."""
626 return self._attr_media_position
630 """When was the position of the current playing media valid.
632 Returns value from homeassistant.util.dt.utcnow().
634 return self._attr_media_position_updated_at
638 """Image url of current playing media."""
639 return self._attr_media_image_url
643 """If the image url is remotely accessible."""
644 return self._attr_media_image_remotely_accessible
648 """Hash value for media image."""
649 if hasattr(self,
"_attr_media_image_hash"):
650 return self._attr_media_image_hash
658 """Fetch media image of current playing image."""
666 media_content_type: str,
667 media_content_id: str,
668 media_image_id: str |
None =
None,
669 ) -> tuple[bytes |
None, str |
None]:
670 """Optionally fetch internally accessible image for media browser.
672 Must be implemented by integration.
678 """Title of current playing media."""
679 return self._attr_media_title
683 """Artist of current playing media, music track only."""
684 return self._attr_media_artist
688 """Album name of current playing media, music track only."""
689 return self._attr_media_album_name
693 """Album artist of current playing media, music track only."""
694 return self._attr_media_album_artist
698 """Track number of current playing media, music track only."""
699 return self._attr_media_track
703 """Title of series of current playing media, TV show only."""
704 return self._attr_media_series_title
708 """Season of current playing media, TV show only."""
709 return self._attr_media_season
713 """Episode of current playing media, TV show only."""
714 return self._attr_media_episode
718 """Channel currently playing."""
719 return self._attr_media_channel
723 """Title of Playlist currently playing."""
724 return self._attr_media_playlist
728 """ID of the current running app."""
729 return self._attr_app_id
733 """Name of the current running app."""
734 return self._attr_app_name
738 """Name of the current input source."""
739 return self._attr_source
743 """List of available input sources."""
744 return self._attr_source_list
748 """Name of the current sound mode."""
749 return self._attr_sound_mode
753 """List of available sound modes."""
754 return self._attr_sound_mode_list
758 """Boolean if shuffle is enabled."""
759 return self._attr_shuffle
762 def repeat(self) -> RepeatMode | str | None:
763 """Return current repeat mode."""
764 return self._attr_repeat
768 """List of members which are currently grouped together."""
769 return self._attr_group_members
773 """Flag media player features that are supported."""
774 return self._attr_supported_features
778 """Return the supported features as MediaPlayerEntityFeature.
780 Remove this compatibility shim in 2025.1 or later.
783 if type(features)
is int:
790 """Turn the media player on."""
791 raise NotImplementedError
794 """Turn the media player on."""
795 await self.
hasshass.async_add_executor_job(self.
turn_onturn_on)
798 """Turn the media player off."""
799 raise NotImplementedError
802 """Turn the media player off."""
803 await self.
hasshass.async_add_executor_job(self.
turn_offturn_off)
806 """Mute the volume."""
807 raise NotImplementedError
810 """Mute the volume."""
811 await self.
hasshass.async_add_executor_job(self.
mute_volumemute_volume, mute)
814 """Set volume level, range 0..1."""
815 raise NotImplementedError
818 """Set volume level, range 0..1."""
822 """Send play command."""
823 raise NotImplementedError
826 """Send play command."""
827 await self.
hasshass.async_add_executor_job(self.
media_playmedia_play)
830 """Send pause command."""
831 raise NotImplementedError
834 """Send pause command."""
835 await self.
hasshass.async_add_executor_job(self.
media_pausemedia_pause)
838 """Send stop command."""
839 raise NotImplementedError
842 """Send stop command."""
843 await self.
hasshass.async_add_executor_job(self.
media_stopmedia_stop)
846 """Send previous track command."""
847 raise NotImplementedError
850 """Send previous track command."""
854 """Send next track command."""
855 raise NotImplementedError
858 """Send next track command."""
862 """Send seek command."""
863 raise NotImplementedError
866 """Send seek command."""
867 await self.
hasshass.async_add_executor_job(self.
media_seekmedia_seek, position)
870 self, media_type: MediaType | str, media_id: str, **kwargs: Any
872 """Play a piece of media."""
873 raise NotImplementedError
876 self, media_type: MediaType | str, media_id: str, **kwargs: Any
878 """Play a piece of media."""
879 await self.
hasshass.async_add_executor_job(
880 ft.partial(self.
play_mediaplay_media, media_type, media_id, **kwargs)
884 """Select input source."""
885 raise NotImplementedError
888 """Select input source."""
889 await self.
hasshass.async_add_executor_job(self.
select_sourceselect_source, source)
892 """Select sound mode."""
893 raise NotImplementedError
896 """Select sound mode."""
900 """Clear players playlist."""
901 raise NotImplementedError
904 """Clear players playlist."""
908 """Enable/disable shuffle mode."""
909 raise NotImplementedError
912 """Enable/disable shuffle mode."""
913 await self.
hasshass.async_add_executor_job(self.
set_shuffleset_shuffle, shuffle)
916 """Set repeat mode."""
917 raise NotImplementedError
920 """Set repeat mode."""
921 await self.
hasshass.async_add_executor_job(self.
set_repeatset_repeat, repeat)
927 """Boolean if play is supported."""
933 """Boolean if pause is supported."""
939 """Boolean if stop is supported."""
945 """Boolean if seek is supported."""
951 """Boolean if setting volume is supported."""
957 """Boolean if muting volume is supported."""
963 """Boolean if previous track command supported."""
969 """Boolean if next track command supported."""
975 """Boolean if play media command supported."""
981 """Boolean if select source command supported."""
987 """Boolean if select sound mode command supported."""
995 """Boolean if clear playlist command supported."""
1001 """Boolean if shuffle is supported."""
1007 """Boolean if player grouping is supported."""
1011 """Toggle the power on the media player."""
1012 if hasattr(self,
"toggle"):
1013 await self.
hasshass.async_add_executor_job(self.toggle)
1017 MediaPlayerState.OFF,
1018 MediaPlayerState.IDLE,
1019 MediaPlayerState.STANDBY,
1026 """Turn volume up for media player.
1028 This method is a coroutine.
1030 if hasattr(self,
"volume_up"):
1031 await self.
hasshass.async_add_executor_job(self.volume_up)
1044 """Turn volume down for media player.
1046 This method is a coroutine.
1048 if hasattr(self,
"volume_down"):
1049 await self.
hasshass.async_add_executor_job(self.volume_down)
1062 """Play or pause the media player."""
1063 if hasattr(self,
"media_play_pause"):
1064 await self.
hasshass.async_add_executor_job(self.media_play_pause)
1074 """Return image of the media playing."""
1085 """Return local url to media image."""
1090 f
"/api/media_player_proxy/{self.entity_id}?"
1091 f
"token={self.access_token}&cache={image_hash}"
1096 """Return capability attributes."""
1097 data: dict[str, Any] = {}
1102 )
and MediaPlayerEntityFeature.SELECT_SOURCE
in supported_features:
1103 data[ATTR_INPUT_SOURCE_LIST] = source_list
1107 )
and MediaPlayerEntityFeature.SELECT_SOUND_MODE
in supported_features:
1108 data[ATTR_SOUND_MODE_LIST] = sound_mode_list
1115 """Return the state attributes."""
1116 state_attr: dict[str, Any] = {}
1119 state_attr[ATTR_GROUP_MEMBERS] = self.
group_membersgroup_members
1124 for attr
in ATTR_TO_PROPERTY:
1125 if (value := getattr(self, attr))
is not None:
1126 state_attr[attr] = value
1135 media_content_type: MediaType | str |
None =
None,
1136 media_content_id: str |
None =
None,
1138 """Return a BrowseMedia instance.
1140 The BrowseMedia instance will be used by the
1141 "media_player/browse_media" websocket command.
1143 raise NotImplementedError
1146 """Join `group_members` as a player group with the current player."""
1147 raise NotImplementedError
1150 """Join `group_members` as a player group with the current player."""
1151 await self.
hasshass.async_add_executor_job(self.
join_playersjoin_players, group_members)
1154 """Remove this player from any group."""
1155 raise NotImplementedError
1158 """Remove this player from any group."""
1163 ) -> tuple[bytes |
None, str |
None]:
1166 Images are cached in memory (the images are typically 10-100kB in size).
1168 cache_images = _ENTITY_IMAGE_CACHE[CACHE_IMAGES]
1169 cache_maxsize = _ENTITY_IMAGE_CACHE[CACHE_MAXSIZE]
1171 if urlparse(url).hostname
is None:
1172 url = f
"{get_url(self.hass)}{url}"
1174 if url
not in cache_images:
1175 cache_images[url] = {CACHE_LOCK: asyncio.Lock()}
1177 async
with cache_images[url][CACHE_LOCK]:
1178 if CACHE_CONTENT
in cache_images[url]:
1179 return cache_images[url][CACHE_CONTENT]
1183 async
with cache_images[url][CACHE_LOCK]:
1184 cache_images[url][CACHE_CONTENT] = content, content_type
1185 while len(cache_images) > cache_maxsize:
1186 cache_images.popitem(last=
False)
1188 return content, content_type
1191 """Retrieve an image."""
1196 media_content_type: str,
1197 media_content_id: str,
1198 media_image_id: str |
None =
None,
1200 """Generate an url for a media browser image."""
1202 f
"/api/media_player_proxy/{self.entity_id}/browse_media"
1205 f
"/{media_content_type}/{quote(media_content_id)}"
1210 url_query[
"media_image_id"] = media_image_id
1212 return str(
URL(url_path).with_query(url_query))
1216 """Media player view to serve an image."""
1218 requires_auth =
False
1219 url =
"/api/media_player_proxy/{entity_id}"
1220 name =
"api:media_player:image"
1224 url +
"/browse_media/{media_content_type}/{media_content_id:.+}",
1227 def __init__(self, component: EntityComponent[MediaPlayerEntity]) ->
None:
1228 """Initialize a media player view."""
1233 request: web.Request,
1235 media_content_type: MediaType | str |
None =
None,
1236 media_content_id: str |
None =
None,
1238 """Start a get request."""
1241 HTTPStatus.NOT_FOUND
1242 if request[KEY_AUTHENTICATED]
1243 else HTTPStatus.UNAUTHORIZED
1245 return web.Response(status=status)
1247 assert isinstance(player, MediaPlayerEntity)
1249 request[KEY_AUTHENTICATED]
1250 or request.query.get(
"token") == player.access_token
1253 if not authenticated:
1254 return web.Response(status=HTTPStatus.UNAUTHORIZED)
1256 if media_content_type
and media_content_id:
1257 media_image_id = request.query.get(
"media_image_id")
1258 data, content_type = await player.async_get_browse_image(
1259 media_content_type, media_content_id, media_image_id
1262 data, content_type = await player.async_get_media_image()
1265 return web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)
1267 headers: LooseHeaders = {CACHE_CONTROL:
"max-age=3600"}
1268 return web.Response(body=data, content_type=content_type, headers=headers)
1271 @websocket_api.websocket_command(
{
vol.Required("type"):
"media_player/browse_media",
1272 vol.Required(
"entity_id"): cv.entity_id,
1274 ATTR_MEDIA_CONTENT_TYPE,
1276 "media_content_type and media_content_id must be provided together",
1279 ATTR_MEDIA_CONTENT_ID,
1281 "media_content_type and media_content_id must be provided together",
1285 @websocket_api.async_response
1287 hass: HomeAssistant,
1289 msg: dict[str, Any],
1291 """Browse media available to the media_player entity.
1293 To use, media_player integrations can implement
1294 MediaPlayerEntity.async_browse_media()
1296 player = hass.data[DATA_COMPONENT].
get_entity(msg[
"entity_id"])
1299 connection.send_error(msg[
"id"],
"entity_not_found",
"Entity not found")
1302 if MediaPlayerEntityFeature.BROWSE_MEDIA
not in player.supported_features_compat:
1303 connection.send_message(
1304 websocket_api.error_message(
1305 msg[
"id"], ERR_NOT_SUPPORTED,
"Player does not support browsing media"
1310 media_content_type = msg.get(ATTR_MEDIA_CONTENT_TYPE)
1311 media_content_id = msg.get(ATTR_MEDIA_CONTENT_ID)
1314 payload = await player.async_browse_media(media_content_type, media_content_id)
1315 except NotImplementedError:
1316 assert player.platform
1318 "%s allows media browsing but its integration (%s) does not",
1320 player.platform.platform_name,
1322 connection.send_message(
1323 websocket_api.error_message(
1326 "Integration does not support browsing media",
1330 except BrowseError
as err:
1331 connection.send_message(
1332 websocket_api.error_message(msg[
"id"], ERR_UNKNOWN_ERROR,
str(err))
1337 if isinstance(payload, BrowseMedia):
1338 result = payload.as_dict()
1341 _LOGGER.warning(
"Browse Media should use new BrowseMedia class")
1343 connection.send_result(msg[
"id"], result)
1346 _FETCH_TIMEOUT = aiohttp.ClientTimeout(total=10)
1350 logger: logging.Logger, hass: HomeAssistant, url: str
1351 ) -> tuple[bytes |
None, str |
None]:
1352 """Retrieve an image."""
1353 content, content_type = (
None,
None)
1355 with suppress(TimeoutError):
1356 response = await websession.get(url, timeout=_FETCH_TIMEOUT)
1357 if response.status == HTTPStatus.OK:
1358 content = await response.read()
1359 if content_type := response.headers.get(CONTENT_TYPE):
1360 content_type = content_type.split(
";")[0]
1363 url_parts =
URL(url)
1364 if url_parts.user
is not None:
1365 url_parts = url_parts.with_user(
"xxxx")
1366 if url_parts.password
is not None:
1367 url_parts = url_parts.with_password(
"xxxxxxxx")
1368 url =
str(url_parts)
1369 logger.warning(
"Error retrieving proxied image from %s", url)
1371 return content, content_type
1377 __getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals())
1378 __dir__ = ft.partial(
1379 dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
1382
None _report_deprecated_supported_features_values(self, IntFlag replacement)
int|None supported_features(self)
CalendarEntity get_entity(HomeAssistant hass, str entity_id)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
list[str] all_with_deprecated_constants(dict[str, Any] module_globals)