spotifywebapiPython 1.0.149__py3-none-any.whl → 1.0.151__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of spotifywebapiPython might be problematic. Click here for more details.
- {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/METADATA +2 -1
- {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/RECORD +27 -27
- spotifywebapipython/const.py +6 -1
- spotifywebapipython/docs/__init__.html +15 -2
- spotifywebapipython/docs/index.html +2 -2
- spotifywebapipython/docs/search.js +1 -1
- spotifywebapipython/docs/spotifywebapipython/const.html +20 -3
- spotifywebapipython/docs/spotifywebapipython/saappmessages.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/sautils.html +65 -2
- spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +42 -2
- spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +2 -2
- spotifywebapipython/models/spotifyconnectdevice.py +13 -1
- spotifywebapipython/models/zeroconfdiscoveryresult.py +1 -1
- spotifywebapipython/sautils.py +50 -0
- spotifywebapipython/spotifyclient.py +494 -169
- spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +107 -19
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +1 -1
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +1 -0
- {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/top_level.txt +0 -0
|
@@ -5,10 +5,16 @@ import json
|
|
|
5
5
|
from io import BytesIO
|
|
6
6
|
from oauthlib.oauth2 import BackendApplicationClient, WebApplicationClient
|
|
7
7
|
import socket
|
|
8
|
+
from soco import SoCo
|
|
9
|
+
from soco.core import (
|
|
10
|
+
PLAY_MODE_BY_MEANING as SONOS_PLAY_MODE_BY_MEANING,
|
|
11
|
+
PLAY_MODES as SONOS_PLAY_MODES,
|
|
12
|
+
)
|
|
8
13
|
import time
|
|
9
14
|
from typing import Tuple, Callable, Union
|
|
10
15
|
from urllib3 import PoolManager, Timeout, HTTPResponse
|
|
11
16
|
from urllib.parse import urlencode
|
|
17
|
+
import urllib.parse
|
|
12
18
|
from lxml.etree import fromstring, Element, tostring
|
|
13
19
|
from lxml import etree
|
|
14
20
|
from zeroconf import Zeroconf
|
|
@@ -18,7 +24,6 @@ from .oauthcli import AuthClient
|
|
|
18
24
|
from .models import *
|
|
19
25
|
from .models import UserProfile as UserProfileCurrentUser
|
|
20
26
|
from .spotifyconnect import SpotifyConnectDirectoryTask
|
|
21
|
-
from .sautils import export, GetUnixTimestampMSFromUtcNow, _xmlGetInnerText, validateDelay
|
|
22
27
|
from .saappmessages import SAAppMessages
|
|
23
28
|
from .spotifyapierror import SpotifyApiError
|
|
24
29
|
from .spotifyapimessage import SpotifyApiMessage
|
|
@@ -28,6 +33,14 @@ from .spotifywebapiauthenticationerror import SpotifyWebApiAuthenticationError
|
|
|
28
33
|
from .spotifywebapierror import SpotifyWebApiError
|
|
29
34
|
from .zeroconfapi.zeroconfconnect import ZeroconfConnect, ZeroconfGetInfo, ZeroconfResponse, SpotifyZeroconfApiError
|
|
30
35
|
from .zeroconfapi.zeroconfgetinfoalias import ZeroconfGetInfoAlias
|
|
36
|
+
from .sautils import (
|
|
37
|
+
_xmlGetInnerText,
|
|
38
|
+
export,
|
|
39
|
+
GetUnixTimestampMSFromUtcNow,
|
|
40
|
+
mediaPositionHMS_fromSeconds,
|
|
41
|
+
mediaPositionHMS_toSeconds,
|
|
42
|
+
validateDelay
|
|
43
|
+
)
|
|
31
44
|
from .const import (
|
|
32
45
|
SPOTIFY_API_AUTHORIZE_URL,
|
|
33
46
|
SPOTIFY_API_TOKEN_URL,
|
|
@@ -42,6 +55,7 @@ from .const import (
|
|
|
42
55
|
TRACE_MSG_AUTHTOKEN_CREATE,
|
|
43
56
|
TRACE_MSG_AUTOPAGING_NEXT,
|
|
44
57
|
TRACE_MSG_DELAY_DEVICE,
|
|
58
|
+
TRACE_MSG_DELAY_DEVICE_SONOS,
|
|
45
59
|
TRACE_MSG_USERPROFILE,
|
|
46
60
|
TRACE_WARN_SPOTIFY_SEARCH_NO_MARKET,
|
|
47
61
|
VERSION
|
|
@@ -813,7 +827,6 @@ class SpotifyClient:
|
|
|
813
827
|
if (self._SpotifyConnectLoginId is None) or (len(self._SpotifyConnectLoginId.strip()) == 0):
|
|
814
828
|
_logsi.LogVerbose("Spotify Connect LoginId not specified on class constructor; using Spotify UserProfile ID value \"%s\"" % (self._UserProfile.Id), colorValue=SIColors.Coral)
|
|
815
829
|
self._SpotifyConnectLoginId = self._UserProfile.Id
|
|
816
|
-
#raise SpotifyApiError(SAAppMessages.MSG_SPOTIFY_ACTIVATE_CREDENTIAL_REQUIRED % ("spotifyConnectLoginId"), logsi=_logsi) #TODO REMOVEME
|
|
817
830
|
|
|
818
831
|
# create new Spotify Connect Directory instance.
|
|
819
832
|
self._SpotifyConnectDirectory = SpotifyConnectDirectoryTask(self, self._ZeroconfClient, self._SpotifyConnectDiscoveryTimeout)
|
|
@@ -7574,6 +7587,11 @@ class SpotifyClient:
|
|
|
7574
7587
|
if scDevice is not None:
|
|
7575
7588
|
device.Id = scDevice.DeviceInfo.DeviceId
|
|
7576
7589
|
|
|
7590
|
+
# update Spotify Connect Directory with active device details.
|
|
7591
|
+
# IMPORTANT - make sure result contains an object, otherwise it's an endless loop!
|
|
7592
|
+
if result is not None:
|
|
7593
|
+
self.SpotifyConnectDirectory.UpdateActiveDevice(result)
|
|
7594
|
+
|
|
7577
7595
|
# trace.
|
|
7578
7596
|
_logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT_TYPE % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
7579
7597
|
return result
|
|
@@ -9159,6 +9177,219 @@ class SpotifyClient:
|
|
|
9159
9177
|
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
9160
9178
|
|
|
9161
9179
|
|
|
9180
|
+
def GetSonosPlayerPlaybackState(
|
|
9181
|
+
self,
|
|
9182
|
+
scDevice:SpotifyConnectDevice,
|
|
9183
|
+
) -> PlayerPlayState:
|
|
9184
|
+
"""
|
|
9185
|
+
Get information about the Sonos user's current playback state, including track or episode, progress,
|
|
9186
|
+
and active device.
|
|
9187
|
+
|
|
9188
|
+
This is not part of the Spotify Web API specification; it is for Sonos devices only!
|
|
9189
|
+
|
|
9190
|
+
Args:
|
|
9191
|
+
scDevice (SpotifyConnectDevice):
|
|
9192
|
+
Spotify Connect device instance used to identify the Sonos player.
|
|
9193
|
+
|
|
9194
|
+
Returns:
|
|
9195
|
+
A `PlayerPlayState` object that contains Sonos player state details as well as
|
|
9196
|
+
currently playing content.
|
|
9197
|
+
|
|
9198
|
+
Raises:
|
|
9199
|
+
SpotifyApiError:
|
|
9200
|
+
If the method fails for any other reason.
|
|
9201
|
+
"""
|
|
9202
|
+
apiMethodName:str = 'GetSonosPlayerPlaybackState'
|
|
9203
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
9204
|
+
|
|
9205
|
+
try:
|
|
9206
|
+
|
|
9207
|
+
# trace.
|
|
9208
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
9209
|
+
apiMethodParms.AppendKeyValue("scDevice", scDevice)
|
|
9210
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Get Sonos user's current playback state", apiMethodParms)
|
|
9211
|
+
|
|
9212
|
+
# create playstate object.
|
|
9213
|
+
playerState = PlayerPlayState()
|
|
9214
|
+
|
|
9215
|
+
# get Sonos Controller instance for the device.
|
|
9216
|
+
sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
|
|
9217
|
+
|
|
9218
|
+
# use the SoCo API to get Sonos playback status.
|
|
9219
|
+
# build a spotify playback status instance with equivalent Sonos state values.
|
|
9220
|
+
_logsi.LogVerbose("Getting Sonos device status via Sonos Control API for device: %s" % (scDevice.Title))
|
|
9221
|
+
sonosTrackInfo:dict = sonosPlayer.get_current_track_info()
|
|
9222
|
+
_logsi.LogDictionary(SILevel.Verbose, "Sonos device current_track_info results for device: %s" % (scDevice.Title), sonosTrackInfo, prettyPrint=True)
|
|
9223
|
+
sonosTransportInfo:dict = sonosPlayer.get_current_transport_info()
|
|
9224
|
+
_logsi.LogDictionary(SILevel.Verbose, "Sonos device get_current_transport_info results for device: %s" % (scDevice.Title), sonosTransportInfo, prettyPrint=True)
|
|
9225
|
+
|
|
9226
|
+
# update Spotify Web API player state with Sonos player state.
|
|
9227
|
+
playerState._Device._Id = scDevice.Id
|
|
9228
|
+
playerState._Device._Name = sonosPlayer.player_name
|
|
9229
|
+
playerState._Device._VolumePercent = sonosPlayer.volume
|
|
9230
|
+
playerState._Device._IsActive = True
|
|
9231
|
+
playerState._Device._IsRestricted = True
|
|
9232
|
+
playerState._Device._SupportsVolume = True
|
|
9233
|
+
playerState._Device._Type = 'SPEAKER'
|
|
9234
|
+
|
|
9235
|
+
# try to get the spotify uri from the "metadata" music library DIDL value, as
|
|
9236
|
+
# the "uri" value (e.g. "x-sonos-vli:RINCON_38420B909DC801400:2,spotify:e934941535d7b182815bf688490ce8a8")
|
|
9237
|
+
# is not a true spotify uri value (e.g. "spotify:track:6kYyS9g4WJeRzTYqsmcMmM")
|
|
9238
|
+
spotifyUri:str = sonosTrackInfo.get('uri','')
|
|
9239
|
+
didl:str = sonosTrackInfo.get('metadata',None)
|
|
9240
|
+
if (didl):
|
|
9241
|
+
METADATA_ID:str = 'x-sonos-spotify:'
|
|
9242
|
+
idx:int = didl.find(METADATA_ID)
|
|
9243
|
+
if (idx > -1):
|
|
9244
|
+
spotifyUri:str = didl[idx + len(METADATA_ID):]
|
|
9245
|
+
spotifyUri = urllib.parse.unquote(spotifyUri)
|
|
9246
|
+
idx = spotifyUri.find('?')
|
|
9247
|
+
if (idx > -1):
|
|
9248
|
+
spotifyUri = spotifyUri[:idx]
|
|
9249
|
+
spotifyType = self.GetTypeFromUri(spotifyUri) or ''
|
|
9250
|
+
spotifyId = self.GetIdFromUri(spotifyUri) or ''
|
|
9251
|
+
|
|
9252
|
+
# set base item properties.
|
|
9253
|
+
playerState.IsEmpty = False
|
|
9254
|
+
playerState.ItemType = spotifyType
|
|
9255
|
+
sTimeValue:str = sonosTrackInfo.get('position',None)
|
|
9256
|
+
playerState._ProgressMS = mediaPositionHMS_toSeconds(sTimeValue) * 1000 # convert h:mm:ss to milliseconds
|
|
9257
|
+
|
|
9258
|
+
# set some item properties based on the playing type (episode or non-episode).
|
|
9259
|
+
if (spotifyType == 'episode'):
|
|
9260
|
+
|
|
9261
|
+
# get the episode data from spotify, as Sonos Soco data is incomplete.
|
|
9262
|
+
playerState._CurrentlyPlayingType = 'episode'
|
|
9263
|
+
episode:Episode = self.GetEpisode(spotifyId)
|
|
9264
|
+
if (episode.Id is not None):
|
|
9265
|
+
playerState._Item = episode
|
|
9266
|
+
if (playerState._Context is None):
|
|
9267
|
+
playerState._Context = Context()
|
|
9268
|
+
playerState._Context._Type = episode.Show.Type
|
|
9269
|
+
playerState._Context._Uri = episode.Show.Uri
|
|
9270
|
+
playerState._Context._Href = episode.Show.Href
|
|
9271
|
+
playerState._Context._ExternalUrls = episode.Show.ExternalUrls
|
|
9272
|
+
else:
|
|
9273
|
+
# if data could not be obtained from spotify, then use what's
|
|
9274
|
+
# available from Sonos Soco api metadata.
|
|
9275
|
+
playerState._Item = Episode()
|
|
9276
|
+
playerState._Item._Name = sonosTrackInfo.get('title','')
|
|
9277
|
+
playerState._Item._Uri = spotifyUri
|
|
9278
|
+
playerState._Item._Description = 'Sonos device does not provide a description'
|
|
9279
|
+
sTimeValue:str = sonosTrackInfo.get('duration',None)
|
|
9280
|
+
playerState._Item._DurationMS = mediaPositionHMS_toSeconds(sTimeValue) * 1000 # convert h:mm:ss to milliseconds
|
|
9281
|
+
playerState._Item._Explicit = False
|
|
9282
|
+
playerState._Item._Href = 'https://api.spotify.com/v1/episodes/' + spotifyId
|
|
9283
|
+
playerState._Item._HtmlDescription = playerState._Item._Description
|
|
9284
|
+
playerState._Item._Id = spotifyId
|
|
9285
|
+
playerState._Item.Images.append(ImageObject())
|
|
9286
|
+
playerState._Item.Images[0]._Url = sonosTrackInfo.get('album_art','')
|
|
9287
|
+
playerState._Item._TrackNumber = sonosTrackInfo.get('playlist_position','')
|
|
9288
|
+
playerState._Item._ReleaseDate = '0000'
|
|
9289
|
+
playerState._Item._ReleaseDatePrecision = 'year'
|
|
9290
|
+
playerState._Item._Type = 'episode'
|
|
9291
|
+
playerState._Item._Show = Show()
|
|
9292
|
+
playerState._Item._Show._Name = sonosTrackInfo.get('album','') # TODO get this from metadata (<r:podcast>The Elfstones of Shannara</r:podcast>)
|
|
9293
|
+
playerState._Item._Show.Images.append(ImageObject())
|
|
9294
|
+
playerState._Item._Show.Images[0]._Url = sonosTrackInfo.get('album_art','')
|
|
9295
|
+
playerState._Item._Show._Publisher = sonosTrackInfo.get('artist','')
|
|
9296
|
+
|
|
9297
|
+
# if show name not set then get it from metadata.
|
|
9298
|
+
if (playerState._Item._Show._Name == ''):
|
|
9299
|
+
if (didl):
|
|
9300
|
+
METADATA_ID:str = '<r:podcast>'
|
|
9301
|
+
METADATA_END:str = '</r:podcast>'
|
|
9302
|
+
idx:int = didl.find(METADATA_ID)
|
|
9303
|
+
if (idx > -1):
|
|
9304
|
+
metaValue:str = didl[idx + len(METADATA_ID):]
|
|
9305
|
+
idx = metaValue.find(METADATA_END)
|
|
9306
|
+
if (idx > -1):
|
|
9307
|
+
metaValue = metaValue[:idx]
|
|
9308
|
+
playerState._Item._Show._Name = metaValue
|
|
9309
|
+
|
|
9310
|
+
# if episode is playing then resolve the underlying type (audiobook / podcast show).
|
|
9311
|
+
if (self.IsChapterEpisode(spotifyId)):
|
|
9312
|
+
playerState.ItemType = 'audiobook'
|
|
9313
|
+
else:
|
|
9314
|
+
playerState.ItemType = 'podcast'
|
|
9315
|
+
|
|
9316
|
+
elif (spotifyType == 'track'):
|
|
9317
|
+
|
|
9318
|
+
# get the track data from spotify, as Sonos Soco data is incomplete.
|
|
9319
|
+
track:Track = self.GetTrack(spotifyId)
|
|
9320
|
+
if (track.Id is not None):
|
|
9321
|
+
playerState._Item = track
|
|
9322
|
+
if (playerState._Context is None):
|
|
9323
|
+
playerState._Context = Context()
|
|
9324
|
+
playerState._Context._Type = track.Album.Type
|
|
9325
|
+
playerState._Context._Uri = track.Album.Uri
|
|
9326
|
+
playerState._Context._Href = track.Album.Href
|
|
9327
|
+
playerState._Context._ExternalUrls = track.Album.ExternalUrls
|
|
9328
|
+
else:
|
|
9329
|
+
# if data could not be obtained from spotify, then use what's
|
|
9330
|
+
# available from Sonos Soco api metadata.
|
|
9331
|
+
playerState._CurrentlyPlayingType = 'track'
|
|
9332
|
+
playerState.ItemType = playerState._CurrentlyPlayingType
|
|
9333
|
+
playerState._Item = Track()
|
|
9334
|
+
playerState._Item._Name = sonosTrackInfo.get('title','')
|
|
9335
|
+
playerState._Item._Uri = spotifyUri
|
|
9336
|
+
playerState._Item._Description = 'Sonos device does not provide a description'
|
|
9337
|
+
sTimeValue:str = sonosTrackInfo.get('duration',None)
|
|
9338
|
+
playerState._Item._DurationMS = mediaPositionHMS_toSeconds(sTimeValue) * 1000 # convert h:mm:ss to milliseconds
|
|
9339
|
+
playerState._Item._Explicit = False
|
|
9340
|
+
playerState._Item._Href = 'https://api.spotify.com/v1/episodes/' + spotifyId
|
|
9341
|
+
playerState._Item._HtmlDescription = playerState._Item._Description
|
|
9342
|
+
playerState._Item._Id = spotifyId
|
|
9343
|
+
playerState._Item._TrackNumber = sonosTrackInfo.get('playlist_position','')
|
|
9344
|
+
playerState._Item._ReleaseDate = '0000'
|
|
9345
|
+
playerState._Item._ReleaseDatePrecision = 'year'
|
|
9346
|
+
playerState._Item._Type = 'track'
|
|
9347
|
+
playerState._Item._Album = Album()
|
|
9348
|
+
playerState._Item._Album._Name = sonosTrackInfo.get('album','')
|
|
9349
|
+
playerState._Item._Album.Images.append(ImageObject())
|
|
9350
|
+
playerState._Item._Album.Images[0]._Url = sonosTrackInfo.get('album_art','')
|
|
9351
|
+
playerState._Item.Artists.append(Artist())
|
|
9352
|
+
playerState._Item.Artists[0]._Name = sonosTrackInfo.get('artist','')
|
|
9353
|
+
|
|
9354
|
+
# set transport actions.
|
|
9355
|
+
currentTransportState:str = sonosTransportInfo.get('current_transport_state','')
|
|
9356
|
+
if currentTransportState == 'PLAYING':
|
|
9357
|
+
playerState._IsPlaying = True
|
|
9358
|
+
elif currentTransportState in ['PAUSED_PLAYBACK','STOPPED']:
|
|
9359
|
+
playerState.Actions._Pausing = True
|
|
9360
|
+
|
|
9361
|
+
# only update the following PlayState attributes from SoCo if they are NOT set.
|
|
9362
|
+
# these values are used to set the Sonos PlayMode value, which is the actual value that determines
|
|
9363
|
+
# what the Repeat and Shuffle settings are (ya, it's weird!). What is odd is that the Spotify
|
|
9364
|
+
# Web API PlayerState values for Shuffle and Repeat can be different than what the Sonos Controller
|
|
9365
|
+
# API (SoCo) report for the device! This is why we use the Spotify Web API PlayerState values.
|
|
9366
|
+
if playerState._ShuffleState is None:
|
|
9367
|
+
playerState._ShuffleState = sonosPlayer.shuffle
|
|
9368
|
+
if playerState._RepeatState is None:
|
|
9369
|
+
sonosRepeat = sonosPlayer.repeat
|
|
9370
|
+
if sonosRepeat == 'ONE':
|
|
9371
|
+
playerState._RepeatState = 'track'
|
|
9372
|
+
elif sonosRepeat == False:
|
|
9373
|
+
playerState._RepeatState = 'off'
|
|
9374
|
+
elif sonosRepeat == True:
|
|
9375
|
+
playerState._RepeatState = 'context'
|
|
9376
|
+
|
|
9377
|
+
# trace.
|
|
9378
|
+
_logsi.LogObject(SILevel.Verbose, "Current Spotify player PlayState (built from Sonos device current state) for device: %s" % (scDevice.Title), playerState, excludeNonPublic=True)
|
|
9379
|
+
return playerState
|
|
9380
|
+
|
|
9381
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
9382
|
+
except Exception as ex:
|
|
9383
|
+
|
|
9384
|
+
# format unhandled exception.
|
|
9385
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
9386
|
+
|
|
9387
|
+
finally:
|
|
9388
|
+
|
|
9389
|
+
# trace.
|
|
9390
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
9391
|
+
|
|
9392
|
+
|
|
9162
9393
|
def GetSpotifyConnectDevice(
|
|
9163
9394
|
self,
|
|
9164
9395
|
deviceValue:str,
|
|
@@ -9245,6 +9476,7 @@ class SpotifyClient:
|
|
|
9245
9476
|
apiMethodName:str = 'GetSpotifyConnectDevice'
|
|
9246
9477
|
apiMethodParms:SIMethodParmListContext = None
|
|
9247
9478
|
scDevice:SpotifyConnectDevice = None
|
|
9479
|
+
sonosPlayer:Soco = None
|
|
9248
9480
|
|
|
9249
9481
|
try:
|
|
9250
9482
|
|
|
@@ -9292,7 +9524,6 @@ class SpotifyClient:
|
|
|
9292
9524
|
# activate the spotify cast application on the device.
|
|
9293
9525
|
_logsi.LogVerbose("Activating Chromecast Spotify Connect device: %s" % (scDevice.Title))
|
|
9294
9526
|
self._SpotifyConnectDirectory.ActivateCastAppSpotify(scDevice.Name, transferPlayback=False)
|
|
9295
|
-
scDevice.WasReConnected = True
|
|
9296
9527
|
|
|
9297
9528
|
# re-fetch device instance, as it has updated Zeroconf Response data.
|
|
9298
9529
|
# do not need to refresh from Spotify Web API, as only zeroconf data was changed.
|
|
@@ -9304,7 +9535,7 @@ class SpotifyClient:
|
|
|
9304
9535
|
# if this is a Sonos device and there is no Spotify Client Application token then don't bother.
|
|
9305
9536
|
# in this case, we will exit and let the calling method switch to the Sonos device and play
|
|
9306
9537
|
# using the Sonos local queue.
|
|
9307
|
-
if (
|
|
9538
|
+
if (scDevice.IsSonos):
|
|
9308
9539
|
|
|
9309
9540
|
# check if the Spotify Desktop Application Client oauth2 token is defined.
|
|
9310
9541
|
hasToken:bool = AuthClient.HasTokenForKey(
|
|
@@ -9320,6 +9551,10 @@ class SpotifyClient:
|
|
|
9320
9551
|
_logsi.LogVerbose("Spotify Desktop Application Client oauth2 token was not found for Spotify LoginId: '%s'" % (self._SpotifyConnectLoginId))
|
|
9321
9552
|
return scDevice
|
|
9322
9553
|
|
|
9554
|
+
# get Sonos Controller instance for the device.
|
|
9555
|
+
_logsi.LogVerbose("Target device is Sonos; getting Sonos Controller instance for device %s" % (scDevice.Title))
|
|
9556
|
+
sonosPlayer = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
|
|
9557
|
+
|
|
9323
9558
|
# at this point, we will try to activate the device.
|
|
9324
9559
|
|
|
9325
9560
|
# was device activation requested? if not, then we are done.
|
|
@@ -9344,87 +9579,20 @@ class SpotifyClient:
|
|
|
9344
9579
|
tokenStorageDir=self.TokenStorageDir,
|
|
9345
9580
|
tokenStorageFile=self.TokenStorageFile)
|
|
9346
9581
|
|
|
9347
|
-
#
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
# # some devices only support a single connection on the spotify connect
|
|
9361
|
-
# # zeroconf webserver, so we have to give it a little time to process the
|
|
9362
|
-
# # disconnect command before we try to call the Connect method.
|
|
9363
|
-
# _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % DELAY_DISCONNECT)
|
|
9364
|
-
# time.sleep(DELAY_DISCONNECT)
|
|
9365
|
-
|
|
9366
|
-
# # some manufacturers will reset the device host ip port after a disconnect (e.g. "Denon", etc).
|
|
9367
|
-
# # due to this, the device will de-register itself via zeroconf and then re-register itself.
|
|
9368
|
-
# # we will now wait for the device to be re-discovered by zeroconf.
|
|
9369
|
-
# loopTotalDelay:float = 0
|
|
9370
|
-
# LOOP_DELAY:float = 0.25
|
|
9371
|
-
# while True:
|
|
9372
|
-
|
|
9373
|
-
# # wait just a bit between available device list queries.
|
|
9374
|
-
# _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % LOOP_DELAY)
|
|
9375
|
-
# time.sleep(LOOP_DELAY)
|
|
9376
|
-
# loopTotalDelay = loopTotalDelay + LOOP_DELAY
|
|
9377
|
-
|
|
9378
|
-
# with self._SpotifyConnectDirectory._Zeroconf_Lock:
|
|
9379
|
-
|
|
9380
|
-
# # check for the device in the available device list.
|
|
9381
|
-
# # if we find it, then we are done.
|
|
9382
|
-
|
|
9383
|
-
# device:Device = self.GetPlayerDevice(deviceResultId, True)
|
|
9384
|
-
# if device is not None:
|
|
9385
|
-
# _logsi.LogVerbose("Spotify Connect device \"%s\" is now in the available device list; device found within %f seconds of Connect" % (scDevice.Title, loopTotalDelay))
|
|
9386
|
-
# break
|
|
9387
|
-
|
|
9582
|
+
# is this a Sonos device?
|
|
9583
|
+
if (scDevice.IsSonos):
|
|
9584
|
+
|
|
9585
|
+
# trace.
|
|
9586
|
+
sonosMusicSource:str = sonosPlayer.music_source
|
|
9587
|
+
_logsi.LogVerbose("Sonos device %s music source before Connect: \"%s\"" % (scDevice.Title, sonosMusicSource))
|
|
9588
|
+
|
|
9589
|
+
# was the Sonos device music source set to SPOTIFY_CONNECT?
|
|
9590
|
+
# if not, then issue a Disconnect to (hopefully) reset the music source. this should allow
|
|
9591
|
+
# the subsequent Connect to re-establish a SPOTIFY_CONNECT music source on the Sonos device.
|
|
9592
|
+
if sonosMusicSource != "SPOTIFY_CONNECT":
|
|
9593
|
+
_logsi.LogVerbose("Issuing Disconnect to Sonos Spotify Connect device %s" % (scDevice.Title))
|
|
9594
|
+
zcfResult = zconn.Disconnect(ignoreStatusResult=True)
|
|
9388
9595
|
|
|
9389
|
-
# # re-discover Spotify Connect devices on the network
|
|
9390
|
-
# _logsi.LogVerbose("Rediscovering Spotify Connect devices on the local network (due to previous disconnect)")
|
|
9391
|
-
# rediscovery:SpotifyDiscovery = SpotifyDiscovery(self._ZeroconfClient, printToConsole=False)
|
|
9392
|
-
# rediscovery.DiscoverDevices(timeout=self._SpotifyConnectDiscoveryTimeout)
|
|
9393
|
-
|
|
9394
|
-
# # trace.
|
|
9395
|
-
# _logsi.LogDictionary(SILevel.Verbose, "Rediscovered Spotify Connect devices on the local network (due to previous disconnect)", rediscovery.DiscoveryResults, prettyPrint=True)
|
|
9396
|
-
|
|
9397
|
-
# # process all rediscovered devices, and update selected device.
|
|
9398
|
-
# rediscovery:ZeroconfDiscoveryResult
|
|
9399
|
-
# for rediscovery in rediscovery.DiscoveryResults:
|
|
9400
|
-
|
|
9401
|
-
# # add / update selected device discovery results by serviceinfo key value.
|
|
9402
|
-
# if (rediscovery.Key == scDevice.DiscoveryResult.Key):
|
|
9403
|
-
|
|
9404
|
-
# # save old discovery result id so we can log it later.
|
|
9405
|
-
# oldDiscoveryResultId:str = discovery.Id
|
|
9406
|
-
|
|
9407
|
-
# # update selected device discovery result.
|
|
9408
|
-
# _logsi.LogObject(SILevel.Verbose, "Spotify Connect device \"%s\" DiscoveryResult instance rediscovered properties" % (rediscovery.Id), rediscovery, excludeNonPublic=True)
|
|
9409
|
-
# _logsi.LogVerbose("Updating Spotify Connect device \"%s\" DiscoveryResult instance with rediscovered properties" % (scDevice.Title))
|
|
9410
|
-
# scDevice.DiscoveryResult = rediscovery
|
|
9411
|
-
# discovery = rediscovery
|
|
9412
|
-
|
|
9413
|
-
# # did the host ip address or port change?
|
|
9414
|
-
# # if so, then we need to recreate the ZeroconfConnect instance
|
|
9415
|
-
# # in order to access the updated ip address and port.
|
|
9416
|
-
# if (discovery.HostIpAddress != zconn.HostIpAddress) \
|
|
9417
|
-
# or (discovery.HostIpPort != zconn.HostIpPort):
|
|
9418
|
-
|
|
9419
|
-
# _logsi.LogVerbose("Spotify Connect device info changed to %s from %s after disconnect; recreating ZeroconfConnect instance" % (discovery.Id, oldDiscoveryResultId))
|
|
9420
|
-
# zconn = ZeroconfConnect(discovery.HostIpAddress,
|
|
9421
|
-
# discovery.HostIpPort,
|
|
9422
|
-
# discovery.SpotifyConnectCPath,
|
|
9423
|
-
# useSSL=False,
|
|
9424
|
-
# tokenStorageDir=self.TokenStorageDir,
|
|
9425
|
-
# tokenStorageFile=self.TokenStorageFile)
|
|
9426
|
-
# break
|
|
9427
|
-
|
|
9428
9596
|
# connect the device to OUR Spotify Connect user context.
|
|
9429
9597
|
# note that the result here only indicates that the connect was submitted - NOT that it was successful!
|
|
9430
9598
|
_logsi.LogVerbose("Issuing Connect to Spotify Connect device %s for user context \"%s\" (ip=%s:%s)" % (scDevice.Title, self._SpotifyConnectLoginId, zconn.HostIpAddress, zconn.HostIpPort))
|
|
@@ -9436,6 +9604,26 @@ class SpotifyClient:
|
|
|
9436
9604
|
# trace.
|
|
9437
9605
|
_logsi.LogVerbose("User context switched from \"%s\" to \"%s\" for Spotify Connect device %s" % (oldActiveUser, self._SpotifyConnectLoginId, scDevice.Title))
|
|
9438
9606
|
|
|
9607
|
+
# is this a Sonos device?
|
|
9608
|
+
if (scDevice.IsSonos):
|
|
9609
|
+
|
|
9610
|
+
# trace.
|
|
9611
|
+
sonosMusicSource:str = sonosPlayer.music_source
|
|
9612
|
+
_logsi.LogVerbose("Sonos device %s music source after Connect: \"%s\"" % (scDevice.Title, sonosMusicSource))
|
|
9613
|
+
|
|
9614
|
+
# was the Sonos device music source set to Spotify Connect?
|
|
9615
|
+
# if not, then it's a lost cause at this point since it's probably in an UNKNOWN state.
|
|
9616
|
+
if sonosMusicSource != "SPOTIFY_CONNECT":
|
|
9617
|
+
_logsi.LogVerbose("Sonos device %s music source after Connect is not SPOTIFY_CONNECT; it will probably fail to play" % (scDevice.Title))
|
|
9618
|
+
|
|
9619
|
+
# at this point the Sonos music source should be set to SPOTIFY_CONNECT, and control
|
|
9620
|
+
# transferred to the device. note that the Sonos device will still NOT appear in the
|
|
9621
|
+
# player device list, but SHOULD be the active (restricted) device in player state.
|
|
9622
|
+
|
|
9623
|
+
# wait for the device to enter the Spotify Web API player device list, or
|
|
9624
|
+
# to become the active device.
|
|
9625
|
+
# Sonos devices are famous for not showing up in the device list, but DO
|
|
9626
|
+
# appear as the active (restricted) device!
|
|
9439
9627
|
loopTotalDelay:float = 0
|
|
9440
9628
|
LOOP_DELAY:float = 0.350
|
|
9441
9629
|
while True:
|
|
@@ -9468,7 +9656,7 @@ class SpotifyClient:
|
|
|
9468
9656
|
# only check so many times before we give up;
|
|
9469
9657
|
# we will keep checking until we find the device, or we exceed the verifyTimeout value.
|
|
9470
9658
|
if (loopTotalDelay > verifyTimeout):
|
|
9471
|
-
_logsi.LogWarning("Verification timeout waiting for Spotify Connect device %s to be added to the player device list; device not found within %f seconds of Connect" % (scDevice.Title, verifyTimeout))
|
|
9659
|
+
_logsi.LogWarning("Verification timeout waiting for Spotify Connect device %s to be added to the player device list or become the active device; device not found within %f seconds of Connect" % (scDevice.Title, verifyTimeout))
|
|
9472
9660
|
break
|
|
9473
9661
|
|
|
9474
9662
|
# return the device instance.
|
|
@@ -11219,15 +11407,24 @@ class SpotifyClient:
|
|
|
11219
11407
|
# resolve / activate the device object if needed.
|
|
11220
11408
|
scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
|
|
11221
11409
|
|
|
11222
|
-
#
|
|
11223
|
-
|
|
11224
|
-
urlParms['device_id'] = scDevice.Id
|
|
11410
|
+
# is this an active Sonos device?
|
|
11411
|
+
if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
|
|
11225
11412
|
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11413
|
+
_logsi.LogVerbose("Issuing command to Sonos device %s: PAUSE" % (scDevice.Title))
|
|
11414
|
+
sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
|
|
11415
|
+
sonosPlayer.pause()
|
|
11416
|
+
|
|
11417
|
+
else:
|
|
11418
|
+
|
|
11419
|
+
# build spotify web api request parameters.
|
|
11420
|
+
urlParms:dict = {}
|
|
11421
|
+
urlParms['device_id'] = scDevice.Id
|
|
11422
|
+
|
|
11423
|
+
# execute spotify web api request.
|
|
11424
|
+
msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/pause')
|
|
11425
|
+
msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
|
|
11426
|
+
msg.UrlParameters = urlParms
|
|
11427
|
+
self.MakeRequest('PUT', msg)
|
|
11231
11428
|
|
|
11232
11429
|
# give spotify web api time to process the change.
|
|
11233
11430
|
if delay > 0:
|
|
@@ -11734,15 +11931,24 @@ class SpotifyClient:
|
|
|
11734
11931
|
# resolve / activate the device object if needed.
|
|
11735
11932
|
scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
|
|
11736
11933
|
|
|
11737
|
-
#
|
|
11738
|
-
|
|
11739
|
-
urlParms['device_id'] = scDevice.Id
|
|
11934
|
+
# is this an active Sonos device?
|
|
11935
|
+
if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
|
|
11740
11936
|
|
|
11741
|
-
|
|
11742
|
-
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11937
|
+
_logsi.LogVerbose("Issuing command to Sonos device %s: PLAY (RESUME)" % (scDevice.Title))
|
|
11938
|
+
sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
|
|
11939
|
+
sonosPlayer.play()
|
|
11940
|
+
|
|
11941
|
+
else:
|
|
11942
|
+
|
|
11943
|
+
# build spotify web api request parameters.
|
|
11944
|
+
urlParms:dict = {}
|
|
11945
|
+
urlParms['device_id'] = scDevice.Id
|
|
11946
|
+
|
|
11947
|
+
# execute spotify web api request.
|
|
11948
|
+
msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/play')
|
|
11949
|
+
msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
|
|
11950
|
+
msg.UrlParameters = urlParms
|
|
11951
|
+
self.MakeRequest('PUT', msg)
|
|
11746
11952
|
|
|
11747
11953
|
# give spotify web api time to process the change.
|
|
11748
11954
|
if delay > 0:
|
|
@@ -11873,18 +12079,28 @@ class SpotifyClient:
|
|
|
11873
12079
|
newPositionMS = 0
|
|
11874
12080
|
positionMS = newPositionMS
|
|
11875
12081
|
|
|
11876
|
-
#
|
|
11877
|
-
|
|
11878
|
-
{
|
|
11879
|
-
'position_ms': positionMS
|
|
11880
|
-
}
|
|
11881
|
-
urlParms['device_id'] = scDevice.Id
|
|
12082
|
+
# is this an active Sonos device?
|
|
12083
|
+
if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
|
|
11882
12084
|
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
|
|
12085
|
+
sonosPosition:str = mediaPositionHMS_fromSeconds(positionMS / 1000) # convert from milliseconds to Sonos H:MM:SS format
|
|
12086
|
+
_logsi.LogVerbose("Issuing command to Sonos device %s: SEEK (sonosPosition=%s)" % (scDevice.Title, sonosPosition))
|
|
12087
|
+
sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
|
|
12088
|
+
sonosPlayer.seek(str(sonosPosition))
|
|
12089
|
+
|
|
12090
|
+
else:
|
|
12091
|
+
|
|
12092
|
+
# build spotify web api request parameters.
|
|
12093
|
+
urlParms:dict = \
|
|
12094
|
+
{
|
|
12095
|
+
'position_ms': positionMS
|
|
12096
|
+
}
|
|
12097
|
+
urlParms['device_id'] = scDevice.Id
|
|
12098
|
+
|
|
12099
|
+
# execute spotify web api request.
|
|
12100
|
+
msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/seek')
|
|
12101
|
+
msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
|
|
12102
|
+
msg.UrlParameters = urlParms
|
|
12103
|
+
self.MakeRequest('PUT', msg)
|
|
11888
12104
|
|
|
11889
12105
|
# give spotify web api time to process the change.
|
|
11890
12106
|
if delay > 0:
|
|
@@ -11966,15 +12182,24 @@ class SpotifyClient:
|
|
|
11966
12182
|
# resolve / activate the device object if needed.
|
|
11967
12183
|
scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
|
|
11968
12184
|
|
|
11969
|
-
#
|
|
11970
|
-
|
|
11971
|
-
urlParms['device_id'] = scDevice.Id
|
|
12185
|
+
# is this an active Sonos device?
|
|
12186
|
+
if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
|
|
11972
12187
|
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
12188
|
+
_logsi.LogVerbose("Issuing command to Sonos device %s: NEXT" % (scDevice.Title))
|
|
12189
|
+
sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
|
|
12190
|
+
sonosPlayer.next()
|
|
12191
|
+
|
|
12192
|
+
else:
|
|
12193
|
+
|
|
12194
|
+
# build spotify web api request parameters.
|
|
12195
|
+
urlParms:dict = {}
|
|
12196
|
+
urlParms['device_id'] = scDevice.Id
|
|
12197
|
+
|
|
12198
|
+
# execute spotify web api request.
|
|
12199
|
+
msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/next')
|
|
12200
|
+
msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
|
|
12201
|
+
msg.UrlParameters = urlParms
|
|
12202
|
+
self.MakeRequest('POST', msg)
|
|
11978
12203
|
|
|
11979
12204
|
# give spotify web api time to process the change.
|
|
11980
12205
|
if delay > 0:
|
|
@@ -12056,15 +12281,24 @@ class SpotifyClient:
|
|
|
12056
12281
|
# resolve / activate the device object if needed.
|
|
12057
12282
|
scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
|
|
12058
12283
|
|
|
12059
|
-
#
|
|
12060
|
-
|
|
12061
|
-
urlParms['device_id'] = scDevice.Id
|
|
12284
|
+
# is this an active Sonos device?
|
|
12285
|
+
if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
|
|
12062
12286
|
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
12066
|
-
|
|
12067
|
-
|
|
12287
|
+
_logsi.LogVerbose("Issuing command to Sonos device %s: PREVIOUS" % (scDevice.Title))
|
|
12288
|
+
sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
|
|
12289
|
+
sonosPlayer.previous()
|
|
12290
|
+
|
|
12291
|
+
else:
|
|
12292
|
+
|
|
12293
|
+
# build spotify web api request parameters.
|
|
12294
|
+
urlParms:dict = {}
|
|
12295
|
+
urlParms['device_id'] = scDevice.Id
|
|
12296
|
+
|
|
12297
|
+
# execute spotify web api request.
|
|
12298
|
+
msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/previous')
|
|
12299
|
+
msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
|
|
12300
|
+
msg.UrlParameters = urlParms
|
|
12301
|
+
self.MakeRequest('POST', msg)
|
|
12068
12302
|
|
|
12069
12303
|
# give spotify web api time to process the change.
|
|
12070
12304
|
if delay > 0:
|
|
@@ -12154,18 +12388,44 @@ class SpotifyClient:
|
|
|
12154
12388
|
# resolve / activate the device object if needed.
|
|
12155
12389
|
scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
|
|
12156
12390
|
|
|
12157
|
-
#
|
|
12158
|
-
|
|
12159
|
-
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
|
|
12391
|
+
# is this an active Sonos device?
|
|
12392
|
+
if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
|
|
12393
|
+
|
|
12394
|
+
# get current Spotify Connect player state.
|
|
12395
|
+
playerState = self.GetSonosPlayerPlaybackState(scDevice)
|
|
12396
|
+
|
|
12397
|
+
# set the Sonos PlayMode directly (instead of setting the Sonos repeat property)!
|
|
12398
|
+
# The Spotify Web API PlayerState `ShuffleState` value can sometimes be different than what the
|
|
12399
|
+
# Sonos device reports for its `shuffle` property value!
|
|
12400
|
+
if state == 'track':
|
|
12401
|
+
sonos_repeat = 'ONE'
|
|
12402
|
+
elif state == 'off':
|
|
12403
|
+
sonos_repeat = False
|
|
12404
|
+
elif state == 'context':
|
|
12405
|
+
sonos_repeat = True
|
|
12406
|
+
else:
|
|
12407
|
+
sonos_repeat = False
|
|
12408
|
+
playMode:str = SONOS_PLAY_MODE_BY_MEANING[(playerState.ShuffleState, sonos_repeat)]
|
|
12409
|
+
|
|
12410
|
+
# execute SoCo api request.
|
|
12411
|
+
_logsi.LogVerbose("Issuing command to Sonos device %s: REPEAT (playmode=%s)" % (scDevice.Title, playMode))
|
|
12412
|
+
sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
|
|
12413
|
+
sonosPlayer.play_mode = playMode
|
|
12163
12414
|
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
|
|
12167
|
-
|
|
12168
|
-
|
|
12415
|
+
else:
|
|
12416
|
+
|
|
12417
|
+
# build spotify web api request parameters.
|
|
12418
|
+
urlParms:dict = \
|
|
12419
|
+
{
|
|
12420
|
+
'state': state
|
|
12421
|
+
}
|
|
12422
|
+
urlParms['device_id'] = scDevice.Id
|
|
12423
|
+
|
|
12424
|
+
# execute spotify web api request.
|
|
12425
|
+
msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/repeat')
|
|
12426
|
+
msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
|
|
12427
|
+
msg.UrlParameters = urlParms
|
|
12428
|
+
self.MakeRequest('PUT', msg)
|
|
12169
12429
|
|
|
12170
12430
|
# give spotify web api time to process the change.
|
|
12171
12431
|
if delay > 0:
|
|
@@ -12254,18 +12514,45 @@ class SpotifyClient:
|
|
|
12254
12514
|
# resolve / activate the device object if needed.
|
|
12255
12515
|
scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
|
|
12256
12516
|
|
|
12257
|
-
#
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12517
|
+
# is this an active Sonos device?
|
|
12518
|
+
if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
|
|
12519
|
+
|
|
12520
|
+
# get current Spotify Connect player state.
|
|
12521
|
+
playerState = self.GetSonosPlayerPlaybackState(scDevice)
|
|
12522
|
+
|
|
12523
|
+
# set the Sonos PlayMode directly (instead of setting the Sonos `shuffle` property)!
|
|
12524
|
+
# the Spotify Web API PlayerState `RepeatState` value can sometimes be different than what the
|
|
12525
|
+
# Sonos device reports for its `repeat` property value!
|
|
12526
|
+
sonos_repeat:str
|
|
12527
|
+
if playerState.RepeatState == 'track':
|
|
12528
|
+
sonos_repeat = 'ONE'
|
|
12529
|
+
elif playerState.RepeatState == 'off':
|
|
12530
|
+
sonos_repeat = False
|
|
12531
|
+
elif playerState.RepeatState == 'context':
|
|
12532
|
+
sonos_repeat = True
|
|
12533
|
+
else: # assume off if nothing else.
|
|
12534
|
+
sonos_repeat = False
|
|
12535
|
+
playMode:str = SONOS_PLAY_MODE_BY_MEANING[(state, sonos_repeat)]
|
|
12536
|
+
|
|
12537
|
+
# execute SoCo api request.
|
|
12538
|
+
_logsi.LogVerbose("Issuing command to Sonos device %s: SHUFFLE (playmode=%s)" % (scDevice.Title, playMode))
|
|
12539
|
+
sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
|
|
12540
|
+
sonosPlayer.play_mode = playMode
|
|
12263
12541
|
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12542
|
+
else:
|
|
12543
|
+
|
|
12544
|
+
# build spotify web api request parameters.
|
|
12545
|
+
urlParms:dict = \
|
|
12546
|
+
{
|
|
12547
|
+
'state': state
|
|
12548
|
+
}
|
|
12549
|
+
urlParms['device_id'] = scDevice.Id
|
|
12550
|
+
|
|
12551
|
+
# execute spotify web api request.
|
|
12552
|
+
msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/shuffle')
|
|
12553
|
+
msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
|
|
12554
|
+
msg.UrlParameters = urlParms
|
|
12555
|
+
self.MakeRequest('PUT', msg)
|
|
12269
12556
|
|
|
12270
12557
|
# give spotify web api time to process the change.
|
|
12271
12558
|
if delay > 0:
|
|
@@ -12353,18 +12640,27 @@ class SpotifyClient:
|
|
|
12353
12640
|
# resolve / activate the device object if needed.
|
|
12354
12641
|
scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
|
|
12355
12642
|
|
|
12356
|
-
#
|
|
12357
|
-
|
|
12358
|
-
{
|
|
12359
|
-
'volume_percent': volumePercent
|
|
12360
|
-
}
|
|
12361
|
-
urlParms['device_id'] = scDevice.Id
|
|
12643
|
+
# is this an active Sonos device?
|
|
12644
|
+
if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
|
|
12362
12645
|
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
|
|
12646
|
+
_logsi.LogVerbose("Issuing command to Sonos device %s: VOLUME = %s" % (scDevice.Title, volumePercent))
|
|
12647
|
+
sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
|
|
12648
|
+
sonosPlayer.volume = volumePercent
|
|
12649
|
+
|
|
12650
|
+
else:
|
|
12651
|
+
|
|
12652
|
+
# build spotify web api request parameters.
|
|
12653
|
+
urlParms:dict = \
|
|
12654
|
+
{
|
|
12655
|
+
'volume_percent': volumePercent
|
|
12656
|
+
}
|
|
12657
|
+
urlParms['device_id'] = scDevice.Id
|
|
12658
|
+
|
|
12659
|
+
# execute spotify web api request.
|
|
12660
|
+
msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/volume')
|
|
12661
|
+
msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
|
|
12662
|
+
msg.UrlParameters = urlParms
|
|
12663
|
+
self.MakeRequest('PUT', msg)
|
|
12368
12664
|
|
|
12369
12665
|
# give spotify web api time to process the change.
|
|
12370
12666
|
if delay > 0:
|
|
@@ -12474,10 +12770,40 @@ class SpotifyClient:
|
|
|
12474
12770
|
|
|
12475
12771
|
# is this a Sonos device?
|
|
12476
12772
|
# if so, then control was automatically transferred by activating the device.
|
|
12477
|
-
# note that
|
|
12478
|
-
# considered a restricted device and the Spotify Web API cannot control it!
|
|
12479
|
-
|
|
12480
|
-
|
|
12773
|
+
# note that the Spotify Web API cannot stop nor start play on the device either,
|
|
12774
|
+
# as it's considered a restricted device and the Spotify Web API cannot control it!
|
|
12775
|
+
# all requests to control the device will be made via the Sonos Controller API.
|
|
12776
|
+
if (scDevice.IsSonos):
|
|
12777
|
+
|
|
12778
|
+
# get Sonos Controller instance for the device.
|
|
12779
|
+
sonosPlayer:SoCo = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
|
|
12780
|
+
|
|
12781
|
+
# get current Sonos transport status.
|
|
12782
|
+
sonosTransportInfo:dict = sonosPlayer.get_current_transport_info()
|
|
12783
|
+
currentTransportState:str = sonosTransportInfo.get('current_transport_state', None)
|
|
12784
|
+
currentTransportStatus:str = sonosTransportInfo.get('current_transport_status', None)
|
|
12785
|
+
_logsi.LogVerbose("Sonos device %s current transport state after activation: \"%s\" (Status=%s)" % (scDevice.Title, currentTransportState, currentTransportStatus))
|
|
12786
|
+
|
|
12787
|
+
# stop / start play as requested.
|
|
12788
|
+
wasCmdIssued:bool = False
|
|
12789
|
+
if currentTransportState == 'PLAYING':
|
|
12790
|
+
if play == False:
|
|
12791
|
+
_logsi.LogVerbose("Issuing command to Sonos device %s: PAUSE" % (scDevice.Title))
|
|
12792
|
+
sonosPlayer.pause()
|
|
12793
|
+
wasCmdIssued = True
|
|
12794
|
+
elif currentTransportState in ['PAUSED_PLAYBACK','STOPPED','TRANSITIONING']:
|
|
12795
|
+
if play == True:
|
|
12796
|
+
_logsi.LogVerbose("Issuing command to Sonos device %s: PLAY" % (scDevice.Title))
|
|
12797
|
+
sonosPlayer.play()
|
|
12798
|
+
wasCmdIssued = True
|
|
12799
|
+
|
|
12800
|
+
# give SoCo api time to process the change.
|
|
12801
|
+
if (wasCmdIssued) and (delay > 0):
|
|
12802
|
+
_logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE_SONOS % delay)
|
|
12803
|
+
time.sleep(delay)
|
|
12804
|
+
|
|
12805
|
+
# trace.
|
|
12806
|
+
_logsi.LogVerbose("Sonos device detected (IsRestricted); bypassing call to Spotify Web API Transfer Playback endpoint")
|
|
12481
12807
|
return
|
|
12482
12808
|
|
|
12483
12809
|
# before transferring playback to the device, we first need to check to see if anything
|
|
@@ -12495,10 +12821,9 @@ class SpotifyClient:
|
|
|
12495
12821
|
return
|
|
12496
12822
|
|
|
12497
12823
|
# if resolved device is already active, then there is nothing to do.
|
|
12498
|
-
if (
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
return
|
|
12824
|
+
if (scActiveDevice.Name == scDevice.Name):
|
|
12825
|
+
_logsi.LogVerbose("Spotify Connect device %s is already active; no need to re-activate" % (scDevice.Title))
|
|
12826
|
+
return
|
|
12502
12827
|
|
|
12503
12828
|
# build spotify web api request parameters.
|
|
12504
12829
|
reqData:dict = \
|