Black
This commit is contained in:
parent
da05dfe708
commit
4de97abc3a
2676 changed files with 163166 additions and 140084 deletions
|
@ -6,90 +6,112 @@ import random
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.components.media_player.const import (
|
||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_VOLUME_SET,
|
||||
ATTR_MEDIA_CONTENT_ID)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING)
|
||||
MEDIA_TYPE_MUSIC,
|
||||
MEDIA_TYPE_PLAYLIST,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY,
|
||||
SUPPORT_PLAY_MEDIA,
|
||||
SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_SELECT_SOURCE,
|
||||
SUPPORT_SHUFFLE_SET,
|
||||
SUPPORT_VOLUME_SET,
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AUTH_CALLBACK_NAME = 'api:spotify'
|
||||
AUTH_CALLBACK_PATH = '/api/spotify'
|
||||
AUTH_CALLBACK_NAME = "api:spotify"
|
||||
AUTH_CALLBACK_PATH = "/api/spotify"
|
||||
|
||||
CONF_ALIASES = 'aliases'
|
||||
CONF_CACHE_PATH = 'cache_path'
|
||||
CONF_CLIENT_ID = 'client_id'
|
||||
CONF_CLIENT_SECRET = 'client_secret'
|
||||
CONF_ALIASES = "aliases"
|
||||
CONF_CACHE_PATH = "cache_path"
|
||||
CONF_CLIENT_ID = "client_id"
|
||||
CONF_CLIENT_SECRET = "client_secret"
|
||||
|
||||
CONFIGURATOR_DESCRIPTION = 'To link your Spotify account, ' \
|
||||
'click the link, login, and authorize:'
|
||||
CONFIGURATOR_LINK_NAME = 'Link Spotify account'
|
||||
CONFIGURATOR_SUBMIT_CAPTION = 'I authorized successfully'
|
||||
CONFIGURATOR_DESCRIPTION = (
|
||||
"To link your Spotify account, " "click the link, login, and authorize:"
|
||||
)
|
||||
CONFIGURATOR_LINK_NAME = "Link Spotify account"
|
||||
CONFIGURATOR_SUBMIT_CAPTION = "I authorized successfully"
|
||||
|
||||
DEFAULT_CACHE_PATH = '.spotify-token-cache'
|
||||
DEFAULT_NAME = 'Spotify'
|
||||
DOMAIN = 'spotify'
|
||||
DEFAULT_CACHE_PATH = ".spotify-token-cache"
|
||||
DEFAULT_NAME = "Spotify"
|
||||
DOMAIN = "spotify"
|
||||
|
||||
SERVICE_PLAY_PLAYLIST = 'play_playlist'
|
||||
ATTR_RANDOM_SONG = 'random_song'
|
||||
SERVICE_PLAY_PLAYLIST = "play_playlist"
|
||||
ATTR_RANDOM_SONG = "random_song"
|
||||
|
||||
PLAY_PLAYLIST_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
|
||||
vol.Optional(ATTR_RANDOM_SONG, default=False): cv.boolean
|
||||
})
|
||||
PLAY_PLAYLIST_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
|
||||
vol.Optional(ATTR_RANDOM_SONG, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
ICON = 'mdi:spotify'
|
||||
ICON = "mdi:spotify"
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
SCOPE = 'user-read-playback-state user-modify-playback-state user-read-private'
|
||||
SCOPE = "user-read-playback-state user-modify-playback-state user-read-private"
|
||||
|
||||
SUPPORT_SPOTIFY = SUPPORT_VOLUME_SET | SUPPORT_PAUSE | SUPPORT_PLAY |\
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK | SUPPORT_SELECT_SOURCE |\
|
||||
SUPPORT_PLAY_MEDIA | SUPPORT_SHUFFLE_SET
|
||||
SUPPORT_SPOTIFY = (
|
||||
SUPPORT_VOLUME_SET
|
||||
| SUPPORT_PAUSE
|
||||
| SUPPORT_PLAY
|
||||
| SUPPORT_NEXT_TRACK
|
||||
| SUPPORT_PREVIOUS_TRACK
|
||||
| SUPPORT_SELECT_SOURCE
|
||||
| SUPPORT_PLAY_MEDIA
|
||||
| SUPPORT_SHUFFLE_SET
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_CACHE_PATH): cv.string,
|
||||
vol.Optional(CONF_ALIASES, default={}): {cv.string: cv.string}
|
||||
})
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_CACHE_PATH): cv.string,
|
||||
vol.Optional(CONF_ALIASES, default={}): {cv.string: cv.string},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def request_configuration(hass, config, add_entities, oauth):
|
||||
"""Request Spotify authorization."""
|
||||
configurator = hass.components.configurator
|
||||
hass.data[DOMAIN] = configurator.request_config(
|
||||
DEFAULT_NAME, lambda _: None,
|
||||
DEFAULT_NAME,
|
||||
lambda _: None,
|
||||
link_name=CONFIGURATOR_LINK_NAME,
|
||||
link_url=oauth.get_authorize_url(),
|
||||
description=CONFIGURATOR_DESCRIPTION,
|
||||
submit_caption=CONFIGURATOR_SUBMIT_CAPTION)
|
||||
submit_caption=CONFIGURATOR_SUBMIT_CAPTION,
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Spotify platform."""
|
||||
import spotipy.oauth2
|
||||
|
||||
callback_url = '{}{}'.format(hass.config.api.base_url, AUTH_CALLBACK_PATH)
|
||||
callback_url = "{}{}".format(hass.config.api.base_url, AUTH_CALLBACK_PATH)
|
||||
cache = config.get(CONF_CACHE_PATH, hass.config.path(DEFAULT_CACHE_PATH))
|
||||
oauth = spotipy.oauth2.SpotifyOAuth(
|
||||
config.get(CONF_CLIENT_ID), config.get(CONF_CLIENT_SECRET),
|
||||
callback_url, scope=SCOPE,
|
||||
cache_path=cache)
|
||||
config.get(CONF_CLIENT_ID),
|
||||
config.get(CONF_CLIENT_SECRET),
|
||||
callback_url,
|
||||
scope=SCOPE,
|
||||
cache_path=cache,
|
||||
)
|
||||
token_info = oauth.get_cached_token()
|
||||
if not token_info:
|
||||
_LOGGER.info("no token; requesting authorization")
|
||||
hass.http.register_view(SpotifyAuthCallbackView(
|
||||
config, add_entities, oauth))
|
||||
hass.http.register_view(SpotifyAuthCallbackView(config, add_entities, oauth))
|
||||
request_configuration(hass, config, add_entities, oauth)
|
||||
return
|
||||
if hass.data.get(DOMAIN):
|
||||
|
@ -97,7 +119,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
configurator.request_done(hass.data.get(DOMAIN))
|
||||
del hass.data[DOMAIN]
|
||||
player = SpotifyMediaPlayer(
|
||||
oauth, config.get(CONF_NAME, DEFAULT_NAME), config[CONF_ALIASES])
|
||||
oauth, config.get(CONF_NAME, DEFAULT_NAME), config[CONF_ALIASES]
|
||||
)
|
||||
add_entities([player], True)
|
||||
|
||||
def play_playlist_service(service):
|
||||
|
@ -106,9 +129,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
player.play_playlist(media_content_id, random_song)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_PLAY_PLAYLIST,
|
||||
DOMAIN,
|
||||
SERVICE_PLAY_PLAYLIST,
|
||||
play_playlist_service,
|
||||
schema=PLAY_PLAYLIST_SCHEMA)
|
||||
schema=PLAY_PLAYLIST_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
class SpotifyAuthCallbackView(HomeAssistantView):
|
||||
|
@ -127,10 +152,9 @@ class SpotifyAuthCallbackView(HomeAssistantView):
|
|||
@callback
|
||||
def get(self, request):
|
||||
"""Receive authorization token."""
|
||||
hass = request.app['hass']
|
||||
self.oauth.get_access_token(request.query['code'])
|
||||
hass.async_add_job(
|
||||
setup_platform, hass, self.config, self.add_entities)
|
||||
hass = request.app["hass"]
|
||||
self.oauth.get_access_token(request.query["code"])
|
||||
hass.async_add_job(setup_platform, hass, self.config, self.add_entities)
|
||||
|
||||
|
||||
class SpotifyMediaPlayer(MediaPlayerDevice):
|
||||
|
@ -158,13 +182,15 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
|||
def refresh_spotify_instance(self):
|
||||
"""Fetch a new spotify instance."""
|
||||
import spotipy
|
||||
|
||||
token_refreshed = False
|
||||
need_token = (self._token_info is None or
|
||||
self._oauth.is_token_expired(self._token_info))
|
||||
need_token = self._token_info is None or self._oauth.is_token_expired(
|
||||
self._token_info
|
||||
)
|
||||
if need_token:
|
||||
new_token = \
|
||||
self._oauth.refresh_access_token(
|
||||
self._token_info['refresh_token'])
|
||||
new_token = self._oauth.refresh_access_token(
|
||||
self._token_info["refresh_token"]
|
||||
)
|
||||
# skip when refresh failed
|
||||
if new_token is None:
|
||||
return
|
||||
|
@ -172,8 +198,7 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
|||
self._token_info = new_token
|
||||
token_refreshed = True
|
||||
if self._player is None or token_refreshed:
|
||||
self._player = \
|
||||
spotipy.Spotify(auth=self._token_info.get('access_token'))
|
||||
self._player = spotipy.Spotify(auth=self._token_info.get("access_token"))
|
||||
self._user = self._player.me()
|
||||
|
||||
def update(self):
|
||||
|
@ -188,15 +213,20 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
|||
# Available devices
|
||||
player_devices = self._player.devices()
|
||||
if player_devices is not None:
|
||||
devices = player_devices.get('devices')
|
||||
devices = player_devices.get("devices")
|
||||
if devices is not None:
|
||||
old_devices = self._devices
|
||||
self._devices = {self._aliases.get(device.get('id'),
|
||||
device.get('name')):
|
||||
device.get('id')
|
||||
for device in devices}
|
||||
device_diff = {name: id for name, id in self._devices.items()
|
||||
if old_devices.get(name, None) is None}
|
||||
self._devices = {
|
||||
self._aliases.get(device.get("id"), device.get("name")): device.get(
|
||||
"id"
|
||||
)
|
||||
for device in devices
|
||||
}
|
||||
device_diff = {
|
||||
name: id
|
||||
for name, id in self._devices.items()
|
||||
if old_devices.get(name, None) is None
|
||||
}
|
||||
if device_diff:
|
||||
_LOGGER.info("New Devices: %s", str(device_diff))
|
||||
# Current playback state
|
||||
|
@ -205,28 +235,29 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
|||
self._state = STATE_IDLE
|
||||
return
|
||||
# Track metadata
|
||||
item = current.get('item')
|
||||
item = current.get("item")
|
||||
if item:
|
||||
self._album = item.get('album').get('name')
|
||||
self._title = item.get('name')
|
||||
self._artist = ', '.join([artist.get('name')
|
||||
for artist in item.get('artists')])
|
||||
self._uri = item.get('uri')
|
||||
images = item.get('album').get('images')
|
||||
self._image_url = images[0].get('url') if images else None
|
||||
self._album = item.get("album").get("name")
|
||||
self._title = item.get("name")
|
||||
self._artist = ", ".join(
|
||||
[artist.get("name") for artist in item.get("artists")]
|
||||
)
|
||||
self._uri = item.get("uri")
|
||||
images = item.get("album").get("images")
|
||||
self._image_url = images[0].get("url") if images else None
|
||||
# Playing state
|
||||
self._state = STATE_PAUSED
|
||||
if current.get('is_playing'):
|
||||
if current.get("is_playing"):
|
||||
self._state = STATE_PLAYING
|
||||
self._shuffle = current.get('shuffle_state')
|
||||
device = current.get('device')
|
||||
self._shuffle = current.get("shuffle_state")
|
||||
device = current.get("device")
|
||||
if device is None:
|
||||
self._state = STATE_IDLE
|
||||
else:
|
||||
if device.get('volume_percent'):
|
||||
self._volume = device.get('volume_percent') / 100
|
||||
if device.get('name'):
|
||||
self._current_device = device.get('name')
|
||||
if device.get("volume_percent"):
|
||||
self._volume = device.get("volume_percent") / 100
|
||||
if device.get("name"):
|
||||
self._current_device = device.get("name")
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set the volume level."""
|
||||
|
@ -255,34 +286,35 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
|||
def select_source(self, source):
|
||||
"""Select playback device."""
|
||||
if self._devices:
|
||||
self._player.transfer_playback(self._devices[source],
|
||||
self._state == STATE_PLAYING)
|
||||
self._player.transfer_playback(
|
||||
self._devices[source], self._state == STATE_PLAYING
|
||||
)
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play media."""
|
||||
kwargs = {}
|
||||
if media_type == MEDIA_TYPE_MUSIC:
|
||||
kwargs['uris'] = [media_id]
|
||||
kwargs["uris"] = [media_id]
|
||||
elif media_type == MEDIA_TYPE_PLAYLIST:
|
||||
kwargs['context_uri'] = media_id
|
||||
kwargs["context_uri"] = media_id
|
||||
else:
|
||||
_LOGGER.error("media type %s is not supported", media_type)
|
||||
return
|
||||
if not media_id.startswith('spotify:'):
|
||||
if not media_id.startswith("spotify:"):
|
||||
_LOGGER.error("media id must be spotify uri")
|
||||
return
|
||||
self._player.start_playback(**kwargs)
|
||||
|
||||
def play_playlist(self, media_id, random_song):
|
||||
"""Play random music in a playlist."""
|
||||
if not media_id.startswith('spotify:playlist:'):
|
||||
if not media_id.startswith("spotify:playlist:"):
|
||||
_LOGGER.error("media id must be spotify playlist uri")
|
||||
return
|
||||
kwargs = {'context_uri': media_id}
|
||||
kwargs = {"context_uri": media_id}
|
||||
if random_song:
|
||||
results = self._player.user_playlist_tracks("me", media_id)
|
||||
position = random.randint(0, results['total'] - 1)
|
||||
kwargs['offset'] = {'position': position}
|
||||
position = random.randint(0, results["total"] - 1)
|
||||
kwargs["offset"] = {"position": position}
|
||||
self._player.start_playback(**kwargs)
|
||||
|
||||
@property
|
||||
|
@ -349,7 +381,7 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
|||
@property
|
||||
def supported_features(self):
|
||||
"""Return the media player features that are supported."""
|
||||
if self._user is not None and self._user['product'] == 'premium':
|
||||
if self._user is not None and self._user["product"] == "premium":
|
||||
return SUPPORT_SPOTIFY
|
||||
return None
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue