mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Spotify browser add more sources (#39296)
Co-authored-by: Tobias Sauerwein <cgtobi@gmail.com>
This commit is contained in:
parent
f828cdcaef
commit
19818d96b7
@ -2,7 +2,7 @@
|
|||||||
"domain": "spotify",
|
"domain": "spotify",
|
||||||
"name": "Spotify",
|
"name": "Spotify",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/spotify",
|
"documentation": "https://www.home-assistant.io/integrations/spotify",
|
||||||
"requirements": ["spotipy==2.12.0"],
|
"requirements": ["spotipy==2.14.0"],
|
||||||
"zeroconf": ["_spotify-connect._tcp.local."],
|
"zeroconf": ["_spotify-connect._tcp.local."],
|
||||||
"dependencies": ["http"],
|
"dependencies": ["http"],
|
||||||
"codeowners": ["@frenck"],
|
"codeowners": ["@frenck"],
|
||||||
|
@ -13,6 +13,7 @@ from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity
|
|||||||
from homeassistant.components.media_player.const import (
|
from homeassistant.components.media_player.const import (
|
||||||
MEDIA_TYPE_ALBUM,
|
MEDIA_TYPE_ALBUM,
|
||||||
MEDIA_TYPE_ARTIST,
|
MEDIA_TYPE_ARTIST,
|
||||||
|
MEDIA_TYPE_EPISODE,
|
||||||
MEDIA_TYPE_MUSIC,
|
MEDIA_TYPE_MUSIC,
|
||||||
MEDIA_TYPE_PLAYLIST,
|
MEDIA_TYPE_PLAYLIST,
|
||||||
MEDIA_TYPE_TRACK,
|
MEDIA_TYPE_TRACK,
|
||||||
@ -70,19 +71,29 @@ SUPPORT_SPOTIFY = (
|
|||||||
|
|
||||||
BROWSE_LIMIT = 48
|
BROWSE_LIMIT = 48
|
||||||
|
|
||||||
|
MEDIA_TYPE_SHOW = "show"
|
||||||
|
|
||||||
PLAYABLE_MEDIA_TYPES = [
|
PLAYABLE_MEDIA_TYPES = [
|
||||||
MEDIA_TYPE_PLAYLIST,
|
MEDIA_TYPE_PLAYLIST,
|
||||||
MEDIA_TYPE_ALBUM,
|
MEDIA_TYPE_ALBUM,
|
||||||
MEDIA_TYPE_ARTIST,
|
MEDIA_TYPE_ARTIST,
|
||||||
|
MEDIA_TYPE_EPISODE,
|
||||||
|
MEDIA_TYPE_SHOW,
|
||||||
MEDIA_TYPE_TRACK,
|
MEDIA_TYPE_TRACK,
|
||||||
]
|
]
|
||||||
|
|
||||||
LIBRARY_MAP = {
|
LIBRARY_MAP = {
|
||||||
"user_playlists": "Playlists",
|
"current_user_playlists": "Playlists",
|
||||||
|
"current_user_followed_artists": "Artists",
|
||||||
|
"current_user_saved_albums": "Albums",
|
||||||
|
"current_user_saved_tracks": "Tracks",
|
||||||
|
"current_user_saved_shows": "Podcasts",
|
||||||
|
"current_user_recently_played": "Recently played",
|
||||||
|
"current_user_top_artists": "Top Artists",
|
||||||
|
"current_user_top_tracks": "Top Tracks",
|
||||||
|
"categories": "Categories",
|
||||||
"featured_playlists": "Featured Playlists",
|
"featured_playlists": "Featured Playlists",
|
||||||
"new_releases": "New Releases",
|
"new_releases": "New Releases",
|
||||||
"current_user_top_artists": "Top Artists",
|
|
||||||
"current_user_recently_played": "Recently played",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -233,7 +244,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
|||||||
or not self._currently_playing["item"]["album"]["images"]
|
or not self._currently_playing["item"]["album"]["images"]
|
||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
return self._currently_playing["item"]["album"]["images"][0]["url"]
|
return fetch_image_url(self._currently_playing["item"]["album"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_image_remotely_accessible(self) -> bool:
|
def media_image_remotely_accessible(self) -> bool:
|
||||||
@ -338,7 +349,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
|||||||
# Yet, they do generate those types of URI in their official clients.
|
# Yet, they do generate those types of URI in their official clients.
|
||||||
media_id = str(URL(media_id).with_query(None).with_fragment(None))
|
media_id = str(URL(media_id).with_query(None).with_fragment(None))
|
||||||
|
|
||||||
if media_type in (MEDIA_TYPE_TRACK, MEDIA_TYPE_MUSIC):
|
if media_type in (MEDIA_TYPE_TRACK, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MUSIC):
|
||||||
kwargs["uris"] = [media_id]
|
kwargs["uris"] = [media_id]
|
||||||
elif media_type in PLAYABLE_MEDIA_TYPES:
|
elif media_type in PLAYABLE_MEDIA_TYPES:
|
||||||
kwargs["context_uri"] = media_id
|
kwargs["context_uri"] = media_id
|
||||||
@ -346,6 +357,9 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
|||||||
_LOGGER.error("Media type %s is not supported", media_type)
|
_LOGGER.error("Media type %s is not supported", media_type)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not self._currently_playing.get("device") and self._devices:
|
||||||
|
kwargs["device_id"] = self._devices[0].get("id")
|
||||||
|
|
||||||
self._spotify.start_playback(**kwargs)
|
self._spotify.start_playback(**kwargs)
|
||||||
|
|
||||||
@spotify_exception_handler
|
@spotify_exception_handler
|
||||||
@ -388,6 +402,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
|||||||
|
|
||||||
async def async_browse_media(self, media_content_type=None, media_content_id=None):
|
async def async_browse_media(self, media_content_type=None, media_content_id=None):
|
||||||
"""Implement the websocket media browsing helper."""
|
"""Implement the websocket media browsing helper."""
|
||||||
|
|
||||||
if not self._scope_ok:
|
if not self._scope_ok:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@ -399,7 +414,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
|||||||
"media_content_id": media_content_id,
|
"media_content_id": media_content_id,
|
||||||
}
|
}
|
||||||
response = await self.hass.async_add_executor_job(
|
response = await self.hass.async_add_executor_job(
|
||||||
build_item_response, self._spotify, payload
|
build_item_response, self._spotify, self._me, payload
|
||||||
)
|
)
|
||||||
if response is None:
|
if response is None:
|
||||||
raise BrowseError(
|
raise BrowseError(
|
||||||
@ -408,34 +423,72 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def build_item_response(spotify, payload):
|
def build_item_response(spotify, user, payload):
|
||||||
"""Create response payload for the provided media query."""
|
"""Create response payload for the provided media query."""
|
||||||
media_content_type = payload.get("media_content_type")
|
media_content_type = payload.get("media_content_type")
|
||||||
|
media_content_id = payload.get("media_content_id")
|
||||||
title = None
|
title = None
|
||||||
if media_content_type == "user_playlists":
|
image = None
|
||||||
|
if media_content_type == "current_user_playlists":
|
||||||
media = spotify.current_user_playlists(limit=BROWSE_LIMIT)
|
media = spotify.current_user_playlists(limit=BROWSE_LIMIT)
|
||||||
items = media.get("items", [])
|
items = media.get("items", [])
|
||||||
|
elif media_content_type == "current_user_followed_artists":
|
||||||
|
media = spotify.current_user_followed_artists(limit=BROWSE_LIMIT)
|
||||||
|
items = media.get("artists", {}).get("items", [])
|
||||||
|
elif media_content_type == "current_user_saved_albums":
|
||||||
|
media = spotify.current_user_saved_albums(limit=BROWSE_LIMIT)
|
||||||
|
items = media.get("items", [])
|
||||||
|
elif media_content_type == "current_user_saved_tracks":
|
||||||
|
media = spotify.current_user_saved_tracks(limit=BROWSE_LIMIT)
|
||||||
|
items = media.get("items", [])
|
||||||
|
elif media_content_type == "current_user_saved_shows":
|
||||||
|
media = spotify.current_user_saved_shows(limit=BROWSE_LIMIT)
|
||||||
|
items = media.get("items", [])
|
||||||
elif media_content_type == "current_user_recently_played":
|
elif media_content_type == "current_user_recently_played":
|
||||||
media = spotify.current_user_recently_played(limit=BROWSE_LIMIT)
|
media = spotify.current_user_recently_played(limit=BROWSE_LIMIT)
|
||||||
items = media.get("items", [])
|
items = media.get("items", [])
|
||||||
elif media_content_type == "featured_playlists":
|
|
||||||
media = spotify.featured_playlists(limit=BROWSE_LIMIT)
|
|
||||||
items = media.get("playlists", {}).get("items", [])
|
|
||||||
elif media_content_type == "current_user_top_artists":
|
elif media_content_type == "current_user_top_artists":
|
||||||
media = spotify.current_user_top_artists(limit=BROWSE_LIMIT)
|
media = spotify.current_user_top_artists(limit=BROWSE_LIMIT)
|
||||||
items = media.get("items", [])
|
items = media.get("items", [])
|
||||||
|
elif media_content_type == "current_user_top_tracks":
|
||||||
|
media = spotify.current_user_top_tracks(limit=BROWSE_LIMIT)
|
||||||
|
items = media.get("items", [])
|
||||||
|
elif media_content_type == "featured_playlists":
|
||||||
|
media = spotify.featured_playlists(country=user["country"], limit=BROWSE_LIMIT)
|
||||||
|
items = media.get("playlists", {}).get("items", [])
|
||||||
|
elif media_content_type == "categories":
|
||||||
|
media = spotify.categories(country=user["country"], limit=BROWSE_LIMIT)
|
||||||
|
items = media.get("categories", {}).get("items", [])
|
||||||
|
elif media_content_type == "category_playlists":
|
||||||
|
media = spotify.category_playlists(
|
||||||
|
category_id=media_content_id,
|
||||||
|
country=user["country"],
|
||||||
|
limit=BROWSE_LIMIT,
|
||||||
|
)
|
||||||
|
category = spotify.category(media_content_id, country=user["country"])
|
||||||
|
title = category.get("name")
|
||||||
|
image = fetch_image_url(category, key="icons")
|
||||||
|
items = media.get("playlists", {}).get("items", [])
|
||||||
elif media_content_type == "new_releases":
|
elif media_content_type == "new_releases":
|
||||||
media = spotify.new_releases(limit=BROWSE_LIMIT)
|
media = spotify.new_releases(country=user["country"], limit=BROWSE_LIMIT)
|
||||||
items = media.get("albums", {}).get("items", [])
|
items = media.get("albums", {}).get("items", [])
|
||||||
elif media_content_type == MEDIA_TYPE_PLAYLIST:
|
elif media_content_type == MEDIA_TYPE_PLAYLIST:
|
||||||
media = spotify.playlist(payload["media_content_id"])
|
media = spotify.playlist(media_content_id)
|
||||||
items = media.get("tracks", {}).get("items", [])
|
items = media.get("tracks", {}).get("items", [])
|
||||||
elif media_content_type == MEDIA_TYPE_ALBUM:
|
elif media_content_type == MEDIA_TYPE_ALBUM:
|
||||||
media = spotify.album(payload["media_content_id"])
|
media = spotify.album(media_content_id)
|
||||||
items = media.get("tracks", {}).get("items", [])
|
items = media.get("tracks", {}).get("items", [])
|
||||||
elif media_content_type == MEDIA_TYPE_ARTIST:
|
elif media_content_type == MEDIA_TYPE_ARTIST:
|
||||||
media = spotify.artist_albums(payload["media_content_id"], limit=BROWSE_LIMIT)
|
media = spotify.artist_albums(media_content_id, limit=BROWSE_LIMIT)
|
||||||
title = spotify.artist(payload["media_content_id"]).get("name")
|
artist = spotify.artist(media_content_id)
|
||||||
|
title = artist.get("name")
|
||||||
|
image = fetch_image_url(artist)
|
||||||
|
items = media.get("items", [])
|
||||||
|
elif media_content_type == MEDIA_TYPE_SHOW:
|
||||||
|
media = spotify.show_episodes(media_content_id, limit=BROWSE_LIMIT)
|
||||||
|
show = spotify.show(media_content_id)
|
||||||
|
title = show.get("name")
|
||||||
|
image = fetch_image_url(show)
|
||||||
items = media.get("items", [])
|
items = media.get("items", [])
|
||||||
else:
|
else:
|
||||||
media = None
|
media = None
|
||||||
@ -444,6 +497,26 @@ def build_item_response(spotify, payload):
|
|||||||
if media is None:
|
if media is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if media_content_type == "categories":
|
||||||
|
return BrowseMedia(
|
||||||
|
title=LIBRARY_MAP.get(media_content_id),
|
||||||
|
media_content_id=media_content_id,
|
||||||
|
media_content_type=media_content_type,
|
||||||
|
can_play=False,
|
||||||
|
can_expand=True,
|
||||||
|
children=[
|
||||||
|
BrowseMedia(
|
||||||
|
title=item.get("name"),
|
||||||
|
media_content_id=item.get("id"),
|
||||||
|
media_content_type="category_playlists",
|
||||||
|
thumbnail=fetch_image_url(item, key="icons"),
|
||||||
|
can_play=False,
|
||||||
|
can_expand=True,
|
||||||
|
)
|
||||||
|
for item in items
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
if title is None:
|
if title is None:
|
||||||
if "name" in media:
|
if "name" in media:
|
||||||
title = media.get("name")
|
title = media.get("name")
|
||||||
@ -452,15 +525,17 @@ def build_item_response(spotify, payload):
|
|||||||
|
|
||||||
response = {
|
response = {
|
||||||
"title": title,
|
"title": title,
|
||||||
"media_content_id": payload.get("media_content_id"),
|
"media_content_id": media_content_id,
|
||||||
"media_content_type": payload.get("media_content_type"),
|
"media_content_type": media_content_type,
|
||||||
"can_play": payload.get("media_content_type") in PLAYABLE_MEDIA_TYPES,
|
"can_play": media_content_type in PLAYABLE_MEDIA_TYPES,
|
||||||
"children": [item_payload(item) for item in items],
|
"children": [item_payload(item) for item in items],
|
||||||
"can_expand": True,
|
"can_expand": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
if "images" in media:
|
if "images" in media:
|
||||||
response["thumbnail"] = fetch_image_url(media)
|
response["thumbnail"] = fetch_image_url(media)
|
||||||
|
elif image:
|
||||||
|
response["thumbnail"] = image
|
||||||
|
|
||||||
return BrowseMedia(**response)
|
return BrowseMedia(**response)
|
||||||
|
|
||||||
@ -471,31 +546,35 @@ def item_payload(item):
|
|||||||
|
|
||||||
Used by async_browse_media.
|
Used by async_browse_media.
|
||||||
"""
|
"""
|
||||||
can_expand = item.get("type") not in [None, MEDIA_TYPE_TRACK]
|
if MEDIA_TYPE_TRACK in item:
|
||||||
|
item = item.get(MEDIA_TYPE_TRACK)
|
||||||
|
elif MEDIA_TYPE_SHOW in item:
|
||||||
|
item = item.get(MEDIA_TYPE_SHOW)
|
||||||
|
elif MEDIA_TYPE_ARTIST in item:
|
||||||
|
item = item.get(MEDIA_TYPE_ARTIST)
|
||||||
|
elif MEDIA_TYPE_ALBUM in item and item.get("type") != MEDIA_TYPE_TRACK:
|
||||||
|
item = item.get(MEDIA_TYPE_ALBUM)
|
||||||
|
|
||||||
if (
|
can_expand = item.get("type") not in [
|
||||||
MEDIA_TYPE_TRACK in item
|
None,
|
||||||
or item.get("type") != MEDIA_TYPE_ALBUM
|
MEDIA_TYPE_TRACK,
|
||||||
and "playlists" in item
|
MEDIA_TYPE_EPISODE,
|
||||||
):
|
]
|
||||||
track = item.get(MEDIA_TYPE_TRACK)
|
|
||||||
return BrowseMedia(
|
|
||||||
title=track.get("name"),
|
|
||||||
thumbnail=fetch_image_url(track.get(MEDIA_TYPE_ALBUM, {})),
|
|
||||||
media_content_id=track.get("uri"),
|
|
||||||
media_content_type=MEDIA_TYPE_TRACK,
|
|
||||||
can_play=True,
|
|
||||||
can_expand=can_expand,
|
|
||||||
)
|
|
||||||
|
|
||||||
return BrowseMedia(
|
payload = {
|
||||||
title=item.get("name"),
|
"title": item.get("name"),
|
||||||
thumbnail=fetch_image_url(item),
|
"media_content_id": item.get("uri"),
|
||||||
media_content_id=item.get("uri"),
|
"media_content_type": item.get("type"),
|
||||||
media_content_type=item.get("type"),
|
"can_play": item.get("type") in PLAYABLE_MEDIA_TYPES,
|
||||||
can_play=item.get("type") in PLAYABLE_MEDIA_TYPES,
|
"can_expand": can_expand,
|
||||||
can_expand=can_expand,
|
}
|
||||||
)
|
|
||||||
|
if "images" in item:
|
||||||
|
payload["thumbnail"] = fetch_image_url(item)
|
||||||
|
elif MEDIA_TYPE_ALBUM in item:
|
||||||
|
payload["thumbnail"] = fetch_image_url(item[MEDIA_TYPE_ALBUM])
|
||||||
|
|
||||||
|
return BrowseMedia(**payload)
|
||||||
|
|
||||||
|
|
||||||
def library_payload():
|
def library_payload():
|
||||||
@ -522,9 +601,9 @@ def library_payload():
|
|||||||
return library_info
|
return library_info
|
||||||
|
|
||||||
|
|
||||||
def fetch_image_url(item):
|
def fetch_image_url(item, key="images"):
|
||||||
"""Fetch image url."""
|
"""Fetch image url."""
|
||||||
try:
|
try:
|
||||||
return item.get("images", [])[0].get("url")
|
return item.get(key, [])[0].get("url")
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return
|
return None
|
||||||
|
@ -2049,7 +2049,7 @@ spiderpy==1.3.1
|
|||||||
spotcrime==1.0.4
|
spotcrime==1.0.4
|
||||||
|
|
||||||
# homeassistant.components.spotify
|
# homeassistant.components.spotify
|
||||||
spotipy==2.12.0
|
spotipy==2.14.0
|
||||||
|
|
||||||
# homeassistant.components.recorder
|
# homeassistant.components.recorder
|
||||||
# homeassistant.components.sql
|
# homeassistant.components.sql
|
||||||
|
@ -954,7 +954,7 @@ speedtest-cli==2.1.2
|
|||||||
spiderpy==1.3.1
|
spiderpy==1.3.1
|
||||||
|
|
||||||
# homeassistant.components.spotify
|
# homeassistant.components.spotify
|
||||||
spotipy==2.12.0
|
spotipy==2.14.0
|
||||||
|
|
||||||
# homeassistant.components.recorder
|
# homeassistant.components.recorder
|
||||||
# homeassistant.components.sql
|
# homeassistant.components.sql
|
||||||
|
Loading…
x
Reference in New Issue
Block a user