core/homeassistant/components/media_player/browse_media.py

162 lines
5.3 KiB
Python

"""Browse media features for media player."""
from __future__ import annotations
from collections.abc import Sequence
from datetime import timedelta
import logging
from typing import Any
from urllib.parse import quote
import yarl
from homeassistant.components.http.auth import async_sign_path
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.network import (
NoURLAvailableError,
get_supervisor_network_url,
get_url,
is_hass_url,
)
from .const import CONTENT_AUTH_EXPIRY_TIME, MediaClass, MediaType
# Paths that we don't need to sign
PATHS_WITHOUT_AUTH = ("/api/tts_proxy/", "/api/esphome/ffmpeg_proxy/")
@callback
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
elif media_content_id[0] != "/":
return media_content_id
# https://github.com/pylint-dev/pylint/issues/3484
# pylint: disable-next=using-constant-test
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
class BrowseMedia:
"""Represent a browsable media file."""
def __init__(
self,
*,
media_class: MediaClass | str,
media_content_id: str,
media_content_type: MediaType | str,
title: str,
can_play: bool,
can_expand: bool,
children: Sequence[BrowseMedia] | None = None,
children_media_class: MediaClass | str | None = None,
thumbnail: str | None = None,
not_shown: int = 0,
) -> None:
"""Initialize browse media item."""
self.media_class = media_class
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.children_media_class = children_media_class
self.thumbnail = thumbnail
self.not_shown = not_shown
def as_dict(self, *, parent: bool = True) -> dict[str, Any]:
"""Convert Media class to browse media dictionary."""
if self.children_media_class is None and self.children:
self.calculate_children_class()
response: dict[str, Any] = {
"title": self.title,
"media_class": self.media_class,
"media_content_type": self.media_content_type,
"media_content_id": self.media_content_id,
"children_media_class": self.children_media_class,
"can_play": self.can_play,
"can_expand": self.can_expand,
"thumbnail": self.thumbnail,
}
if not parent:
return response
response["not_shown"] = self.not_shown
if self.children:
response["children"] = [
child.as_dict(parent=False) for child in self.children
]
else:
response["children"] = []
return response
def calculate_children_class(self) -> None:
"""Count the children media classes and calculate the correct class."""
self.children_media_class = MediaClass.DIRECTORY
assert self.children is not None
proposed_class = self.children[0].media_class
if all(child.media_class == proposed_class for child in self.children):
self.children_media_class = proposed_class
def __repr__(self) -> str:
"""Return representation of browse media."""
return f"<BrowseMedia {self.title} ({self.media_class})>"