spotifywebapiPython 1.0.78__py3-none-any.whl → 1.0.80__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.

Files changed (25) hide show
  1. {spotifywebapiPython-1.0.78.dist-info → spotifywebapiPython-1.0.80.dist-info}/METADATA +1 -1
  2. {spotifywebapiPython-1.0.78.dist-info → spotifywebapiPython-1.0.80.dist-info}/RECORD +25 -25
  3. spotifywebapipython/const.py +6 -1
  4. spotifywebapipython/docs/__init__.html +18 -3
  5. spotifywebapipython/docs/index.html +2 -2
  6. spotifywebapipython/docs/search.js +1 -1
  7. spotifywebapipython/docs/spotifywebapiPython/const.html +20 -3
  8. spotifywebapipython/docs/spotifywebapiPython/saappmessages.html +2 -2
  9. spotifywebapipython/docs/spotifywebapiPython/sautils.html +2 -2
  10. spotifywebapipython/docs/spotifywebapiPython/spotifyapierror.html +2 -2
  11. spotifywebapipython/docs/spotifywebapiPython/spotifyapimessage.html +2 -2
  12. spotifywebapipython/docs/spotifywebapiPython/spotifyauthtoken.html +38 -2
  13. spotifywebapipython/docs/spotifywebapiPython/spotifyclient.html +168 -72
  14. spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +2 -2
  15. spotifywebapipython/docs/spotifywebapiPython/spotifywebapiauthenticationerror.html +2 -2
  16. spotifywebapipython/docs/spotifywebapiPython/spotifywebapierror.html +2 -2
  17. spotifywebapipython/models/spotifyconnectdevice.py +60 -1
  18. spotifywebapipython/models/spotifyconnectdevices.py +2 -0
  19. spotifywebapipython/spotifyauthtoken.py +26 -0
  20. spotifywebapipython/spotifyclient.py +440 -195
  21. spotifywebapipython/zeroconfapi/zeroconfconnect.py +52 -46
  22. spotifywebapipython/zeroconfapi/zeroconfgetinfoalias.py +8 -0
  23. {spotifywebapiPython-1.0.78.dist-info → spotifywebapiPython-1.0.80.dist-info}/LICENSE +0 -0
  24. {spotifywebapiPython-1.0.78.dist-info → spotifywebapiPython-1.0.80.dist-info}/WHEEL +0 -0
  25. {spotifywebapiPython-1.0.78.dist-info → spotifywebapiPython-1.0.80.dist-info}/top_level.txt +0 -0
@@ -119,7 +119,7 @@ class SpotifyClient:
119
119
  The `spotifyConnectUsername`, `spotifyConnectPassword` and `spotifyConnectLoginId` arguments are only used
120
120
  when a Spotify Connect account switch is performed on a selected player device. Note that these credentials
121
121
  are NOT used to access the Spotify Web API (the Spotify Web API token is used for that).
122
- Please see the `PlayerResolveDeviceId` method for more details.
122
+ Please see the `GetSpotifyConnectDevice` method for more details.
123
123
 
124
124
  Note that if you don't have a password setup for your Spotify account (e.g. you utilize the "Continue with Google"
125
125
  or other non-password methods for login), then you will need to define a "device password" in order to use the
@@ -6084,35 +6084,39 @@ class SpotifyClient:
6084
6084
  # store the currently active user of the device, in case we need to switch users later on.
6085
6085
  deviceActiveUser = info.ActiveUser.lower()
6086
6086
  deviceIdResult = info.DeviceId
6087
-
6087
+
6088
6088
  status:str = "INACTIVE, "
6089
6089
  if (info.HasActiveUser):
6090
6090
  status = "ACTIVE, "
6091
6091
 
6092
6092
  # did we resolve the device id?
6093
6093
  if deviceIdResult is not None:
6094
-
6095
- # is this a dynamic device? if so, then we can't control it.
6094
+
6095
+ # reset device reconnected flag.
6096
+ scDevice.WasReConnected = False
6097
+
6098
+ # is this a dynamic device? if so, then there is no need to activate it as it's already active.
6096
6099
  if (discoverResult.IsDynamicDevice):
6097
- _logsi.LogVerbose("Spotify Connect Device id '%s' (%s) is a dynamic device; we cannot control it" % (deviceIdResult, info.RemoteName))
6100
+ _logsi.LogVerbose("Activation not required for dynamic Spotify Connect device: '%s'" % (scDevice.Title))
6098
6101
  status = status + "dynamic device, cannot switch user context"
6099
6102
 
6100
6103
  # does our Spotify Connect user account need to take control of the device?
6101
6104
  # if not, then we are done.
6102
- # this takes into account if the device is in the active device list, as well
6105
+ # this takes into account if the device is in the available device list, as well
6103
6106
  # as if the active user is not our user context.
6104
6107
  elif (info.IsInDeviceList == True) \
6105
6108
  and ((deviceActiveUser == self._SpotifyConnectUsername.lower()) or (deviceActiveUser == self.UserProfile.Id.lower())):
6106
- _logsi.LogVerbose("Spotify Connect user context '%s' was verified for Device id '%s'; switch not necessary" % (deviceActiveUser, deviceIdResult))
6109
+ _logsi.LogVerbose("User context '%s' was verified for Spotify Connect device '%s'; switch not necessary" % (deviceActiveUser, scDevice.Title))
6107
6110
  status = status + "user context switch not needed"
6108
6111
 
6109
6112
  # did caller request user context switch bypass?
6110
6113
  # if so, then just return the device id.
6111
6114
  elif (info.HasActiveUser) and (not verifyUserContext):
6112
- _logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as caller chose to bypass user context switch" % (deviceActiveUser, deviceIdResult))
6115
+ _logsi.LogVerbose("User context '%s' will be used for Spotify Connect device '%s', as user chose to bypass user context switch" % (deviceActiveUser, scDevice.Title))
6113
6116
  status = status + "caller bypassed user context switch"
6114
6117
 
6115
6118
  else:
6119
+
6116
6120
  zcfResult:ZeroconfResponse
6117
6121
 
6118
6122
  # create a ZeroconfConnect object to access the device.
@@ -6128,18 +6132,21 @@ class SpotifyClient:
6128
6132
  # if a different user context has control of the device then we need to disconnect
6129
6133
  # the current user context before connecting a different user.
6130
6134
  if (info.HasActiveUser):
6131
- _logsi.LogVerbose("Spotify Connect user context '%s' is being disconnected for Device id '%s'" % (deviceActiveUser, deviceIdResult))
6135
+ _logsi.LogVerbose("Issuing Disconnect to Spotify Connect device '%s' for user context '%s'" % (scDevice.Title, deviceActiveUser))
6132
6136
  zcfResult = zconn.Disconnect(delay)
6133
6137
  status = status + 'disconnected from user context "%s";' % deviceActiveUser
6134
6138
 
6135
6139
  # connect the device to OUR Spotify Connect user context.
6136
6140
  # note that the result here only indicates that the connect was submitted - NOT that it was successful!
6137
- _logsi.LogVerbose("Spotify Connect user context '%s' is being connected for Device id '%s'" % (self._SpotifyConnectUsername, deviceIdResult))
6141
+ _logsi.LogVerbose("Issuing Connect to Spotify Connect device '%s' for user context '%s'" % (scDevice.Title, self._SpotifyConnectUsername))
6138
6142
  zcfResult = zconn.Connect(self._SpotifyConnectUsername, self._SpotifyConnectPassword, self._SpotifyConnectLoginId, delay=delay)
6139
6143
  status = status + 'connected to user context "%s"' % self._SpotifyConnectUsername
6144
+
6145
+ # indicate device was reconnected.
6146
+ scDevice.WasReConnected = True
6140
6147
 
6141
6148
  # trace.
6142
- _logsi.LogVerbose("Spotify Connect user context switch from '%s' to '%s' was requested for Device id '%s'" % (deviceActiveUser, self._SpotifyConnectUsername, deviceIdResult))
6149
+ _logsi.LogVerbose("User context switch from '%s' to '%s' was requested for Spotify Connect device '%s'" % (deviceActiveUser, self._SpotifyConnectUsername, scDevice.Title))
6143
6150
 
6144
6151
  # append device status to results.
6145
6152
  result = "%s%s\n" % (result, MSG_RESULT % (info.RemoteName, info.DeviceId, info.ActiveUser, status))
@@ -6538,6 +6545,18 @@ class SpotifyClient:
6538
6545
 
6539
6546
  # process results.
6540
6547
  result = PlayerPlayState(root=msg.ResponseData)
6548
+
6549
+ # check for a restricted device; if found, then grab the device id from the
6550
+ # cached list of spotify connect devices (if present).
6551
+ if result is not None:
6552
+ device:Device = result.Device
6553
+ if (device is not None) and (device.Id is None or device.Id == ''):
6554
+ cacheKey:str = 'GetSpotifyConnectDevices'
6555
+ if (cacheKey in self._ConfigurationCache):
6556
+ devices:SpotifyConnectDevices = self._ConfigurationCache[cacheKey]
6557
+ scDevice:SpotifyConnectDevice = devices.GetDeviceByName(device.Name)
6558
+ if scDevice is not None:
6559
+ device.Id = scDevice.DeviceInfo.DeviceId
6541
6560
 
6542
6561
  # trace.
6543
6562
  _logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT_TYPE % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
@@ -7984,9 +8003,299 @@ class SpotifyClient:
7984
8003
  _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
7985
8004
 
7986
8005
 
8006
+ def GetSpotifyConnectDevice(
8007
+ self,
8008
+ deviceValue:str,
8009
+ verifyUserContext:bool=True,
8010
+ verifyTimeout:float=5.0,
8011
+ refreshDeviceList:bool=False,
8012
+ activateDevice:bool=True,
8013
+ delay:float=0.25
8014
+ ) -> Device:
8015
+ """
8016
+ Get information about a specific Spotify Connect player device, and (optionally)
8017
+ activate the device if it requires it.
8018
+
8019
+ This method requires the `user-read-playback-state` scope.
8020
+
8021
+ Resolves a Spotify Connect device from a specified device id, name, alias id,
8022
+ or alias name. This will ensure that the device can be found on the network, as well
8023
+ as connect to the device if necessary with the current user context.
8024
+
8025
+ Args:
8026
+ deviceValue (str):
8027
+ The device id / name value to check.
8028
+ verifyUserContext (bool):
8029
+ If True, the active user context of the resolved device is checked to ensure it
8030
+ matches the user context specified on the class constructor.
8031
+ If False, the user context will not be checked.
8032
+ Default is True.
8033
+ verifyTimeout (float):
8034
+ Maximum time to wait (in seconds) for the device to become active in the Spotify
8035
+ Connect device list. This value is only used if a Connect command has to be
8036
+ issued to activate the device.
8037
+ Default is 5; value range is 0 - 10.
8038
+ refreshDeviceList (bool):
8039
+ True to refresh the Spotify Connect device list; otherwise, False to use the
8040
+ Spotify Connect device list cache.
8041
+ activateDevice (bool):
8042
+ True to activate the device if necessary; otherwise, False.
8043
+ delay (float):
8044
+ Time delay (in seconds) to wait AFTER issuing any command to the device.
8045
+ This delay will give the spotify zeroconf api time to process the change before
8046
+ another command is issued.
8047
+ Default is 0.25; value range is 0 - 10.
8048
+
8049
+ Returns:
8050
+ A `Device` object for the deviceValue if one could be resolved;
8051
+ otherwise, a null value with the understanding that subsequent operations will probably
8052
+ fail since it's not in the Spotify Connect discovery list.
8053
+
8054
+ Raises:
8055
+ SpotifyWebApiError:
8056
+ If the Spotify Web API request was for a non-authorization service
8057
+ and the response contains error information.
8058
+ SpotifyZeroconfApiError:
8059
+ If any Spotify Zeroconf API request failed.
8060
+ SpotifApiError:
8061
+ If the method fails for any other reason.
8062
+
8063
+ The Spotify Connect discovery list is searched for the specified device value; the search will
8064
+ match on either a Device ID or RemoteName as well as any alias ID's or Names that are in use.
8065
+ The cached Spotify Connect discovery list will be used if the `refreshDeviceList` argument is
8066
+ False, or if the cache is empty; otherwise, Spotify Connect discovery list will be refreshed
8067
+ and the cache updated prior to the search.
8068
+
8069
+ The `SpotifyConnectDevice` object is returned if a match is found; otherwise, a null value is
8070
+ returned.
8071
+
8072
+ A user context switch will also be performed if a device id is resolved AND the active user of the
8073
+ device does not match the `SpotifyConnectUsername` argument specified on the class constructor.
8074
+ The user context switch is bypassed if the `verifyUserContext` argument is False, or if the
8075
+ `SpotifyConnectUsername` was not supplied on the class constructor. If the user context is to be
8076
+ switched, then a Disconnect will be issued if a user context is active on the device followed by
8077
+ a Connect to the user context specified on the class constructor.
8078
+
8079
+ <details>
8080
+ <summary>Sample Code</summary>
8081
+ ```python
8082
+ .. include:: ../docs/include/samplecode/SpotifyClient/GetSpotifyConnectDevice.py
8083
+ ```
8084
+ </details>
8085
+ """
8086
+ apiMethodName:str = 'GetSpotifyConnectDevice'
8087
+ apiMethodParms:SIMethodParmListContext = None
8088
+ deviceResultId:str = None
8089
+ deviceResult:SpotifyConnectDevice = None
8090
+
8091
+ try:
8092
+
8093
+ # trace.
8094
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
8095
+ apiMethodParms.AppendKeyValue("deviceValue", deviceValue)
8096
+ apiMethodParms.AppendKeyValue("verifyUserContext", verifyUserContext)
8097
+ apiMethodParms.AppendKeyValue("verifyTimeout", verifyTimeout)
8098
+ apiMethodParms.AppendKeyValue("refreshDeviceList", refreshDeviceList)
8099
+ apiMethodParms.AppendKeyValue("activateDevice", activateDevice)
8100
+ apiMethodParms.AppendKeyValue("delay", delay)
8101
+ _logsi.LogMethodParmList(SILevel.Verbose, "Resolving Spotify Connect Device", apiMethodParms)
8102
+
8103
+ # if device value not specified then we are done.
8104
+ if (deviceValue is None) or (len(deviceValue.strip()) == 0):
8105
+ _logsi.LogVerbose("deviceValue not specified; active Spotify Player will be used")
8106
+ return None
8107
+
8108
+ # validations.
8109
+ delay = validateDelay(delay, 0.25, 10)
8110
+ verifyTimeout = validateDelay(verifyTimeout, 5, 10)
8111
+ if (not isinstance(verifyUserContext, bool)):
8112
+ verifyUserContext = True
8113
+
8114
+ # determine if the device value specified is a name (e.g. "Bose-ST10-1") or
8115
+ # an id (e.g. "30fbc80e35598f3c242f2120413c943dfd9715fe", "48b677ca-ef9b-516f-b702-93bf2e8c67ba").
8116
+ isDeviceId:bool = SpotifyClient.IsDeviceId(deviceValue)
8117
+
8118
+ deviceValueCompare:str = deviceValue.lower()
8119
+ deviceActiveUser:str = None
8120
+ discoverResult:ZeroconfDiscoveryResult
8121
+ info:ZeroconfGetInfo
8122
+ scDevice:SpotifyConnectDevice
8123
+
8124
+ # get all available Spotify Connect devices on the network.
8125
+ scDevices:SpotifyConnectDevices = self.GetSpotifyConnectDevices(refresh=refreshDeviceList)
8126
+
8127
+ # process all discovered devices.
8128
+ for scDevice in scDevices:
8129
+
8130
+ discoverResult = scDevice.DiscoveryResult
8131
+ info = scDevice.DeviceInfo
8132
+
8133
+ # store the currently active user of the device, in case we need to switch users later on.
8134
+ deviceActiveUser = info.ActiveUser.lower()
8135
+
8136
+ # are aliases being used (RemoteName is null if so)?
8137
+ if info.HasAliases:
8138
+
8139
+ # if aliases are defined for the device, then we have to check each alias for a match.
8140
+ infoAlias:ZeroconfGetInfoAlias
8141
+ for infoAlias in info.Aliases:
8142
+
8143
+ # if input deviceValue is a device id then compare the id; otherwise, compare the name.
8144
+ if isDeviceId:
8145
+ if (infoAlias.Id.lower() == deviceValueCompare):
8146
+ _logsi.LogVerbose("Device value '%s' was matched to device Alias: %s" % (deviceValue, infoAlias.Title))
8147
+ deviceResultId = infoAlias.Id
8148
+ deviceResult = scDevice
8149
+ break # alias processing loop
8150
+ else:
8151
+ if (infoAlias.Name.lower() == deviceValueCompare):
8152
+ _logsi.LogVerbose("Device value '%s' was matched to device Alias: %s" % (deviceValue, infoAlias.Title))
8153
+ deviceResultId = infoAlias.Id
8154
+ deviceResult = scDevice
8155
+ break # alias processing loop
8156
+
8157
+ else:
8158
+
8159
+ # if input deviceValue is a device id then compare the id; otherwise, compare the name.
8160
+ if isDeviceId:
8161
+ if (scDevice.Id.lower() == deviceValueCompare):
8162
+ _logsi.LogVerbose("Device value '%s' was matched to device: '%s'" % (deviceValue, scDevice.Title))
8163
+ deviceResultId = scDevice.Id
8164
+ deviceResult = scDevice
8165
+ else:
8166
+ if (scDevice.Name.lower() == deviceValueCompare):
8167
+ _logsi.LogVerbose("Device value '%s' was matched to device: '%s'" % (deviceValue, scDevice.Title))
8168
+ deviceResultId = scDevice.Id
8169
+ deviceResult = scDevice
8170
+
8171
+ # did we resolve the device id?
8172
+ if deviceResult is not None:
8173
+
8174
+ # at this point, we will try to activate the device if it requires it.
8175
+
8176
+ # reset device reconnected flag.
8177
+ scDevice.WasReConnected = False
8178
+
8179
+ # was device activation requested? if not, then we are done.
8180
+ if activateDevice == False:
8181
+ _logsi.LogVerbose("Activation not requested for Spotify Connect device: '%s'" % (deviceResult.Title))
8182
+ break
8183
+
8184
+ # is this a dynamic device? if so, then there is no need to activate it as it's already active.
8185
+ if (discoverResult.IsDynamicDevice):
8186
+ _logsi.LogVerbose("Activation not required for dynamic Spotify Connect device: '%s'" % (deviceResult.Title))
8187
+ break
8188
+
8189
+ # did caller request user context switch bypass?
8190
+ # if so, then just return the device.
8191
+ if (not verifyUserContext):
8192
+ _logsi.LogVerbose("User context '%s' will be used for Spotify Connect device '%s', as user chose to bypass user context switch" % (deviceActiveUser, deviceResult.Title))
8193
+ break
8194
+
8195
+ # was a Spotify Connect user account specified on the class constructor?
8196
+ # if not, then do not switch the user context.
8197
+ if (self._SpotifyConnectUsername is None):
8198
+ _logsi.LogVerbose("User context '%s' will be used for Spotify Connect device '%s', as a SpotifyConnectUsername parameter was not supplied" % (deviceActiveUser, deviceResult.Title))
8199
+ break
8200
+
8201
+ # does our Spotify Connect user account need to take control of the device?
8202
+ # if not, then we are done.
8203
+ # this takes into account if the device is in the available device list, as well
8204
+ # as if the active user is not our user context.
8205
+ if (info.IsInDeviceList == True) \
8206
+ and ((deviceActiveUser == self._SpotifyConnectUsername.lower()) or (deviceActiveUser == self.UserProfile.Id.lower())):
8207
+ _logsi.LogVerbose("User context '%s' was verified for Spotify Connect device '%s'; switch not necessary" % (deviceActiveUser, deviceResult.Title))
8208
+ break
8209
+
8210
+ zcfResult:ZeroconfResponse
8211
+
8212
+ # create a ZeroconfConnect object to access the device.
8213
+ # note that we are using the "HostIpAddress" property value here, with "Server" as a fallback.
8214
+ # the "Server" property is an alias, which must be resolved via a DNS lookup under
8215
+ # the covers and adds a significant delay (2-3 seconds!) to the activation time.
8216
+ zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpAddress,
8217
+ discoverResult.HostIpPort,
8218
+ discoverResult.SpotifyConnectCPath,
8219
+ useSSL=False,
8220
+ tokenStorageDir=self.TokenStorageDir)
8221
+
8222
+ # if a different user context has control of the device then we need to disconnect
8223
+ # the current user context before connecting a different user.
8224
+ if (info.HasActiveUser):
8225
+ _logsi.LogVerbose("Issuing Disconnect to Spotify Connect device '%s' for user context '%s'" % (deviceResult.Title, deviceActiveUser))
8226
+ zcfResult = zconn.Disconnect(delay)
8227
+
8228
+ # connect the device to OUR Spotify Connect user context.
8229
+ # note that the result here only indicates that the connect was submitted - NOT that it was successful!
8230
+ _logsi.LogVerbose("Issuing Connect to Spotify Connect device '%s' for user context '%s'" % (deviceResult.Title, self._SpotifyConnectUsername))
8231
+ zcfResult = zconn.Connect(self._SpotifyConnectUsername, self._SpotifyConnectPassword, self._SpotifyConnectLoginId)
8232
+
8233
+ # indicate device was reconnected.
8234
+ deviceResult.WasReConnected = True
8235
+
8236
+ # trace.
8237
+ _logsi.LogVerbose("User context switch from '%s' to '%s' was requested for Spotify Connect device: '%s'" % (deviceActiveUser, self._SpotifyConnectUsername, deviceResult.Title))
8238
+
8239
+ loopTotalDelay:float = 0
8240
+ LOOP_DELAY:float = 0.25
8241
+ while True:
8242
+
8243
+ # wait just a bit between available device list queries.
8244
+ _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % LOOP_DELAY)
8245
+ time.sleep(LOOP_DELAY)
8246
+ loopTotalDelay = loopTotalDelay + LOOP_DELAY
8247
+
8248
+ # check for the device in the available device list.
8249
+ # if we find it, then we are done.
8250
+ device:Device = self.GetPlayerDevice(deviceResultId, True)
8251
+ if device is not None:
8252
+ _logsi.LogVerbose("Spotify Connect device '%s' is now in the available device list; device found within %f seconds of Connect" % (deviceResult.Title, loopTotalDelay))
8253
+ break
8254
+
8255
+ # check if the active device is restricted; if it is, then it may not show up in the
8256
+ # available device list of devices (common issue with Sonos devices). in this case,
8257
+ # it WILL show up in the player state device property as the active device.
8258
+ # note that the PlayerPlayState device is a name and not a device id.
8259
+ playerState:PlayerPlayState = self.GetPlayerPlaybackState()
8260
+ if playerState.Device is not None:
8261
+ device = playerState.Device
8262
+ if (device.Id == deviceResultId) or (device.Name == deviceResultId):
8263
+ _logsi.LogVerbose("Spotify Connect device '%s' is now the active device; device found within %f seconds of Connect" % (deviceResult.Title, loopTotalDelay))
8264
+ break
8265
+
8266
+ # only check so many times before we give up;
8267
+ # we will keep checking until we find the device, or we exceed the verifyTimeout value.
8268
+ if (loopTotalDelay > verifyTimeout):
8269
+ _logsi.LogWarning("Verification timeout waiting for Spotify Connect device '%s' to be added to the available device list; device not found within %f seconds of Connect" % (deviceResult.Title, verifyTimeout))
8270
+ break
8271
+
8272
+ # end the device processing loop.
8273
+ break
8274
+
8275
+ # did we resolve the device id?
8276
+ if deviceResult is None:
8277
+ _logsi.LogWarning("Spotify Connect device '%s' could not be resolved; please ensure that the specified device is powered on and available on the local network" % (deviceValue))
8278
+
8279
+ # return the resolved device id.
8280
+ return deviceResult
8281
+
8282
+ except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
8283
+ except SpotifyApiError: raise # pass handled exceptions on thru
8284
+ except Exception as ex:
8285
+
8286
+ # format unhandled exception.
8287
+ raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
8288
+
8289
+ finally:
8290
+
8291
+ # trace.
8292
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
8293
+
8294
+
7987
8295
  def GetSpotifyConnectDevices(
7988
8296
  self,
7989
- refresh:bool=True
8297
+ refresh:bool=True,
8298
+ sortResult:bool=True,
7990
8299
  ) -> SpotifyConnectDevices:
7991
8300
  """
7992
8301
  Get information about all available Spotify Connect player devices.
@@ -7997,6 +8306,10 @@ class SpotifyClient:
7997
8306
  refresh (bool):
7998
8307
  True (default) to return real-time information from the spotify zeroconf api and
7999
8308
  update the cache; otherwise, False to just return the cached value.
8309
+ sortResult (bool):
8310
+ True to sort the items by name; otherwise, False to leave the items in the same order they
8311
+ were returned in by the Spotify Web API.
8312
+ Default: True
8000
8313
 
8001
8314
  Returns:
8002
8315
  A `SpotifyConnectDevices` object that contain the discovery results
@@ -8038,20 +8351,24 @@ class SpotifyClient:
8038
8351
  </details>
8039
8352
  """
8040
8353
  apiMethodName:str = 'GetSpotifyConnectDevices'
8354
+ apiMethodParms:SIMethodParmListContext = None
8041
8355
  result:SpotifyConnectDevices = SpotifyConnectDevices()
8042
8356
  cacheDesc:str = CACHE_SOURCE_CURRENT
8043
8357
 
8044
8358
  try:
8045
8359
 
8046
8360
  # trace.
8047
- _logsi.EnterMethod(SILevel.Debug, apiMethodName)
8361
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
8362
+ apiMethodParms.AppendKeyValue("refresh", refresh)
8363
+ apiMethodParms.AppendKeyValue("sortResult", sortResult)
8364
+ _logsi.LogMethodParmList(SILevel.Verbose, "Get available Spotify Connect devices", apiMethodParms)
8048
8365
 
8049
8366
  # can we use the cached value?
8050
8367
  if (not refresh) and (apiMethodName in self._ConfigurationCache):
8051
8368
 
8052
8369
  result = self._ConfigurationCache[apiMethodName]
8053
8370
  cacheDesc = CACHE_SOURCE_CACHED
8054
-
8371
+
8055
8372
  else:
8056
8373
 
8057
8374
  # discover Spotify Connect devices on the network, waiting up to the specified timeout
@@ -8134,6 +8451,8 @@ class SpotifyClient:
8134
8451
 
8135
8452
  # create new spotify connect device object.
8136
8453
  scDevice:SpotifyConnectDevice = SpotifyConnectDevice()
8454
+ scDevice.Id = info.DeviceId
8455
+ scDevice.Name = info.RemoteName
8137
8456
  scDevice.DiscoveryResult = discoverResult
8138
8457
  scDevice.DeviceInfo = info
8139
8458
  result.Items.append(scDevice)
@@ -8151,9 +8470,10 @@ class SpotifyClient:
8151
8470
 
8152
8471
  # is this a dynamic device?
8153
8472
  if not result.ContainsDeviceId(device.Id):
8473
+ _logsi.LogVerbose("Spotify Connect dynamic device detected: '%s' (%s)" % (device.Name, device.Id))
8154
8474
  result.AddDynamicDevice(device, self.UserProfile.Id)
8155
8475
 
8156
- # is the device in the active device list for this user context?
8476
+ # is the device in the available device list for this user context?
8157
8477
  # if not, then set an indicator so we can re-activate it later if need be.
8158
8478
  scDevice:SpotifyConnectDevice
8159
8479
  for scDevice in result:
@@ -8161,10 +8481,30 @@ class SpotifyClient:
8161
8481
  scDevice.DeviceInfo.IsInDeviceList = True
8162
8482
  scDevice.DeviceInfo.IsActiveDevice = device.IsActive
8163
8483
  break
8484
+
8485
+ # get the currently active device (if any).
8486
+ # we do this in case the active device is a "restricted" device; if it is, then it may
8487
+ # not show up as the active device (common issue with Sonos devices).
8488
+ # in this case, it WILL show up in the player state device property as the active device.
8489
+ # if it IS active, then we will set the active flag.
8490
+ # note that the PlayerPlayState device is a name and not a device id.
8491
+ playerState:PlayerPlayState = self.GetPlayerPlaybackState()
8492
+ if playerState.Device is not None:
8493
+ device = playerState.Device
8494
+ scDevice:SpotifyConnectDevice
8495
+ for scDevice in result:
8496
+ if (device.Name == scDevice.Name):
8497
+ _logsi.LogVerbose("Spotify Connect active device detected: '%s' (%s)" % (device.Name, device.Id))
8498
+ scDevice.DeviceInfo.IsActiveDevice = True
8499
+ break
8164
8500
 
8165
8501
  # update cache.
8166
8502
  self._ConfigurationCache[apiMethodName] = result
8167
8503
 
8504
+ # sort items on Name property, ascending order.
8505
+ if (len(result.Items) > 0) and (sortResult is True):
8506
+ result.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
8507
+
8168
8508
  # update last refresh date.
8169
8509
  result.DateLastRefreshed = datetime.utcnow().timestamp()
8170
8510
 
@@ -9728,7 +10068,8 @@ class SpotifyClient:
9728
10068
  Default is 0.50; value range is 0 - 10.
9729
10069
  resolveDeviceId (bool):
9730
10070
  True to resolve the supplied `deviceId` value; otherwise, False not resolve the `deviceId`
9731
- value as it has already been resolved.
10071
+ value as it has already been resolved.
10072
+ Default is True.
9732
10073
 
9733
10074
  Raises:
9734
10075
  SpotifyWebApiError:
@@ -9786,8 +10127,17 @@ class SpotifyClient:
9786
10127
 
9787
10128
  # ensure the specified device id / name is active and available, and
9788
10129
  # the user context is set correctly.
9789
- deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
9790
-
10130
+ scDevice = self.GetSpotifyConnectDevice(
10131
+ deviceId,
10132
+ refreshDeviceList=False,
10133
+ activateDevice=True,
10134
+ verifyUserContext=True)
10135
+
10136
+ # did we find the device?
10137
+ # if so, then use the found device id in case a device name was specified.
10138
+ if scDevice is not None:
10139
+ deviceId = scDevice.Id
10140
+
9791
10141
  # build spotify web api request parameters.
9792
10142
  reqData:dict = \
9793
10143
  {
@@ -9860,7 +10210,8 @@ class SpotifyClient:
9860
10210
  Default is 0.50; value range is 0 - 10.
9861
10211
  resolveDeviceId (bool):
9862
10212
  True to resolve the supplied `deviceId` value; otherwise, False not resolve the `deviceId`
9863
- value as it has already been resolved.
10213
+ value as it has already been resolved.
10214
+ Default is True.
9864
10215
  limitTotal (int):
9865
10216
  The maximum number of items to retrieve from favorites.
9866
10217
  Default: 200
@@ -9918,7 +10269,16 @@ class SpotifyClient:
9918
10269
 
9919
10270
  # ensure the specified device id / name is active and available, and
9920
10271
  # the user context is set correctly.
9921
- deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
10272
+ scDevice = self.GetSpotifyConnectDevice(
10273
+ deviceId,
10274
+ refreshDeviceList=False,
10275
+ activateDevice=True,
10276
+ verifyUserContext=True)
10277
+
10278
+ # did we find the device?
10279
+ # if so, then use the found device id in case a device name was specified.
10280
+ if scDevice is not None:
10281
+ deviceId = scDevice.Id
9922
10282
 
9923
10283
  # set desired shuffle mode.
9924
10284
  if shuffle is not None:
@@ -9984,7 +10344,8 @@ class SpotifyClient:
9984
10344
  Default is 0.50; value range is 0 - 10.
9985
10345
  resolveDeviceId (bool):
9986
10346
  True to resolve the supplied `deviceId` value; otherwise, False not resolve the `deviceId`
9987
- value as it has already been resolved.
10347
+ value as it has already been resolved.
10348
+ Default is True.
9988
10349
 
9989
10350
  Raises:
9990
10351
  SpotifyWebApiError:
@@ -10036,7 +10397,16 @@ class SpotifyClient:
10036
10397
 
10037
10398
  # ensure the specified device id / name is active and available, and
10038
10399
  # the user context is set correctly.
10039
- deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
10400
+ scDevice = self.GetSpotifyConnectDevice(
10401
+ deviceId,
10402
+ refreshDeviceList=False,
10403
+ activateDevice=True,
10404
+ verifyUserContext=True)
10405
+
10406
+ # did we find the device?
10407
+ # if so, then use the found device id in case a device name was specified.
10408
+ if scDevice is not None:
10409
+ deviceId = scDevice.Id
10040
10410
 
10041
10411
  # build spotify web api request parameters.
10042
10412
  reqData:dict = \
@@ -10455,6 +10825,9 @@ class SpotifyClient:
10455
10825
  delay:float=0.25
10456
10826
  ) -> str:
10457
10827
  """
10828
+ This method has been deprecated.
10829
+ Use the `GetSpotifyConnectDevice` method instead.
10830
+
10458
10831
  Resolves a Spotify Connect device identifier from a specified device id, name, alias id,
10459
10832
  or alias name. This will ensure that the device id can be found on the network, as well
10460
10833
  as connect to the device if necessary with the current user context.
@@ -10508,13 +10881,6 @@ class SpotifyClient:
10508
10881
  `SpotifyConnectUsername` was not supplied on the class constructor. If the user context is to be
10509
10882
  switched, then a Disconnect will be issued if a user context is active on the device followed by
10510
10883
  a Connect to the user context specified on the class constructor.
10511
-
10512
- <details>
10513
- <summary>Sample Code</summary>
10514
- ```python
10515
- .. include:: ../docs/include/samplecode/SpotifyClient/PlayerResolveDeviceId.py
10516
- ```
10517
- </details>
10518
10884
  """
10519
10885
  apiMethodName:str = 'PlayerResolveDeviceId'
10520
10886
  apiMethodParms:SIMethodParmListContext = None
@@ -10522,6 +10888,9 @@ class SpotifyClient:
10522
10888
 
10523
10889
  try:
10524
10890
 
10891
+ # TODO method is deprecated as of 2024/08/15, v1.0.80.
10892
+ # remove in a future release.
10893
+
10525
10894
  # trace.
10526
10895
  apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
10527
10896
  apiMethodParms.AppendKeyValue("deviceValue", deviceValue)
@@ -10530,163 +10899,24 @@ class SpotifyClient:
10530
10899
  apiMethodParms.AppendKeyValue("delay", delay)
10531
10900
  _logsi.LogMethodParmList(SILevel.Verbose, "Resolving Spotify Connect Player Device Id", apiMethodParms)
10532
10901
 
10533
- # if device value not specified then we are done.
10534
- if (deviceValue is None) or (len(deviceValue.strip()) == 0):
10535
- _logsi.LogVerbose("device not specified; active Spotify Player will be used")
10902
+ # trace.
10903
+ _logsi.LogWarning("SpotifyWebApiPython `PlayerResolveDeviceId` method is deprecated, and will be removed in a future release; use the `GetSpotifyConnectDevice` instead.")
10904
+
10905
+ # call newer method to resolve the device id.
10906
+ scDevice:SpotifyConnectDevice = self.GetSpotifyConnectDevice(
10907
+ deviceValue=deviceValue,
10908
+ verifyUserContext=verifyUserContext,
10909
+ verifyTimeout=verifyTimeout,
10910
+ refreshDeviceList=True,
10911
+ activateDevice=True,
10912
+ delay=delay)
10913
+
10914
+ # if device not found then we are done.
10915
+ if scDevice is None:
10536
10916
  return None
10537
10917
 
10538
- # validations.
10539
- delay = validateDelay(delay, 0.25, 10)
10540
- verifyTimeout = validateDelay(verifyTimeout, 5, 10)
10541
- if (not isinstance(verifyUserContext, bool)):
10542
- verifyUserContext = True
10543
-
10544
- # determine if the device value specified is a name (e.g. "Bose-ST10-1") or
10545
- # an id (e.g. "30fbc80e35598f3c242f2120413c943dfd9715fe", "48b677ca-ef9b-516f-b702-93bf2e8c67ba").
10546
- isDeviceId:bool = SpotifyClient.IsDeviceId(deviceValue)
10547
-
10548
- deviceValueCompare:str = deviceValue.lower()
10549
- deviceActiveUser:str = None
10550
- discoverResult:ZeroconfDiscoveryResult
10551
- info:ZeroconfGetInfo
10552
- scDevice:SpotifyConnectDevice
10553
-
10554
- # get all Spotify Connect available devices on the network.
10555
- scDevices:SpotifyConnectDevices = self.GetSpotifyConnectDevices()
10556
-
10557
- # process all discovered devices.
10558
- for scDevice in scDevices:
10559
-
10560
- discoverResult = scDevice.DiscoveryResult
10561
- info = scDevice.DeviceInfo
10562
-
10563
- # store the currently active user of the device, in case we need to switch users later on.
10564
- deviceActiveUser = info.ActiveUser.lower()
10565
-
10566
- # are aliases being used (RemoteName is null if so)?
10567
- if info.HasAliases:
10568
-
10569
- # if aliases are defined for the device, then we have to check each alias for a match.
10570
- infoAlias:ZeroconfGetInfoAlias
10571
- for infoAlias in info.Aliases:
10572
-
10573
- # if input deviceValue is a device id then compare the id; otherwise, compare the name.
10574
- if isDeviceId:
10575
- if (infoAlias.Id.lower() == deviceValueCompare):
10576
- _logsi.LogVerbose("Device value '%s' was matched to device Alias Id '%s' (%s)" % (deviceValue, infoAlias.Name, infoAlias.Id))
10577
- deviceIdResult = infoAlias.Id
10578
- break # alias processing loop
10579
- else:
10580
- if (infoAlias.Name.lower() == deviceValueCompare):
10581
- _logsi.LogVerbose("Device value '%s' was matched to device Alias Name '%s' (%s)" % (deviceValue, infoAlias.Name, infoAlias.Id))
10582
- deviceIdResult = infoAlias.Id
10583
- break # alias processing loop
10584
-
10585
- else:
10586
-
10587
- # if input deviceValue is a device id then compare the id; otherwise, compare the name.
10588
- if isDeviceId:
10589
- if (info.DeviceId.lower() == deviceValueCompare):
10590
- _logsi.LogVerbose("Device value '%s' was matched to device Id '%s' (%s)" % (deviceValue, info.RemoteName, info.DeviceId))
10591
- deviceIdResult = info.DeviceId
10592
- else:
10593
- if (info.RemoteName.lower() == deviceValueCompare):
10594
- _logsi.LogVerbose("Device value '%s' was matched to device RemoteName '%s' (%s)" % (deviceValue, info.RemoteName, info.DeviceId))
10595
- deviceIdResult = info.DeviceId
10596
-
10597
- # did we resolve the device id?
10598
- if deviceIdResult is not None:
10599
-
10600
- # is this a dynamic device? if so, then we can't control it.
10601
- if (discoverResult.IsDynamicDevice):
10602
- _logsi.LogVerbose("Spotify Connect Device '%s' (%s) is a dynamic device; we cannot control it" % (info.RemoteName, deviceIdResult))
10603
- break
10604
-
10605
- # did caller request user context switch bypass?
10606
- # if so, then just return the device id.
10607
- if (not verifyUserContext):
10608
- _logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as user chose to bypass user context switch" % (deviceActiveUser, deviceIdResult))
10609
- break
10610
-
10611
- # was a Spotify Connect user account specified on the class constructor?
10612
- # if not, then do not switch the user context.
10613
- if (self._SpotifyConnectUsername is None):
10614
- _logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as a SpotifyConnectUsername parameter was not supplied" % (deviceActiveUser, deviceIdResult))
10615
- break
10616
-
10617
- # does our Spotify Connect user account need to take control of the device?
10618
- # if not, then we are done.
10619
- # this takes into account if the device is in the active device list, as well
10620
- # as if the active user is not our user context.
10621
- if (info.IsInDeviceList == True) \
10622
- and ((deviceActiveUser == self._SpotifyConnectUsername.lower()) or (deviceActiveUser == self.UserProfile.Id.lower())):
10623
- _logsi.LogVerbose("Spotify Connect user context '%s' was verified for Device id '%s'; switch not necessary" % (deviceActiveUser, deviceIdResult))
10624
- break
10625
-
10626
- # is this a Sonos device with no active user?
10627
- # if so, then don't bother trying to switch the user context as the Sonos
10628
- # device is a "restricted" type and cannot be controlled via Spotify WebServices API.
10629
- if (info.IsBrandSonos) and (not info.HasActiveUser):
10630
- _logsi.LogVerbose("Spotify Connect Device id '%s' is a Sonos device; user account switching is not supported" % (deviceIdResult))
10631
- break
10632
-
10633
- zcfResult:ZeroconfResponse
10634
-
10635
- # create a ZeroconfConnect object to access the device.
10636
- # note that we are using the "HostIpAddress" property value here, with "Server" as a fallback.
10637
- # the "Server" property is an alias, which must be resolved via a DNS lookup under
10638
- # the covers and adds a significant delay (2-3 seconds!) to the activation time.
10639
- zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpAddress,
10640
- discoverResult.HostIpPort,
10641
- discoverResult.SpotifyConnectCPath,
10642
- useSSL=False,
10643
- tokenStorageDir=self.TokenStorageDir)
10644
-
10645
- # if a different user context has control of the device then we need to disconnect
10646
- # the current user context before connecting a different user.
10647
- if (info.HasActiveUser):
10648
- _logsi.LogVerbose("Spotify Connect user context '%s' is being disconnected for Device id '%s'" % (deviceActiveUser, deviceIdResult))
10649
- zcfResult = zconn.Disconnect(delay)
10650
-
10651
- # connect the device to OUR Spotify Connect user context.
10652
- # note that the result here only indicates that the connect was submitted - NOT that it was successful!
10653
- _logsi.LogVerbose("Spotify Connect user context '%s' is being connected for Device id '%s'" % (self._SpotifyConnectUsername, deviceIdResult))
10654
- zcfResult = zconn.Connect(self._SpotifyConnectUsername, self._SpotifyConnectPassword, self._SpotifyConnectLoginId)
10655
-
10656
- # trace.
10657
- _logsi.LogVerbose("Spotify Connect user context switch from '%s' to '%s' was requested for Device id '%s'" % (deviceActiveUser, self._SpotifyConnectUsername, deviceIdResult))
10658
-
10659
- loopTotalDelay:float = 0
10660
- LOOP_DELAY:float = 0.25
10661
- while True:
10662
-
10663
- # wait just a bit between active device list queries.
10664
- _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % LOOP_DELAY)
10665
- time.sleep(LOOP_DELAY)
10666
- loopTotalDelay = loopTotalDelay + LOOP_DELAY
10667
-
10668
- # check for the device in the active device list.
10669
- # if we find it, then we are done.
10670
- device:Device = self.GetPlayerDevice(deviceIdResult, True)
10671
- if device is not None:
10672
- _logsi.LogVerbose("Spotify Connect Device id '%s' is now in the active device list; device found within %f seconds of Connect" % (deviceIdResult, loopTotalDelay))
10673
- break
10674
-
10675
- # only check so many times before we give up;
10676
- # we will keep checking until we find the device, or we exceed the verifyTimeout value.
10677
- if (loopTotalDelay > verifyTimeout):
10678
- _logsi.LogWarning("Verification timeout waiting for Spotify Connect Device id '%s' to be added to the active device list; device not found within %f seconds of Connect" % (deviceIdResult, verifyTimeout))
10679
- break
10680
-
10681
- # end the device processing loop.
10682
- break
10683
-
10684
- # did we resolve the device id?
10685
- if deviceIdResult is None:
10686
- _logsi.LogWarning("Spotify Connect device name '%s' could not be resolved; please ensure that the specified device is powered on and available on the local network" % (deviceValue))
10687
-
10688
- # return the resolved device id.
10689
- return deviceIdResult
10918
+ # otherwise return the device id value.
10919
+ return scDevice.Id
10690
10920
 
10691
10921
  except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
10692
10922
  except SpotifyApiError: raise # pass handled exceptions on thru
@@ -11003,7 +11233,7 @@ class SpotifyClient:
11003
11233
  deviceId:str=None,
11004
11234
  play:bool=True,
11005
11235
  delay:float=0.50,
11006
- resolveDeviceId:bool=True,
11236
+ refreshDeviceList:bool=False,
11007
11237
  ) -> None:
11008
11238
  """
11009
11239
  Transfer playback to a new Spotify Connect device and optionally begin playback.
@@ -11025,9 +11255,9 @@ class SpotifyClient:
11025
11255
  This delay will give the spotify web api time to process the change before
11026
11256
  another command is issued.
11027
11257
  Default is 0.50; value range is 0 - 10.
11028
- resolveDeviceId (bool):
11029
- True to resolve the supplied `deviceId` value; otherwise, False not resolve the `deviceId`
11030
- value as it has already been resolved.
11258
+ refreshDeviceList (bool):
11259
+ True to refresh the Spotify Connect device list; otherwise, False to use the
11260
+ Spotify Connect device list cache.
11031
11261
 
11032
11262
  Raises:
11033
11263
  SpotifyWebApiError:
@@ -11049,6 +11279,7 @@ class SpotifyClient:
11049
11279
  """
11050
11280
  apiMethodName:str = 'PlayerTransferPlayback'
11051
11281
  apiMethodParms:SIMethodParmListContext = None
11282
+ scDevice:SpotifyConnectDevice = None
11052
11283
 
11053
11284
  try:
11054
11285
 
@@ -11057,18 +11288,32 @@ class SpotifyClient:
11057
11288
  apiMethodParms.AppendKeyValue("deviceId", deviceId)
11058
11289
  apiMethodParms.AppendKeyValue("play", play)
11059
11290
  apiMethodParms.AppendKeyValue("delay", delay)
11060
- apiMethodParms.AppendKeyValue("resolveDeviceId", resolveDeviceId)
11291
+ apiMethodParms.AppendKeyValue("refreshDeviceList", refreshDeviceList)
11061
11292
  _logsi.LogMethodParmList(SILevel.Verbose, "Transfer playback to a new Spotify Connect device", apiMethodParms)
11062
11293
 
11063
11294
  # validations.
11064
11295
  delay = validateDelay(delay, 0.50, 10)
11065
11296
 
11066
- # has the device id been resolved?
11067
- if (resolveDeviceId):
11068
-
11069
- # ensure the specified device id / name is active and available, and
11070
- # the user context is set correctly.
11071
- deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
11297
+ # ensure the specified device id / name is active and available, and
11298
+ # the user context is set correctly.
11299
+ scDevice = self.GetSpotifyConnectDevice(
11300
+ deviceId,
11301
+ verifyUserContext=True,
11302
+ refreshDeviceList=refreshDeviceList,
11303
+ activateDevice=True)
11304
+
11305
+ # did we find the device?
11306
+ # if so, then use the found device id in case a device name was specified.
11307
+ if scDevice is not None:
11308
+
11309
+ deviceId = scDevice.Id
11310
+
11311
+ # is this a Sonos device?
11312
+ # if so, then control was automatically transferred by activating the device.
11313
+ # it's a restricted device, so the Spotify Web API cannot control it!
11314
+ if scDevice.DeviceInfo.IsBrandSonos:
11315
+ _logsi.LogVerbose('Sonos device detected; bypassing call to Spotify Web API Transfer Playback endpoint')
11316
+ return
11072
11317
 
11073
11318
  # build spotify web api request parameters.
11074
11319
  reqData:dict = \