diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 59ebf1ead55..c057ea240c0 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -30,7 +30,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from . import config_flow from .browse_media import async_browse_media from .const import DOMAIN, LOGGER, SPOTIFY_SCOPES -from .util import is_spotify_media_type, resolve_spotify_media_type +from .util import ( + is_spotify_media_type, + resolve_spotify_media_type, + spotify_uri_from_media_browser_url, +) CONFIG_SCHEMA = vol.Schema( { @@ -50,6 +54,7 @@ PLATFORMS = [Platform.MEDIA_PLAYER] __all__ = [ "async_browse_media", "DOMAIN", + "spotify_uri_from_media_browser_url", "is_spotify_media_type", "resolve_spotify_media_type", ] diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index efaa07b3172..db2379a57fc 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -6,11 +6,13 @@ import logging from typing import Any from spotipy import Spotify +import yarl from homeassistant.backports.enum import StrEnum from homeassistant.components.media_player import BrowseError, BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, + MEDIA_CLASS_APP, MEDIA_CLASS_ARTIST, MEDIA_CLASS_DIRECTORY, MEDIA_CLASS_EPISODE, @@ -137,15 +139,53 @@ class UnknownMediaType(BrowseError): async def async_browse_media( hass: HomeAssistant, - media_content_type: str, - media_content_id: str, + media_content_type: str | None, + media_content_id: str | None, *, can_play_artist: bool = True, ) -> BrowseMedia: """Browse Spotify media.""" - if not (info := next(iter(hass.data[DOMAIN].values()), None)): - raise BrowseError("No Spotify accounts available") - return await async_browse_media_internal( + parsed_url = None + info = None + + # Check if caller is requesting the root nodes + if media_content_type is None and media_content_id is None: + children = [] + for config_entry_id, info in hass.data[DOMAIN].items(): + config_entry = hass.config_entries.async_get_entry(config_entry_id) + assert config_entry is not None + children.append( + BrowseMedia( + title=config_entry.title, + media_class=MEDIA_CLASS_APP, + media_content_id=f"{MEDIA_PLAYER_PREFIX}{config_entry_id}", + media_content_type=f"{MEDIA_PLAYER_PREFIX}library", + thumbnail="https://brands.home-assistant.io/_/spotify/logo.png", + can_play=False, + can_expand=True, + ) + ) + return BrowseMedia( + title="Spotify", + media_class=MEDIA_CLASS_APP, + media_content_id=MEDIA_PLAYER_PREFIX, + media_content_type="spotify", + thumbnail="https://brands.home-assistant.io/_/spotify/logo.png", + can_play=False, + can_expand=True, + children=children, + ) + + if media_content_id is None or not media_content_id.startswith(MEDIA_PLAYER_PREFIX): + raise BrowseError("Invalid Spotify URL specified") + + # Check for config entry specifier, and extract Spotify URI + parsed_url = yarl.URL(media_content_id) + if (info := hass.data[DOMAIN].get(parsed_url.host)) is None: + raise BrowseError("Invalid Spotify account specified") + media_content_id = parsed_url.name + + result = await async_browse_media_internal( hass, info.client, info.session, @@ -155,6 +195,13 @@ async def async_browse_media( can_play_artist=can_play_artist, ) + # Build new URLs with config entry specifyers + result.media_content_id = str(parsed_url.with_name(result.media_content_id)) + if result.children: + for child in result.children: + child.media_content_id = str(parsed_url.with_name(child.media_content_id)) + return result + async def async_browse_media_internal( hass: HomeAssistant, diff --git a/homeassistant/components/spotify/util.py b/homeassistant/components/spotify/util.py index cdb8e933523..7f7f682fb9e 100644 --- a/homeassistant/components/spotify/util.py +++ b/homeassistant/components/spotify/util.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Any +import yarl + from .const import MEDIA_PLAYER_PREFIX @@ -22,3 +24,11 @@ def fetch_image_url(item: dict[str, Any], key="images") -> str | None: return item.get(key, [])[0].get("url") except IndexError: return None + + +def spotify_uri_from_media_browser_url(media_content_id: str) -> str: + """Extract spotify URI from media browser URL.""" + if media_content_id and media_content_id.startswith(MEDIA_PLAYER_PREFIX): + parsed_url = yarl.URL(media_content_id) + media_content_id = parsed_url.name + return media_content_id