Bump spotifyaio to 2.0.2 (#164114)

Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
Joost Lekkerkerker
2026-03-06 20:28:04 +01:00
committed by GitHub
co-authored by Robert Resch
parent ffca43027f
commit 4bcea27151
17 changed files with 94 additions and 589 deletions
@@ -118,7 +118,6 @@ class BrowsableMedia(StrEnum):
CURRENT_USER_RECENTLY_PLAYED = "current_user_recently_played" CURRENT_USER_RECENTLY_PLAYED = "current_user_recently_played"
CURRENT_USER_TOP_ARTISTS = "current_user_top_artists" CURRENT_USER_TOP_ARTISTS = "current_user_top_artists"
CURRENT_USER_TOP_TRACKS = "current_user_top_tracks" CURRENT_USER_TOP_TRACKS = "current_user_top_tracks"
NEW_RELEASES = "new_releases"
LIBRARY_MAP = { LIBRARY_MAP = {
@@ -130,7 +129,6 @@ LIBRARY_MAP = {
BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED.value: "Recently played", BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED.value: "Recently played",
BrowsableMedia.CURRENT_USER_TOP_ARTISTS.value: "Top Artists", BrowsableMedia.CURRENT_USER_TOP_ARTISTS.value: "Top Artists",
BrowsableMedia.CURRENT_USER_TOP_TRACKS.value: "Top Tracks", BrowsableMedia.CURRENT_USER_TOP_TRACKS.value: "Top Tracks",
BrowsableMedia.NEW_RELEASES.value: "New Releases",
} }
CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = { CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = {
@@ -166,10 +164,6 @@ CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = {
"parent": MediaClass.DIRECTORY, "parent": MediaClass.DIRECTORY,
"children": MediaClass.TRACK, "children": MediaClass.TRACK,
}, },
BrowsableMedia.NEW_RELEASES.value: {
"parent": MediaClass.DIRECTORY,
"children": MediaClass.ALBUM,
},
MediaType.PLAYLIST: { MediaType.PLAYLIST: {
"parent": MediaClass.PLAYLIST, "parent": MediaClass.PLAYLIST,
"children": MediaClass.TRACK, "children": MediaClass.TRACK,
@@ -356,14 +350,11 @@ async def build_item_response( # noqa: C901
elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_TRACKS: elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_TRACKS:
if top_tracks := await spotify.get_top_tracks(): if top_tracks := await spotify.get_top_tracks():
items = [_get_track_item_payload(track) for track in top_tracks] items = [_get_track_item_payload(track) for track in top_tracks]
elif media_content_type == BrowsableMedia.NEW_RELEASES:
if new_releases := await spotify.get_new_releases():
items = [_get_album_item_payload(album) for album in new_releases]
elif media_content_type == MediaType.PLAYLIST: elif media_content_type == MediaType.PLAYLIST:
if playlist := await spotify.get_playlist(media_content_id): if playlist := await spotify.get_playlist(media_content_id):
title = playlist.name title = playlist.name
image = playlist.images[0].url if playlist.images else None image = playlist.images[0].url if playlist.images else None
for playlist_item in playlist.tracks.items: for playlist_item in playlist.items.items:
if playlist_item.track.type is ItemType.TRACK: if playlist_item.track.type is ItemType.TRACK:
if TYPE_CHECKING: if TYPE_CHECKING:
assert isinstance(playlist_item.track, Track) assert isinstance(playlist_item.track, Track)
@@ -6,7 +6,7 @@ from collections.abc import Mapping
import logging import logging
from typing import Any from typing import Any
from spotifyaio import SpotifyClient from spotifyaio import SpotifyClient, SpotifyForbiddenError
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, CONF_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, CONF_TOKEN
@@ -41,6 +41,9 @@ class SpotifyFlowHandler(
try: try:
current_user = await spotify.get_current_user() current_user = await spotify.get_current_user()
except SpotifyForbiddenError:
self.logger.exception("User is not subscribed to Spotify")
return self.async_abort(reason="user_not_premium")
except Exception: except Exception:
self.logger.exception("Error while connecting to Spotify") self.logger.exception("Error while connecting to Spotify")
return self.async_abort(reason="connection_error") return self.async_abort(reason="connection_error")
@@ -11,12 +11,15 @@ from spotifyaio import (
Playlist, Playlist,
SpotifyClient, SpotifyClient,
SpotifyConnectionError, SpotifyConnectionError,
SpotifyForbiddenError,
SpotifyNotFoundError, SpotifyNotFoundError,
UserProfile, UserProfile,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@@ -33,6 +36,11 @@ type SpotifyConfigEntry = ConfigEntry[SpotifyData]
UPDATE_INTERVAL = timedelta(seconds=30) UPDATE_INTERVAL = timedelta(seconds=30)
FREE_API_BLOGPOST = (
"https://developer.spotify.com/blog/"
"2026-02-06-update-on-developer-access-and-platform-security"
)
@dataclass @dataclass
class SpotifyCoordinatorData: class SpotifyCoordinatorData:
@@ -78,6 +86,19 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]):
"""Set up the coordinator.""" """Set up the coordinator."""
try: try:
self.current_user = await self.client.get_current_user() self.current_user = await self.client.get_current_user()
except SpotifyForbiddenError as err:
async_create_issue(
self.hass,
DOMAIN,
f"user_not_premium_{self.config_entry.unique_id}",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.ERROR,
translation_key="user_not_premium",
translation_placeholders={"entry_title": self.config_entry.title},
learn_more_url=FREE_API_BLOGPOST,
)
raise ConfigEntryError("User is not subscribed to Spotify") from err
except SpotifyConnectionError as err: except SpotifyConnectionError as err:
raise UpdateFailed("Error communicating with Spotify API") from err raise UpdateFailed("Error communicating with Spotify API") from err
@@ -8,5 +8,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["spotifyaio"], "loggers": ["spotifyaio"],
"requirements": ["spotifyaio==1.0.0"] "requirements": ["spotifyaio==2.0.2"]
} }
@@ -14,10 +14,10 @@ from spotifyaio import (
Item, Item,
ItemType, ItemType,
PlaybackState, PlaybackState,
ProductType,
RepeatMode as SpotifyRepeatMode, RepeatMode as SpotifyRepeatMode,
Track, Track,
) )
from spotifyaio.models import ProductType
from yarl import URL from yarl import URL
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
@@ -222,7 +222,7 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
if item.type == ItemType.EPISODE: if item.type == ItemType.EPISODE:
if TYPE_CHECKING: if TYPE_CHECKING:
assert isinstance(item, Episode) assert isinstance(item, Episode)
return item.show.publisher return item.show.name
if TYPE_CHECKING: if TYPE_CHECKING:
assert isinstance(item, Track) assert isinstance(item, Track)
@@ -230,12 +230,10 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
@property @property
@ensure_item @ensure_item
def media_album_name(self, item: Item) -> str: # noqa: PLR0206 def media_album_name(self, item: Item) -> str | None: # noqa: PLR0206
"""Return the media album.""" """Return the media album."""
if item.type == ItemType.EPISODE: if item.type == ItemType.EPISODE:
if TYPE_CHECKING: return None
assert isinstance(item, Episode)
return item.show.name
if TYPE_CHECKING: if TYPE_CHECKING:
assert isinstance(item, Track) assert isinstance(item, Track)
@@ -12,7 +12,8 @@
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"reauth_account_mismatch": "The Spotify account authenticated with does not match the account that needed re-authentication.", "reauth_account_mismatch": "The Spotify account authenticated with does not match the account that needed re-authentication.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"user_not_premium": "The Spotify API has been changed and Developer applications created with a free account can no longer access the API. To continue using the Spotify integration, you should use an Spotify Developer application created with a Spotify Premium account, or upgrade to Spotify Premium."
}, },
"create_entry": { "create_entry": {
"default": "Successfully authenticated with Spotify." "default": "Successfully authenticated with Spotify."
@@ -41,6 +42,12 @@
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]" "message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
} }
}, },
"issues": {
"user_not_premium": {
"description": "[%key:component::spotify::config::abort::user_not_premium%]",
"title": "Spotify integration requires a Spotify Premium account"
}
},
"system_health": { "system_health": {
"info": { "info": {
"api_endpoint_reachable": "Spotify API endpoint reachable" "api_endpoint_reachable": "Spotify API endpoint reachable"
+1 -1
View File
@@ -2978,7 +2978,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3 speedtest-cli==2.1.3
# homeassistant.components.spotify # homeassistant.components.spotify
spotifyaio==1.0.0 spotifyaio==2.0.2
# homeassistant.components.sql # homeassistant.components.sql
sqlparse==0.5.5 sqlparse==0.5.5
+1 -1
View File
@@ -2514,7 +2514,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3 speedtest-cli==2.1.3
# homeassistant.components.spotify # homeassistant.components.spotify
spotifyaio==1.0.0 spotifyaio==2.0.2
# homeassistant.components.sql # homeassistant.components.sql
sqlparse==0.5.5 sqlparse==0.5.5
-4
View File
@@ -10,7 +10,6 @@ from spotifyaio.models import (
Artist, Artist,
Devices, Devices,
FollowedArtistResponse, FollowedArtistResponse,
NewReleasesResponse,
NewReleasesResponseInner, NewReleasesResponseInner,
PlaybackState, PlaybackState,
PlayedTrackResponse, PlayedTrackResponse,
@@ -142,9 +141,6 @@ def mock_spotify() -> Generator[AsyncMock]:
client.get_followed_artists.return_value = FollowedArtistResponse.from_json( client.get_followed_artists.return_value = FollowedArtistResponse.from_json(
load_fixture("followed_artists.json", DOMAIN) load_fixture("followed_artists.json", DOMAIN)
).artists.items ).artists.items
client.get_new_releases.return_value = NewReleasesResponse.from_json(
load_fixture("new_releases.json", DOMAIN)
).albums.items
client.get_devices.return_value = Devices.from_json( client.get_devices.return_value = Devices.from_json(
load_fixture("devices.json", DOMAIN) load_fixture("devices.json", DOMAIN)
).devices ).devices
@@ -1,469 +0,0 @@
{
"albums": {
"href": "https://api.spotify.com/v1/browse/new-releases?offset=0&limit=20&locale=en-US,en;q%3D0.5",
"items": [
{
"album_type": "album",
"artists": [
{
"external_urls": {
"spotify": "https://open.spotify.com/artist/4gzpq5DPGxSnKTe4SA8HAU"
},
"href": "https://api.spotify.com/v1/artists/4gzpq5DPGxSnKTe4SA8HAU",
"id": "4gzpq5DPGxSnKTe4SA8HAU",
"name": "Coldplay",
"type": "artist",
"uri": "spotify:artist:4gzpq5DPGxSnKTe4SA8HAU"
}
],
"available_markets": [
"AR",
"AU",
"AT",
"BE",
"BO",
"BR",
"BG",
"CA",
"CL",
"CO",
"CR",
"CY",
"CZ",
"DK",
"DO",
"DE",
"EC",
"EE",
"SV",
"FI",
"FR",
"GR",
"GT",
"HN",
"HK",
"HU",
"IS",
"IE",
"IT",
"LV",
"LT",
"LU",
"MY",
"MT",
"MX",
"NL",
"NZ",
"NI",
"NO",
"PA",
"PY",
"PE",
"PH",
"PL",
"PT",
"SG",
"SK",
"ES",
"SE",
"CH",
"TW",
"TR",
"UY",
"US",
"GB",
"AD",
"LI",
"MC",
"ID",
"JP",
"TH",
"VN",
"RO",
"IL",
"ZA",
"SA",
"AE",
"BH",
"QA",
"OM",
"KW",
"EG",
"MA",
"DZ",
"TN",
"LB",
"JO",
"PS",
"IN",
"KZ",
"MD",
"UA",
"AL",
"BA",
"HR",
"ME",
"MK",
"RS",
"SI",
"KR",
"BD",
"PK",
"LK",
"GH",
"KE",
"NG",
"TZ",
"UG",
"AG",
"AM",
"BS",
"BB",
"BZ",
"BT",
"BW",
"BF",
"CV",
"CW",
"DM",
"FJ",
"GM",
"GE",
"GD",
"GW",
"GY",
"HT",
"JM",
"KI",
"LS",
"LR",
"MW",
"MV",
"ML",
"MH",
"FM",
"NA",
"NR",
"NE",
"PW",
"PG",
"PR",
"WS",
"SM",
"ST",
"SN",
"SC",
"SL",
"SB",
"KN",
"LC",
"VC",
"SR",
"TL",
"TO",
"TT",
"TV",
"VU",
"AZ",
"BN",
"BI",
"KH",
"CM",
"TD",
"KM",
"GQ",
"SZ",
"GA",
"GN",
"KG",
"LA",
"MO",
"MR",
"MN",
"NP",
"RW",
"TG",
"UZ",
"ZW",
"BJ",
"MG",
"MU",
"MZ",
"AO",
"CI",
"DJ",
"ZM",
"CD",
"CG",
"IQ",
"LY",
"TJ",
"VE",
"ET",
"XK"
],
"external_urls": {
"spotify": "https://open.spotify.com/album/5SGtrmYbIo0Dsg4kJ4qjM6"
},
"href": "https://api.spotify.com/v1/albums/5SGtrmYbIo0Dsg4kJ4qjM6",
"id": "5SGtrmYbIo0Dsg4kJ4qjM6",
"images": [
{
"height": 300,
"url": "https://i.scdn.co/image/ab67616d00001e0209ba52a5116e0c3e8461f58b",
"width": 300
},
{
"height": 64,
"url": "https://i.scdn.co/image/ab67616d0000485109ba52a5116e0c3e8461f58b",
"width": 64
},
{
"height": 640,
"url": "https://i.scdn.co/image/ab67616d0000b27309ba52a5116e0c3e8461f58b",
"width": 640
}
],
"name": "Moon Music",
"release_date": "2024-10-04",
"release_date_precision": "day",
"total_tracks": 10,
"type": "album",
"uri": "spotify:album:5SGtrmYbIo0Dsg4kJ4qjM6"
},
{
"album_type": "album",
"artists": [
{
"external_urls": {
"spotify": "https://open.spotify.com/artist/4U9nsRTH2mr9L4UXEWqG5e"
},
"href": "https://api.spotify.com/v1/artists/4U9nsRTH2mr9L4UXEWqG5e",
"id": "4U9nsRTH2mr9L4UXEWqG5e",
"name": "Bente",
"type": "artist",
"uri": "spotify:artist:4U9nsRTH2mr9L4UXEWqG5e"
}
],
"available_markets": [
"AR",
"AU",
"AT",
"BE",
"BO",
"BR",
"BG",
"CA",
"CL",
"CO",
"CR",
"CY",
"CZ",
"DK",
"DO",
"DE",
"EC",
"EE",
"SV",
"FI",
"FR",
"GR",
"GT",
"HN",
"HK",
"HU",
"IS",
"IE",
"IT",
"LV",
"LT",
"LU",
"MY",
"MT",
"MX",
"NL",
"NZ",
"NI",
"NO",
"PA",
"PY",
"PE",
"PH",
"PL",
"PT",
"SG",
"SK",
"ES",
"SE",
"CH",
"TW",
"TR",
"UY",
"US",
"GB",
"AD",
"LI",
"MC",
"ID",
"JP",
"TH",
"VN",
"RO",
"IL",
"ZA",
"SA",
"AE",
"BH",
"QA",
"OM",
"KW",
"EG",
"MA",
"DZ",
"TN",
"LB",
"JO",
"PS",
"IN",
"KZ",
"MD",
"UA",
"AL",
"BA",
"HR",
"ME",
"MK",
"RS",
"SI",
"KR",
"BD",
"PK",
"LK",
"GH",
"KE",
"NG",
"TZ",
"UG",
"AG",
"AM",
"BS",
"BB",
"BZ",
"BT",
"BW",
"BF",
"CV",
"CW",
"DM",
"FJ",
"GM",
"GE",
"GD",
"GW",
"GY",
"HT",
"JM",
"KI",
"LS",
"LR",
"MW",
"MV",
"ML",
"MH",
"FM",
"NA",
"NR",
"NE",
"PW",
"PG",
"WS",
"SM",
"ST",
"SN",
"SC",
"SL",
"SB",
"KN",
"LC",
"VC",
"SR",
"TL",
"TO",
"TT",
"TV",
"VU",
"AZ",
"BN",
"BI",
"KH",
"CM",
"TD",
"KM",
"GQ",
"SZ",
"GA",
"GN",
"KG",
"LA",
"MO",
"MR",
"MN",
"NP",
"RW",
"TG",
"UZ",
"ZW",
"BJ",
"MG",
"MU",
"MZ",
"AO",
"CI",
"DJ",
"ZM",
"CD",
"CG",
"IQ",
"LY",
"TJ",
"VE",
"ET",
"XK"
],
"external_urls": {
"spotify": "https://open.spotify.com/album/713lZ7AF55fEFSQgcttj9y"
},
"href": "https://api.spotify.com/v1/albums/713lZ7AF55fEFSQgcttj9y",
"id": "713lZ7AF55fEFSQgcttj9y",
"images": [
{
"height": 300,
"url": "https://i.scdn.co/image/ab67616d00001e02ab9953b1d18f8233f6b26027",
"width": 300
},
{
"height": 64,
"url": "https://i.scdn.co/image/ab67616d00004851ab9953b1d18f8233f6b26027",
"width": 64
},
{
"height": 640,
"url": "https://i.scdn.co/image/ab67616d0000b273ab9953b1d18f8233f6b26027",
"width": 640
}
],
"name": "drift",
"release_date": "2024-10-03",
"release_date_precision": "day",
"total_tracks": 14,
"type": "album",
"uri": "spotify:album:713lZ7AF55fEFSQgcttj9y"
}
],
"limit": 20,
"next": "https://api.spotify.com/v1/browse/new-releases?offset=20&limit=20&locale=en-US,en;q%3D0.5",
"offset": 0,
"previous": null,
"total": 100
}
}
@@ -108,21 +108,7 @@
'width': None, 'width': None,
}), }),
]), ]),
'name': 'Spotify Web API Testing playlist', 'items': dict({
'object_type': 'playlist',
'owner': dict({
'display_name': 'JMPerez²',
'external_urls': dict({
'spotify': 'https://open.spotify.com/user/jmperezperez',
}),
'href': 'https://api.spotify.com/v1/users/jmperezperez',
'object_type': 'user',
'owner_id': 'jmperezperez',
'uri': 'spotify:user:jmperezperez',
}),
'playlist_id': '3cEYpjA9oz9GiPac4AsH4n',
'public': True,
'tracks': dict({
'items': list([ 'items': list([
dict({ dict({
'added_at': '2015-01-15T12:39:22+00:00', 'added_at': '2015-01-15T12:39:22+00:00',
@@ -517,7 +503,6 @@
}), }),
]), ]),
'name': 'Safety Third', 'name': 'Safety Third',
'publisher': 'Safety Third ',
'show_id': '1Y9ExMgMxoBVrgrfU7u0nD', 'show_id': '1Y9ExMgMxoBVrgrfU7u0nD',
'total_episodes': 120, 'total_episodes': 120,
'uri': 'spotify:show:1Y9ExMgMxoBVrgrfU7u0nD', 'uri': 'spotify:show:1Y9ExMgMxoBVrgrfU7u0nD',
@@ -528,6 +513,20 @@
}), }),
]), ]),
}), }),
'name': 'Spotify Web API Testing playlist',
'object_type': 'playlist',
'owner': dict({
'display_name': 'JMPerez²',
'external_urls': dict({
'spotify': 'https://open.spotify.com/user/jmperezperez',
}),
'href': 'https://api.spotify.com/v1/users/jmperezperez',
'object_type': 'user',
'owner_id': 'jmperezperez',
'uri': 'spotify:user:jmperezperez',
}),
'playlist_id': '3cEYpjA9oz9GiPac4AsH4n',
'public': True,
'uri': 'spotify:playlist:3cEYpjA9oz9GiPac4AsH4n', 'uri': 'spotify:playlist:3cEYpjA9oz9GiPac4AsH4n',
}), }),
}), }),
@@ -93,17 +93,6 @@
'thumbnail': None, 'thumbnail': None,
'title': 'Top Tracks', 'title': 'Top Tracks',
}), }),
dict({
'can_expand': True,
'can_play': False,
'can_search': False,
'children_media_class': <MediaClass.ALBUM: 'album'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/new_releases',
'media_content_type': 'spotify://new_releases',
'thumbnail': None,
'title': 'New Releases',
}),
]), ]),
'children_media_class': <MediaClass.DIRECTORY: 'directory'>, 'children_media_class': <MediaClass.DIRECTORY: 'directory'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>, 'media_class': <MediaClass.DIRECTORY: 'directory'>,
@@ -608,44 +597,6 @@
'title': 'Top Tracks', 'title': 'Top Tracks',
}) })
# --- # ---
# name: test_browsing[new_releases-new_releases]
dict({
'can_expand': True,
'can_play': False,
'can_search': False,
'children': list([
dict({
'can_expand': True,
'can_play': True,
'can_search': False,
'children_media_class': <MediaClass.TRACK: 'track'>,
'media_class': <MediaClass.ALBUM: 'album'>,
'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/spotify:album:5SGtrmYbIo0Dsg4kJ4qjM6',
'media_content_type': 'spotify://album',
'thumbnail': 'https://i.scdn.co/image/ab67616d00001e0209ba52a5116e0c3e8461f58b',
'title': 'Moon Music',
}),
dict({
'can_expand': True,
'can_play': True,
'can_search': False,
'children_media_class': <MediaClass.TRACK: 'track'>,
'media_class': <MediaClass.ALBUM: 'album'>,
'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/spotify:album:713lZ7AF55fEFSQgcttj9y',
'media_content_type': 'spotify://album',
'thumbnail': 'https://i.scdn.co/image/ab67616d00001e02ab9953b1d18f8233f6b26027',
'title': 'drift',
}),
]),
'children_media_class': <MediaClass.ALBUM: 'album'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/new_releases',
'media_content_type': 'spotify://new_releases',
'not_shown': 0,
'thumbnail': None,
'title': 'New Releases',
})
# ---
# name: test_browsing[playlist-spotify:playlist:3cEYpjA9oz9GiPac4AsH4n] # name: test_browsing[playlist-spotify:playlist:3cEYpjA9oz9GiPac4AsH4n]
dict({ dict({
'can_expand': True, 'can_expand': True,
@@ -116,8 +116,7 @@
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'entity_picture': '/api/media_player_proxy/media_player.spotify_spotify_1?token=mock-token&cache=cf1e6e1e830f08d3', 'entity_picture': '/api/media_player_proxy/media_player.spotify_spotify_1?token=mock-token&cache=cf1e6e1e830f08d3',
'friendly_name': 'Spotify spotify_1', 'friendly_name': 'Spotify spotify_1',
'media_album_name': 'Safety Third', 'media_artist': 'Safety Third',
'media_artist': 'Safety Third ',
'media_content_id': 'spotify:episode:3o0RYoo5iOMKSmEbunsbvW', 'media_content_id': 'spotify:episode:3o0RYoo5iOMKSmEbunsbvW',
'media_content_type': <MediaType.PODCAST: 'podcast'>, 'media_content_type': <MediaType.PODCAST: 'podcast'>,
'media_duration': 3690, 'media_duration': 3690,
+12 -3
View File
@@ -4,7 +4,7 @@ from http import HTTPStatus
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
from spotifyaio import SpotifyConnectionError from spotifyaio import SpotifyConnectionError, SpotifyForbiddenError
from homeassistant.components.spotify.const import DOMAIN from homeassistant.components.spotify.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER from homeassistant.config_entries import SOURCE_USER
@@ -95,6 +95,13 @@ async def test_full_flow(
assert result["result"].unique_id == "1112264111" assert result["result"].unique_id == "1112264111"
@pytest.mark.parametrize(
("exception", "reason"),
[
(SpotifyConnectionError, "connection_error"),
(SpotifyForbiddenError, "user_not_premium"),
],
)
@pytest.mark.usefixtures("current_request_with_host") @pytest.mark.usefixtures("current_request_with_host")
@pytest.mark.usefixtures("setup_credentials") @pytest.mark.usefixtures("setup_credentials")
async def test_abort_if_spotify_error( async def test_abort_if_spotify_error(
@@ -102,6 +109,8 @@ async def test_abort_if_spotify_error(
hass_client_no_auth: ClientSessionGenerator, hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker, aioclient_mock: AiohttpClientMocker,
mock_spotify: MagicMock, mock_spotify: MagicMock,
exception: Exception,
reason: str,
) -> None: ) -> None:
"""Check Spotify errors causes flow to abort.""" """Check Spotify errors causes flow to abort."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@@ -128,12 +137,12 @@ async def test_abort_if_spotify_error(
}, },
) )
mock_spotify.return_value.get_current_user.side_effect = SpotifyConnectionError mock_spotify.return_value.get_current_user.side_effect = exception
result = await hass.config_entries.flow.async_configure(result["flow_id"]) result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "connection_error" assert result["reason"] == reason
@pytest.mark.usefixtures("current_request_with_host") @pytest.mark.usefixtures("current_request_with_host")
+23 -1
View File
@@ -3,10 +3,12 @@
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
from spotifyaio import SpotifyConnectionError from spotifyaio import SpotifyConnectionError, SpotifyForbiddenError
from homeassistant.components.spotify.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.config_entry_oauth2_flow import ( from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError, ImplementationUnavailableError,
) )
@@ -53,6 +55,26 @@ async def test_setup_with_required_calls_failing(
assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) assert not await hass.config_entries.async_setup(mock_config_entry.entry_id)
@pytest.mark.usefixtures("setup_credentials")
async def test_setup_free_account_is_failing(
hass: HomeAssistant,
mock_spotify: MagicMock,
mock_config_entry: MockConfigEntry,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test the Spotify setup with a free account is failing."""
mock_spotify.return_value.get_current_user.side_effect = SpotifyForbiddenError(
"Check settings on developer.spotify.com/dashboard, the user may not be registered."
)
mock_config_entry.add_to_hass(hass)
assert not await hass.config_entries.async_setup(mock_config_entry.entry_id)
issue = issue_registry.issues.get(
(DOMAIN, f"user_not_premium_{mock_config_entry.unique_id}")
)
assert issue, "Repair issue not created"
@pytest.mark.usefixtures("setup_credentials") @pytest.mark.usefixtures("setup_credentials")
async def test_oauth_implementation_not_available( async def test_oauth_implementation_not_available(
hass: HomeAssistant, hass: HomeAssistant,
@@ -66,7 +66,7 @@ async def test_browse_media_categories(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("config_entry_id"), [("01J5TX5A0FF6G5V0QJX6HBC94T"), ("32oesphrnacjcf7vw5bf6odx3")] "config_entry_id", ["01J5TX5A0FF6G5V0QJX6HBC94T", "32oesphrnacjcf7vw5bf6odx3"]
) )
@pytest.mark.usefixtures("setup_credentials") @pytest.mark.usefixtures("setup_credentials")
async def test_browse_media_playlists( async def test_browse_media_playlists(
@@ -112,7 +112,6 @@ async def test_browse_media_playlists(
("current_user_recently_played", "current_user_recently_played"), ("current_user_recently_played", "current_user_recently_played"),
("current_user_top_artists", "current_user_top_artists"), ("current_user_top_artists", "current_user_top_artists"),
("current_user_top_tracks", "current_user_top_tracks"), ("current_user_top_tracks", "current_user_top_tracks"),
("new_releases", "new_releases"),
("playlist", "spotify:playlist:3cEYpjA9oz9GiPac4AsH4n"), ("playlist", "spotify:playlist:3cEYpjA9oz9GiPac4AsH4n"),
("album", "spotify:album:3IqzqH6ShrRtie9Yd2ODyG"), ("album", "spotify:album:3IqzqH6ShrRtie9Yd2ODyG"),
("artist", "spotify:artist:0TnOYISbd1XYRBk9myaseg"), ("artist", "spotify:artist:0TnOYISbd1XYRBk9myaseg"),
@@ -138,13 +137,7 @@ async def test_browsing(
assert response.as_dict() == snapshot assert response.as_dict() == snapshot
@pytest.mark.parametrize( @pytest.mark.parametrize("media_content_id", ["artist", None])
("media_content_id"),
[
"artist",
None,
],
)
@pytest.mark.usefixtures("setup_credentials") @pytest.mark.usefixtures("setup_credentials")
async def test_invalid_spotify_url( async def test_invalid_spotify_url(
hass: HomeAssistant, hass: HomeAssistant,
@@ -7,7 +7,6 @@ from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from spotifyaio import ( from spotifyaio import (
PlaybackState, PlaybackState,
ProductType,
RepeatMode as SpotifyRepeatMode, RepeatMode as SpotifyRepeatMode,
SpotifyConnectionError, SpotifyConnectionError,
SpotifyNotFoundError, SpotifyNotFoundError,
@@ -108,20 +107,6 @@ async def test_podcast(
) )
@pytest.mark.usefixtures("setup_credentials")
async def test_free_account(
hass: HomeAssistant,
mock_spotify: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the Spotify entities with a free account."""
mock_spotify.return_value.get_current_user.return_value.product = ProductType.FREE
await setup_integration(hass, mock_config_entry)
state = hass.states.get("media_player.spotify_spotify_1")
assert state
assert state.attributes["supported_features"] == 0
@pytest.mark.usefixtures("setup_credentials") @pytest.mark.usefixtures("setup_credentials")
async def test_restricted_device( async def test_restricted_device(
hass: HomeAssistant, hass: HomeAssistant,