def async_process_play_media_url(hass: HomeAssistant, media_content_id: str, *, allow_relative_url: bool = False) -> str: """Update a media URL with authentication if it points at Home Assistant.""" if media_content_id[0] != "/" and not is_hass_url(hass, media_content_id): return media_content_id parsed = yarl.URL(media_content_id) if parsed.query: logging.getLogger(__name__).debug( "Not signing path for content with query param") else: signed_path = async_sign_path( hass, quote(parsed.path), timedelta(seconds=CONTENT_AUTH_EXPIRY_TIME), ) media_content_id = str(parsed.join(yarl.URL(signed_path))) # convert relative URL to absolute URL if media_content_id[0] == "/" and not allow_relative_url: media_content_id = f"{get_url(hass)}{media_content_id}" return media_content_id
def async_process_play_media_url( hass: HomeAssistant, media_content_id: str, *, allow_relative_url: bool = False, for_supervisor_network: bool = False, ) -> str: """Update a media URL with authentication if it points at Home Assistant.""" parsed = yarl.URL(media_content_id) if parsed.scheme and parsed.scheme not in ("http", "https"): return media_content_id if parsed.is_absolute(): if not is_hass_url(hass, media_content_id): return media_content_id else: if media_content_id[0] != "/": raise ValueError("URL is relative, but does not start with a /") if parsed.query: logging.getLogger(__name__).debug( "Not signing path for content with query param" ) elif parsed.path.startswith(PATHS_WITHOUT_AUTH): # We don't sign this path if it doesn't need auth. Although signing itself can't hurt, # some devices are unable to handle long URLs and the auth signature might push it over. pass else: signed_path = async_sign_path( hass, quote(parsed.path), timedelta(seconds=CONTENT_AUTH_EXPIRY_TIME), ) media_content_id = str(parsed.join(yarl.URL(signed_path))) # convert relative URL to absolute URL if not parsed.is_absolute() and not allow_relative_url: base_url = None if for_supervisor_network: base_url = get_supervisor_network_url(hass) if not base_url: try: base_url = get_url(hass) except NoURLAvailableError as err: msg = "Unable to determine Home Assistant URL to send to device" if ( hass.config.api and hass.config.api.use_ssl and (not hass.config.external_url or not hass.config.internal_url) ): msg += ". Configure internal and external URL in general settings." raise HomeAssistantError(msg) from err media_content_id = f"{base_url}{media_content_id}" return media_content_id
async def test_is_hass_url_addon_url(hass): """Test is_hass_url with a supervisor network URL.""" assert is_hass_url(hass, "http://homeassistant:8123") is False hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") await async_process_ha_core_config( hass, {"internal_url": "http://example.local:8123"}, ) assert is_hass_url(hass, "http://homeassistant:8123") is False mock_component(hass, "hassio") assert is_hass_url(hass, "http://homeassistant:8123") assert not is_hass_url(hass, "https://homeassistant:8123") hass.config.api = Mock(use_ssl=True, port=8123, local_ip="192.168.123.123") assert not is_hass_url(hass, "http://homeassistant:8123") assert is_hass_url(hass, "https://homeassistant:8123")
async def test_is_hass_url(hass): """Test is_hass_url.""" assert hass.config.api is None assert hass.config.internal_url is None assert hass.config.external_url is None assert is_hass_url(hass, "http://example.com") is False assert is_hass_url(hass, "bad_url") is False assert is_hass_url(hass, "bad_url.com") is False assert is_hass_url(hass, "http:/bad_url.com") is False hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") assert is_hass_url(hass, "http://192.168.123.123:8123") is True assert is_hass_url(hass, "https://192.168.123.123:8123") is False assert is_hass_url(hass, "http://192.168.123.123") is False await async_process_ha_core_config( hass, {"internal_url": "http://example.local:8123"}, ) assert is_hass_url(hass, "http://example.local:8123") is True assert is_hass_url(hass, "https://example.local:8123") is False assert is_hass_url(hass, "http://example.local") is False await async_process_ha_core_config( hass, {"external_url": "https://example.com:443"}, ) assert is_hass_url(hass, "https://example.com:443") is True assert is_hass_url(hass, "https://example.com") is True assert is_hass_url(hass, "http://example.com:443") is False assert is_hass_url(hass, "http://example.com") is False with patch.object( hass.components.cloud, "async_remote_ui_url", return_value="https://example.nabu.casa", ): assert is_hass_url(hass, "https://example.nabu.casa") is False hass.config.components.add("cloud") assert is_hass_url(hass, "https://example.nabu.casa:443") is True assert is_hass_url(hass, "https://example.nabu.casa") is True assert is_hass_url(hass, "http://example.nabu.casa:443") is False assert is_hass_url(hass, "http://example.nabu.casa") is False
async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" # Handle media_source if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media( self.hass, media_id) media_type = sourced_media.mime_type media_id = sourced_media.url extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) metadata = extra.get("metadata") # Handle media supported by a known cast app if media_type == CAST_DOMAIN: try: app_data = json.loads(media_id) if metadata is not None: app_data["metadata"] = extra.get("metadata") except json.JSONDecodeError: _LOGGER.error("Invalid JSON in media_content_id") raise # Special handling for passed `app_id` parameter. This will only launch # an arbitrary cast app, generally for UX. if "app_id" in app_data: app_id = app_data.pop("app_id") _LOGGER.info("Starting Cast app by ID %s", app_id) await self.hass.async_add_executor_job( self._chromecast.start_app, app_id) if app_data: _LOGGER.warning( "Extra keys %s were ignored. Please use app_name to cast media", app_data.keys(), ) return app_name = app_data.pop("app_name") try: await self.hass.async_add_executor_job(quick_play, self._chromecast, app_name, app_data) except NotImplementedError: _LOGGER.error("App %s not supported", app_name) return # Try the cast platforms for platform in self.hass.data[CAST_DOMAIN].values(): result = await platform.async_play_media(self.hass, self.entity_id, self._chromecast, media_type, media_id) if result: return # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) # Configure play command for when playing a HLS stream if is_hass_url(self.hass, media_id): parsed = yarl.URL(media_id) if parsed.path.startswith("/api/hls/"): extra = { **extra, "stream_type": "LIVE", "media_info": { "hlsVideoSegmentFormat": "fmp4", }, } # Default to play with the default media receiver app_data = {"media_id": media_id, "media_type": media_type, **extra} await self.hass.async_add_executor_job(quick_play, self._chromecast, "default_media_receiver", app_data)
async def async_play_media( self, media_type: str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" chromecast = self._get_chromecast() # Handle media_source if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_type = sourced_media.mime_type media_id = sourced_media.url extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) # Handle media supported by a known cast app if media_type == CAST_DOMAIN: try: app_data = json.loads(media_id) if metadata := extra.get("metadata"): app_data["metadata"] = metadata except json.JSONDecodeError: _LOGGER.error("Invalid JSON in media_content_id") raise # Special handling for passed `app_id` parameter. This will only launch # an arbitrary cast app, generally for UX. if "app_id" in app_data: app_id = app_data.pop("app_id") _LOGGER.info("Starting Cast app by ID %s", app_id) await self.hass.async_add_executor_job(chromecast.start_app, app_id) if app_data: _LOGGER.warning( "Extra keys %s were ignored. Please use app_name to cast media", app_data.keys(), ) return app_name = app_data.pop("app_name") try: await self.hass.async_add_executor_job( quick_play, chromecast, app_name, app_data ) except NotImplementedError: _LOGGER.error("App %s not supported", app_name) return # Try the cast platforms for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): result = await platform.async_play_media( self.hass, self.entity_id, chromecast, media_type, media_id ) if result: return # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) # Configure play command for when playing a HLS stream if is_hass_url(self.hass, media_id): parsed = yarl.URL(media_id) if parsed.path.startswith("/api/hls/"): extra = { **extra, "stream_type": "LIVE", "media_info": { "hlsVideoSegmentFormat": "fmp4", }, } elif ( media_id.endswith(".m3u") or media_id.endswith(".m3u8") or media_id.endswith(".pls") ): try: playlist = await parse_playlist(self.hass, media_id) _LOGGER.debug( "[%s %s] Playing item %s from playlist %s", self.entity_id, self._cast_info.friendly_name, playlist[0].url, media_id, ) media_id = playlist[0].url if title := playlist[0].title: extra = { **extra, "metadata": {"title": title}, } except PlaylistSupported as err: _LOGGER.debug( "[%s %s] Playlist %s is supported: %s", self.entity_id, self._cast_info.friendly_name, media_id, err, ) except PlaylistError as err: _LOGGER.warning( "[%s %s] Failed to parse playlist %s: %s", self.entity_id, self._cast_info.friendly_name, media_id, err, ) # Default to play with the default media receiver app_data = {"media_id": media_id, "media_type": media_type, **extra} _LOGGER.debug( "[%s %s] Playing %s with default_media_receiver", self.entity_id, self._cast_info.friendly_name, app_data, ) await self.hass.async_add_executor_job( quick_play, chromecast, "default_media_receiver", app_data )