Type Spotify hass data (#66285)

This commit is contained in:
Franck Nijhof 2022-02-10 21:38:33 +01:00 committed by GitHub
parent c91a20537a
commit c6f3c5da79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 64 additions and 112 deletions

View File

@ -1,5 +1,8 @@
"""The spotify integration.""" """The spotify integration."""
from dataclasses import dataclass
from typing import Any
import aiohttp import aiohttp
from spotipy import Spotify, SpotifyException from spotipy import Spotify, SpotifyException
import voluptuous as vol import voluptuous as vol
@ -22,13 +25,7 @@ from homeassistant.helpers.typing import ConfigType
from . import config_flow from . import config_flow
from .browse_media import async_browse_media from .browse_media import async_browse_media
from .const import ( from .const import DOMAIN, SPOTIFY_SCOPES
DATA_SPOTIFY_CLIENT,
DATA_SPOTIFY_ME,
DATA_SPOTIFY_SESSION,
DOMAIN,
SPOTIFY_SCOPES,
)
from .util import is_spotify_media_type, resolve_spotify_media_type from .util import is_spotify_media_type, resolve_spotify_media_type
CONFIG_SCHEMA = vol.Schema( 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: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Spotify integration.""" """Set up the Spotify integration."""
if DOMAIN not in config: if DOMAIN not in config:
@ -92,12 +98,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except SpotifyException as err: except SpotifyException as err:
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
if not current_user:
raise ConfigEntryNotReady
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = { hass.data[DOMAIN][entry.entry_id] = HomeAssistantSpotifyData(
DATA_SPOTIFY_CLIENT: spotify, client=spotify,
DATA_SPOTIFY_ME: current_user, current_user=current_user,
DATA_SPOTIFY_SESSION: session, session=session,
} )
if not set(session.token["scope"].split(" ")).issuperset(SPOTIFY_SCOPES): if not set(session.token["scope"].split(" ")).issuperset(SPOTIFY_SCOPES):
raise ConfigEntryAuthFailed raise ConfigEntryAuthFailed

View File

@ -27,15 +27,7 @@ from homeassistant.components.media_player.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from .const import ( from .const import DOMAIN, MEDIA_PLAYER_PREFIX, MEDIA_TYPE_SHOW, PLAYABLE_MEDIA_TYPES
DATA_SPOTIFY_CLIENT,
DATA_SPOTIFY_ME,
DATA_SPOTIFY_SESSION,
DOMAIN,
MEDIA_PLAYER_PREFIX,
MEDIA_TYPE_SHOW,
PLAYABLE_MEDIA_TYPES,
)
from .util import fetch_image_url from .util import fetch_image_url
BROWSE_LIMIT = 48 BROWSE_LIMIT = 48
@ -155,9 +147,9 @@ async def async_browse_media(
raise BrowseError("No Spotify accounts available") raise BrowseError("No Spotify accounts available")
return await async_browse_media_internal( return await async_browse_media_internal(
hass, hass,
info[DATA_SPOTIFY_CLIENT], info.client,
info[DATA_SPOTIFY_SESSION], info.session,
info[DATA_SPOTIFY_ME], info.current_user,
media_content_type, media_content_type,
media_content_id, media_content_id,
can_play_artist=can_play_artist, can_play_artist=can_play_artist,

View File

@ -9,10 +9,6 @@ from homeassistant.components.media_player.const import (
DOMAIN = "spotify" DOMAIN = "spotify"
DATA_SPOTIFY_CLIENT = "spotify_client"
DATA_SPOTIFY_ME = "spotify_me"
DATA_SPOTIFY_SESSION = "spotify_session"
SPOTIFY_SCOPES = [ SPOTIFY_SCOPES = [
# Needed to be able to control playback # Needed to be able to control playback
"user-modify-playback-state", "user-modify-playback-state",

View File

@ -5,7 +5,6 @@ from asyncio import run_coroutine_threadsafe
import datetime as dt import datetime as dt
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any
import requests import requests
from spotipy import Spotify, SpotifyException from spotipy import Spotify, SpotifyException
@ -33,31 +32,17 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_SET,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import CONF_ID, STATE_IDLE, STATE_PAUSED, STATE_PLAYING
CONF_ID,
CONF_NAME,
STATE_IDLE,
STATE_PAUSED,
STATE_PLAYING,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.dt import utc_from_timestamp
from . import HomeAssistantSpotifyData
from .browse_media import async_browse_media_internal from .browse_media import async_browse_media_internal
from .const import ( from .const import DOMAIN, MEDIA_PLAYER_PREFIX, PLAYABLE_MEDIA_TYPES, SPOTIFY_SCOPES
DATA_SPOTIFY_CLIENT,
DATA_SPOTIFY_ME,
DATA_SPOTIFY_SESSION,
DOMAIN,
MEDIA_PLAYER_PREFIX,
PLAYABLE_MEDIA_TYPES,
SPOTIFY_SCOPES,
)
from .util import fetch_image_url from .util import fetch_image_url
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -98,7 +83,7 @@ async def async_setup_entry(
spotify = SpotifyMediaPlayer( spotify = SpotifyMediaPlayer(
hass.data[DOMAIN][entry.entry_id], hass.data[DOMAIN][entry.entry_id],
entry.data[CONF_ID], entry.data[CONF_ID],
entry.data[CONF_NAME], entry.title,
) )
async_add_entities([spotify], True) async_add_entities([spotify], True)
@ -135,57 +120,36 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
def __init__( def __init__(
self, self,
spotify_data, data: HomeAssistantSpotifyData,
user_id: str, user_id: str,
name: str, name: str,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
self._id = user_id self._id = user_id
self._spotify_data = spotify_data self.data = data
self._name = f"Spotify {name}"
self._scope_ok = set(self._session.token["scope"].split(" ")).issuperset(
SPOTIFY_SCOPES
)
self._currently_playing: dict | None = {} self._attr_name = f"Spotify {name}"
self._devices: list[dict] | None = []
self._playlist: dict | None = None
self._attr_name = self._name
self._attr_unique_id = user_id self._attr_unique_id = user_id
@property if self.data.current_user["product"] == "premium":
def _me(self) -> dict[str, Any]: self._attr_supported_features = SUPPORT_SPOTIFY
"""Return spotify user info."""
return self._spotify_data[DATA_SPOTIFY_ME]
@property self._attr_device_info = DeviceInfo(
def _session(self) -> OAuth2Session: identifiers={(DOMAIN, user_id)},
"""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)},
manufacturer="Spotify AB", manufacturer="Spotify AB",
model=model, model=f"Spotify {data.current_user['product']}",
name=self._name, name=f"Spotify {name}",
entry_type=DeviceEntryType.SERVICE, entry_type=DeviceEntryType.SERVICE,
configuration_url="https://open.spotify.com", 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 @property
def state(self) -> str | None: def state(self) -> str | None:
"""Return the playback state.""" """Return the playback state."""
@ -315,42 +279,35 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
return None return None
return REPEAT_MODE_MAPPING_TO_HA.get(repeat_state) 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 @spotify_exception_handler
def set_volume_level(self, volume: int) -> None: def set_volume_level(self, volume: int) -> None:
"""Set the volume level.""" """Set the volume level."""
self._spotify.volume(int(volume * 100)) self.data.client.volume(int(volume * 100))
@spotify_exception_handler @spotify_exception_handler
def media_play(self) -> None: def media_play(self) -> None:
"""Start or resume playback.""" """Start or resume playback."""
self._spotify.start_playback() self.data.client.start_playback()
@spotify_exception_handler @spotify_exception_handler
def media_pause(self) -> None: def media_pause(self) -> None:
"""Pause playback.""" """Pause playback."""
self._spotify.pause_playback() self.data.client.pause_playback()
@spotify_exception_handler @spotify_exception_handler
def media_previous_track(self) -> None: def media_previous_track(self) -> None:
"""Skip to previous track.""" """Skip to previous track."""
self._spotify.previous_track() self.data.client.previous_track()
@spotify_exception_handler @spotify_exception_handler
def media_next_track(self) -> None: def media_next_track(self) -> None:
"""Skip to next track.""" """Skip to next track."""
self._spotify.next_track() self.data.client.next_track()
@spotify_exception_handler @spotify_exception_handler
def media_seek(self, position): def media_seek(self, position):
"""Send seek command.""" """Send seek command."""
self._spotify.seek_track(int(position * 1000)) self.data.client.seek_track(int(position * 1000))
@spotify_exception_handler @spotify_exception_handler
def play_media(self, media_type: str, media_id: str, **kwargs) -> None: 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") kwargs["device_id"] = self._devices[0].get("id")
self._spotify.start_playback(**kwargs) self.data.client.start_playback(**kwargs)
@spotify_exception_handler @spotify_exception_handler
def select_source(self, source: str) -> None: def select_source(self, source: str) -> None:
@ -389,7 +346,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
for device in self._devices: for device in self._devices:
if device["name"] == source: if device["name"] == source:
self._spotify.transfer_playback( self.data.client.transfer_playback(
device["id"], self.state == STATE_PLAYING device["id"], self.state == STATE_PLAYING
) )
return return
@ -397,14 +354,14 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
@spotify_exception_handler @spotify_exception_handler
def set_shuffle(self, shuffle: bool) -> None: def set_shuffle(self, shuffle: bool) -> None:
"""Enable/Disable shuffle mode.""" """Enable/Disable shuffle mode."""
self._spotify.shuffle(shuffle) self.data.client.shuffle(shuffle)
@spotify_exception_handler @spotify_exception_handler
def set_repeat(self, repeat: str) -> None: def set_repeat(self, repeat: str) -> None:
"""Set repeat mode.""" """Set repeat mode."""
if repeat not in REPEAT_MODE_MAPPING_TO_SPOTIFY: if repeat not in REPEAT_MODE_MAPPING_TO_SPOTIFY:
raise ValueError(f"Unsupported repeat mode: {repeat}") 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 @spotify_exception_handler
def update(self) -> None: def update(self) -> None:
@ -412,23 +369,21 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
if not self.enabled: if not self.enabled:
return 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( run_coroutine_threadsafe(
self._session.async_ensure_token_valid(), self.hass.loop self.data.session.async_ensure_token_valid(), self.hass.loop
).result() ).result()
self._spotify_data[DATA_SPOTIFY_CLIENT] = Spotify( self.data.client = Spotify(auth=self.data.session.token["access_token"])
auth=self._session.token["access_token"]
)
current = self._spotify.current_playback() current = self.data.client.current_playback()
self._currently_playing = current or {} self._currently_playing = current or {}
self._playlist = None self._playlist = None
context = self._currently_playing.get("context") context = self._currently_playing.get("context")
if context is not None and context["type"] == MEDIA_TYPE_PLAYLIST: 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", []) self._devices = devices.get("devices", [])
async def async_browse_media( async def async_browse_media(
@ -444,9 +399,9 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
return await async_browse_media_internal( return await async_browse_media_internal(
self.hass, self.hass,
self._spotify, self.data.client,
self._session, self.data.session,
self._me, self.data.current_user,
media_content_type, media_content_type,
media_content_id, media_content_id,
) )