示例#1
0
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
示例#2
0
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
示例#3
0
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")
示例#4
0
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
示例#5
0
    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)
示例#6
0
    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
        )