diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 5599965a2a6..24266e2d8bb 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -1,5 +1,8 @@ """The spotify integration.""" +from dataclasses import dataclass +from typing import Any + import aiohttp from spotipy import Spotify, SpotifyException import voluptuous as vol @@ -22,13 +25,7 @@ from homeassistant.helpers.typing import ConfigType from . import config_flow from .browse_media import async_browse_media -from .const import ( - DATA_SPOTIFY_CLIENT, - DATA_SPOTIFY_ME, - DATA_SPOTIFY_SESSION, - DOMAIN, - SPOTIFY_SCOPES, -) +from .const import DOMAIN, SPOTIFY_SCOPES from .util import is_spotify_media_type, resolve_spotify_media_type CONFIG_SCHEMA = vol.Schema( @@ -54,6 +51,15 @@ __all__ = [ ] +@dataclass +class HomeAssistantSpotifyData: + """Spotify data stored in the Home Assistant data object.""" + + client: Spotify + current_user: dict[str, Any] + session: OAuth2Session + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Spotify integration.""" if DOMAIN not in config: @@ -92,12 +98,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SpotifyException as err: raise ConfigEntryNotReady from err + if not current_user: + raise ConfigEntryNotReady + hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_SPOTIFY_CLIENT: spotify, - DATA_SPOTIFY_ME: current_user, - DATA_SPOTIFY_SESSION: session, - } + hass.data[DOMAIN][entry.entry_id] = HomeAssistantSpotifyData( + client=spotify, + current_user=current_user, + session=session, + ) if not set(session.token["scope"].split(" ")).issuperset(SPOTIFY_SCOPES): raise ConfigEntryAuthFailed diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index 0ffdfd6ace6..ae7c24e80d2 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -27,15 +27,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session -from .const import ( - DATA_SPOTIFY_CLIENT, - DATA_SPOTIFY_ME, - DATA_SPOTIFY_SESSION, - DOMAIN, - MEDIA_PLAYER_PREFIX, - MEDIA_TYPE_SHOW, - PLAYABLE_MEDIA_TYPES, -) +from .const import DOMAIN, MEDIA_PLAYER_PREFIX, MEDIA_TYPE_SHOW, PLAYABLE_MEDIA_TYPES from .util import fetch_image_url BROWSE_LIMIT = 48 @@ -155,9 +147,9 @@ async def async_browse_media( raise BrowseError("No Spotify accounts available") return await async_browse_media_internal( hass, - info[DATA_SPOTIFY_CLIENT], - info[DATA_SPOTIFY_SESSION], - info[DATA_SPOTIFY_ME], + info.client, + info.session, + info.current_user, media_content_type, media_content_id, can_play_artist=can_play_artist, diff --git a/homeassistant/components/spotify/const.py b/homeassistant/components/spotify/const.py index 6e54ed21ec1..4c86234045b 100644 --- a/homeassistant/components/spotify/const.py +++ b/homeassistant/components/spotify/const.py @@ -9,10 +9,6 @@ from homeassistant.components.media_player.const import ( DOMAIN = "spotify" -DATA_SPOTIFY_CLIENT = "spotify_client" -DATA_SPOTIFY_ME = "spotify_me" -DATA_SPOTIFY_SESSION = "spotify_session" - SPOTIFY_SCOPES = [ # Needed to be able to control playback "user-modify-playback-state", diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 58f581fdf82..cdcc0132f93 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -5,7 +5,6 @@ from asyncio import run_coroutine_threadsafe import datetime as dt from datetime import timedelta import logging -from typing import Any import requests from spotipy import Spotify, SpotifyException @@ -33,31 +32,17 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_ID, - CONF_NAME, - STATE_IDLE, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_ID, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utc_from_timestamp +from . import HomeAssistantSpotifyData from .browse_media import async_browse_media_internal -from .const import ( - DATA_SPOTIFY_CLIENT, - DATA_SPOTIFY_ME, - DATA_SPOTIFY_SESSION, - DOMAIN, - MEDIA_PLAYER_PREFIX, - PLAYABLE_MEDIA_TYPES, - SPOTIFY_SCOPES, -) +from .const import DOMAIN, MEDIA_PLAYER_PREFIX, PLAYABLE_MEDIA_TYPES, SPOTIFY_SCOPES from .util import fetch_image_url _LOGGER = logging.getLogger(__name__) @@ -98,7 +83,7 @@ async def async_setup_entry( spotify = SpotifyMediaPlayer( hass.data[DOMAIN][entry.entry_id], entry.data[CONF_ID], - entry.data[CONF_NAME], + entry.title, ) async_add_entities([spotify], True) @@ -135,57 +120,36 @@ class SpotifyMediaPlayer(MediaPlayerEntity): def __init__( self, - spotify_data, + data: HomeAssistantSpotifyData, user_id: str, name: str, ) -> None: """Initialize.""" self._id = user_id - self._spotify_data = spotify_data - self._name = f"Spotify {name}" - self._scope_ok = set(self._session.token["scope"].split(" ")).issuperset( - SPOTIFY_SCOPES - ) + self.data = data - self._currently_playing: dict | None = {} - self._devices: list[dict] | None = [] - self._playlist: dict | None = None - - self._attr_name = self._name + self._attr_name = f"Spotify {name}" self._attr_unique_id = user_id - @property - def _me(self) -> dict[str, Any]: - """Return spotify user info.""" - return self._spotify_data[DATA_SPOTIFY_ME] + if self.data.current_user["product"] == "premium": + self._attr_supported_features = SUPPORT_SPOTIFY - @property - def _session(self) -> OAuth2Session: - """Return spotify session.""" - return self._spotify_data[DATA_SPOTIFY_SESSION] - - @property - def _spotify(self) -> Spotify: - """Return spotify API.""" - return self._spotify_data[DATA_SPOTIFY_CLIENT] - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this entity.""" - model = "Spotify Free" - if self._me is not None: - product = self._me["product"] - model = f"Spotify {product}" - - return DeviceInfo( - identifiers={(DOMAIN, self._id)}, + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, user_id)}, manufacturer="Spotify AB", - model=model, - name=self._name, + model=f"Spotify {data.current_user['product']}", + name=f"Spotify {name}", entry_type=DeviceEntryType.SERVICE, configuration_url="https://open.spotify.com", ) + self._scope_ok = set(data.session.token["scope"].split(" ")).issuperset( + SPOTIFY_SCOPES + ) + self._currently_playing: dict | None = {} + self._devices: list[dict] | None = [] + self._playlist: dict | None = None + @property def state(self) -> str | None: """Return the playback state.""" @@ -315,42 +279,35 @@ class SpotifyMediaPlayer(MediaPlayerEntity): return None return REPEAT_MODE_MAPPING_TO_HA.get(repeat_state) - @property - def supported_features(self) -> int: - """Return the media player features that are supported.""" - if self._me["product"] != "premium": - return 0 - return SUPPORT_SPOTIFY - @spotify_exception_handler def set_volume_level(self, volume: int) -> None: """Set the volume level.""" - self._spotify.volume(int(volume * 100)) + self.data.client.volume(int(volume * 100)) @spotify_exception_handler def media_play(self) -> None: """Start or resume playback.""" - self._spotify.start_playback() + self.data.client.start_playback() @spotify_exception_handler def media_pause(self) -> None: """Pause playback.""" - self._spotify.pause_playback() + self.data.client.pause_playback() @spotify_exception_handler def media_previous_track(self) -> None: """Skip to previous track.""" - self._spotify.previous_track() + self.data.client.previous_track() @spotify_exception_handler def media_next_track(self) -> None: """Skip to next track.""" - self._spotify.next_track() + self.data.client.next_track() @spotify_exception_handler def media_seek(self, position): """Send seek command.""" - self._spotify.seek_track(int(position * 1000)) + self.data.client.seek_track(int(position * 1000)) @spotify_exception_handler def play_media(self, media_type: str, media_id: str, **kwargs) -> None: @@ -379,7 +336,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): ): kwargs["device_id"] = self._devices[0].get("id") - self._spotify.start_playback(**kwargs) + self.data.client.start_playback(**kwargs) @spotify_exception_handler def select_source(self, source: str) -> None: @@ -389,7 +346,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): for device in self._devices: if device["name"] == source: - self._spotify.transfer_playback( + self.data.client.transfer_playback( device["id"], self.state == STATE_PLAYING ) return @@ -397,14 +354,14 @@ class SpotifyMediaPlayer(MediaPlayerEntity): @spotify_exception_handler def set_shuffle(self, shuffle: bool) -> None: """Enable/Disable shuffle mode.""" - self._spotify.shuffle(shuffle) + self.data.client.shuffle(shuffle) @spotify_exception_handler def set_repeat(self, repeat: str) -> None: """Set repeat mode.""" if repeat not in REPEAT_MODE_MAPPING_TO_SPOTIFY: raise ValueError(f"Unsupported repeat mode: {repeat}") - self._spotify.repeat(REPEAT_MODE_MAPPING_TO_SPOTIFY[repeat]) + self.data.client.repeat(REPEAT_MODE_MAPPING_TO_SPOTIFY[repeat]) @spotify_exception_handler def update(self) -> None: @@ -412,23 +369,21 @@ class SpotifyMediaPlayer(MediaPlayerEntity): if not self.enabled: return - if not self._session.valid_token or self._spotify is None: + if not self.data.session.valid_token or self.data.client is None: run_coroutine_threadsafe( - self._session.async_ensure_token_valid(), self.hass.loop + self.data.session.async_ensure_token_valid(), self.hass.loop ).result() - self._spotify_data[DATA_SPOTIFY_CLIENT] = Spotify( - auth=self._session.token["access_token"] - ) + self.data.client = Spotify(auth=self.data.session.token["access_token"]) - current = self._spotify.current_playback() + current = self.data.client.current_playback() self._currently_playing = current or {} self._playlist = None context = self._currently_playing.get("context") if context is not None and context["type"] == MEDIA_TYPE_PLAYLIST: - self._playlist = self._spotify.playlist(current["context"]["uri"]) + self._playlist = self.data.client.playlist(current["context"]["uri"]) - devices = self._spotify.devices() or {} + devices = self.data.client.devices() or {} self._devices = devices.get("devices", []) async def async_browse_media( @@ -444,9 +399,9 @@ class SpotifyMediaPlayer(MediaPlayerEntity): return await async_browse_media_internal( self.hass, - self._spotify, - self._session, - self._me, + self.data.client, + self.data.session, + self.data.current_user, media_content_type, media_content_id, )