spotifywebapiPython 1.0.79__py3-none-any.whl → 1.0.81__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 (24) hide show
  1. {spotifywebapiPython-1.0.79.dist-info → spotifywebapiPython-1.0.81.dist-info}/METADATA +1 -1
  2. {spotifywebapiPython-1.0.79.dist-info → spotifywebapiPython-1.0.81.dist-info}/RECORD +24 -24
  3. spotifywebapipython/const.py +1 -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 +3 -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 +2 -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/spotifyclient.py +437 -195
  20. spotifywebapipython/zeroconfapi/zeroconfconnect.py +4 -2
  21. spotifywebapipython/zeroconfapi/zeroconfgetinfoalias.py +8 -0
  22. {spotifywebapiPython-1.0.79.dist-info → spotifywebapiPython-1.0.81.dist-info}/LICENSE +0 -0
  23. {spotifywebapiPython-1.0.79.dist-info → spotifywebapiPython-1.0.81.dist-info}/WHEEL +0 -0
  24. {spotifywebapiPython-1.0.79.dist-info → spotifywebapiPython-1.0.81.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,296 @@ 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 `SpotifyConnectDevice` 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
+ # are aliases being used (RemoteName is null if so)?
8134
+ if info.HasAliases:
8135
+
8136
+ # if aliases are defined for the device, then we have to check each alias for a match.
8137
+ infoAlias:ZeroconfGetInfoAlias
8138
+ for infoAlias in info.Aliases:
8139
+
8140
+ # if input deviceValue is a device id then compare the id; otherwise, compare the name.
8141
+ if isDeviceId:
8142
+ if (infoAlias.Id.lower() == deviceValueCompare):
8143
+ _logsi.LogVerbose("Device value '%s' was matched to device Alias: %s" % (deviceValue, infoAlias.Title))
8144
+ deviceResultId = infoAlias.Id
8145
+ deviceResult = scDevice
8146
+ break # alias processing loop
8147
+ else:
8148
+ if (infoAlias.Name.lower() == deviceValueCompare):
8149
+ _logsi.LogVerbose("Device value '%s' was matched to device Alias: %s" % (deviceValue, infoAlias.Title))
8150
+ deviceResultId = infoAlias.Id
8151
+ deviceResult = scDevice
8152
+ break # alias processing loop
8153
+
8154
+ else:
8155
+
8156
+ # if input deviceValue is a device id then compare the id; otherwise, compare the name.
8157
+ if isDeviceId:
8158
+ if (scDevice.Id.lower() == deviceValueCompare):
8159
+ _logsi.LogVerbose("Device value '%s' was matched to device: '%s'" % (deviceValue, scDevice.Title))
8160
+ deviceResultId = scDevice.Id
8161
+ deviceResult = scDevice
8162
+ else:
8163
+ if (scDevice.Name.lower() == deviceValueCompare):
8164
+ _logsi.LogVerbose("Device value '%s' was matched to device: '%s'" % (deviceValue, scDevice.Title))
8165
+ deviceResultId = scDevice.Id
8166
+ deviceResult = scDevice
8167
+
8168
+ # did we resolve the device id?
8169
+ if deviceResult is not None:
8170
+
8171
+ # at this point, we will try to activate the device if it requires it.
8172
+
8173
+ # reset device reconnected flag.
8174
+ scDevice.WasReConnected = False
8175
+
8176
+ # create a ZeroconfConnect object to access the device.
8177
+ # note that we are using the "HostIpAddress" property value here, with "Server" as a fallback.
8178
+ # the "Server" property is an alias, which must be resolved via a DNS lookup under
8179
+ # the covers and adds a significant delay (2-3 seconds!) to the activation time.
8180
+ zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpAddress,
8181
+ discoverResult.HostIpPort,
8182
+ discoverResult.SpotifyConnectCPath,
8183
+ useSSL=False,
8184
+ tokenStorageDir=self.TokenStorageDir)
8185
+
8186
+ # if we did not refresh the device list, then query the device for real-time status; just in
8187
+ # case the device was disconnected after the last cache refresh.
8188
+ if not refreshDeviceList:
8189
+ _logsi.LogVerbose("Getting real-time status information for Spotify Connect device: '%s'" % (deviceResult.Title))
8190
+ info = zconn.GetInformation()
8191
+
8192
+ # store the currently active user of the device, in case we need to switch users later on.
8193
+ deviceActiveUser = info.ActiveUser.lower()
8194
+
8195
+ # was device activation requested? if not, then we are done.
8196
+ if activateDevice == False:
8197
+ _logsi.LogVerbose("Activation not requested for Spotify Connect device: '%s'" % (deviceResult.Title))
8198
+ break
8199
+
8200
+ # is this a dynamic device? if so, then there is no need to activate it as it's already active.
8201
+ if (discoverResult.IsDynamicDevice):
8202
+ _logsi.LogVerbose("Activation not required for dynamic Spotify Connect device: '%s'" % (deviceResult.Title))
8203
+ break
8204
+
8205
+ # did caller request user context switch bypass?
8206
+ # if so, then just return the device.
8207
+ if (not verifyUserContext):
8208
+ _logsi.LogVerbose("User context '%s' will be used for Spotify Connect device '%s', as user chose to bypass user context switch" % (deviceActiveUser, deviceResult.Title))
8209
+ break
8210
+
8211
+ # was a Spotify Connect user account specified on the class constructor?
8212
+ # if not, then do not switch the user context.
8213
+ if (self._SpotifyConnectUsername is None):
8214
+ _logsi.LogVerbose("User context '%s' will be used for Spotify Connect device '%s', as a SpotifyConnectUsername parameter was not supplied" % (deviceActiveUser, deviceResult.Title))
8215
+ break
8216
+
8217
+ zcfResult:ZeroconfResponse
8218
+
8219
+ # if a different user context has control of the device then we need to disconnect
8220
+ # the current user context before connecting a different user.
8221
+ if (info.HasActiveUser):
8222
+ _logsi.LogVerbose("Issuing Disconnect to Spotify Connect device '%s' for user context '%s'" % (deviceResult.Title, deviceActiveUser))
8223
+ zcfResult = zconn.Disconnect(delay)
8224
+
8225
+ # connect the device to OUR Spotify Connect user context.
8226
+ # note that the result here only indicates that the connect was submitted - NOT that it was successful!
8227
+ _logsi.LogVerbose("Issuing Connect to Spotify Connect device '%s' for user context '%s'" % (deviceResult.Title, self._SpotifyConnectUsername))
8228
+ zcfResult = zconn.Connect(self._SpotifyConnectUsername, self._SpotifyConnectPassword, self._SpotifyConnectLoginId)
8229
+
8230
+ # indicate device was reconnected.
8231
+ deviceResult.WasReConnected = True
8232
+
8233
+ # trace.
8234
+ _logsi.LogVerbose("User context switch from '%s' to '%s' was requested for Spotify Connect device: '%s'" % (deviceActiveUser, self._SpotifyConnectUsername, deviceResult.Title))
8235
+
8236
+ loopTotalDelay:float = 0
8237
+ LOOP_DELAY:float = 0.25
8238
+ while True:
8239
+
8240
+ # wait just a bit between available device list queries.
8241
+ _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % LOOP_DELAY)
8242
+ time.sleep(LOOP_DELAY)
8243
+ loopTotalDelay = loopTotalDelay + LOOP_DELAY
8244
+
8245
+ # check for the device in the available device list.
8246
+ # if we find it, then we are done.
8247
+ device:Device = self.GetPlayerDevice(deviceResultId, True)
8248
+ if device is not None:
8249
+ _logsi.LogVerbose("Spotify Connect device '%s' is now in the available device list; device found within %f seconds of Connect" % (deviceResult.Title, loopTotalDelay))
8250
+ break
8251
+
8252
+ # check if the active device is restricted; if it is, then it may not show up in the
8253
+ # available device list of devices (common issue with Sonos devices). in this case,
8254
+ # it WILL show up in the player state device property as the active device.
8255
+ # note that the PlayerPlayState device is a name and not a device id.
8256
+ playerState:PlayerPlayState = self.GetPlayerPlaybackState()
8257
+ if playerState.Device is not None:
8258
+ device = playerState.Device
8259
+ if (device.Id == deviceResultId) or (device.Name == deviceResultId):
8260
+ _logsi.LogVerbose("Spotify Connect device '%s' is now the active device; device found within %f seconds of Connect" % (deviceResult.Title, loopTotalDelay))
8261
+ break
8262
+
8263
+ # only check so many times before we give up;
8264
+ # we will keep checking until we find the device, or we exceed the verifyTimeout value.
8265
+ if (loopTotalDelay > verifyTimeout):
8266
+ _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))
8267
+ break
8268
+
8269
+ # end the device processing loop.
8270
+ break
8271
+
8272
+ # did we resolve the device id?
8273
+ if deviceResult is None:
8274
+ _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))
8275
+
8276
+ # return the resolved device id.
8277
+ return deviceResult
8278
+
8279
+ except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
8280
+ except SpotifyApiError: raise # pass handled exceptions on thru
8281
+ except Exception as ex:
8282
+
8283
+ # format unhandled exception.
8284
+ raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
8285
+
8286
+ finally:
8287
+
8288
+ # trace.
8289
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
8290
+
8291
+
7987
8292
  def GetSpotifyConnectDevices(
7988
8293
  self,
7989
- refresh:bool=True
8294
+ refresh:bool=True,
8295
+ sortResult:bool=True,
7990
8296
  ) -> SpotifyConnectDevices:
7991
8297
  """
7992
8298
  Get information about all available Spotify Connect player devices.
@@ -7997,6 +8303,10 @@ class SpotifyClient:
7997
8303
  refresh (bool):
7998
8304
  True (default) to return real-time information from the spotify zeroconf api and
7999
8305
  update the cache; otherwise, False to just return the cached value.
8306
+ sortResult (bool):
8307
+ True to sort the items by name; otherwise, False to leave the items in the same order they
8308
+ were returned in by the Spotify Web API.
8309
+ Default: True
8000
8310
 
8001
8311
  Returns:
8002
8312
  A `SpotifyConnectDevices` object that contain the discovery results
@@ -8038,20 +8348,24 @@ class SpotifyClient:
8038
8348
  </details>
8039
8349
  """
8040
8350
  apiMethodName:str = 'GetSpotifyConnectDevices'
8351
+ apiMethodParms:SIMethodParmListContext = None
8041
8352
  result:SpotifyConnectDevices = SpotifyConnectDevices()
8042
8353
  cacheDesc:str = CACHE_SOURCE_CURRENT
8043
8354
 
8044
8355
  try:
8045
8356
 
8046
8357
  # trace.
8047
- _logsi.EnterMethod(SILevel.Debug, apiMethodName)
8358
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
8359
+ apiMethodParms.AppendKeyValue("refresh", refresh)
8360
+ apiMethodParms.AppendKeyValue("sortResult", sortResult)
8361
+ _logsi.LogMethodParmList(SILevel.Verbose, "Get available Spotify Connect devices", apiMethodParms)
8048
8362
 
8049
8363
  # can we use the cached value?
8050
8364
  if (not refresh) and (apiMethodName in self._ConfigurationCache):
8051
8365
 
8052
8366
  result = self._ConfigurationCache[apiMethodName]
8053
8367
  cacheDesc = CACHE_SOURCE_CACHED
8054
-
8368
+
8055
8369
  else:
8056
8370
 
8057
8371
  # discover Spotify Connect devices on the network, waiting up to the specified timeout
@@ -8134,6 +8448,8 @@ class SpotifyClient:
8134
8448
 
8135
8449
  # create new spotify connect device object.
8136
8450
  scDevice:SpotifyConnectDevice = SpotifyConnectDevice()
8451
+ scDevice.Id = info.DeviceId
8452
+ scDevice.Name = info.RemoteName
8137
8453
  scDevice.DiscoveryResult = discoverResult
8138
8454
  scDevice.DeviceInfo = info
8139
8455
  result.Items.append(scDevice)
@@ -8151,9 +8467,10 @@ class SpotifyClient:
8151
8467
 
8152
8468
  # is this a dynamic device?
8153
8469
  if not result.ContainsDeviceId(device.Id):
8470
+ _logsi.LogVerbose("Spotify Connect dynamic device detected: '%s' (%s)" % (device.Name, device.Id))
8154
8471
  result.AddDynamicDevice(device, self.UserProfile.Id)
8155
8472
 
8156
- # is the device in the active device list for this user context?
8473
+ # is the device in the available device list for this user context?
8157
8474
  # if not, then set an indicator so we can re-activate it later if need be.
8158
8475
  scDevice:SpotifyConnectDevice
8159
8476
  for scDevice in result:
@@ -8161,10 +8478,30 @@ class SpotifyClient:
8161
8478
  scDevice.DeviceInfo.IsInDeviceList = True
8162
8479
  scDevice.DeviceInfo.IsActiveDevice = device.IsActive
8163
8480
  break
8481
+
8482
+ # get the currently active device (if any).
8483
+ # we do this in case the active device is a "restricted" device; if it is, then it may
8484
+ # not show up as the active device (common issue with Sonos devices).
8485
+ # in this case, it WILL show up in the player state device property as the active device.
8486
+ # if it IS active, then we will set the active flag.
8487
+ # note that the PlayerPlayState device is a name and not a device id.
8488
+ playerState:PlayerPlayState = self.GetPlayerPlaybackState()
8489
+ if playerState.Device is not None:
8490
+ device = playerState.Device
8491
+ scDevice:SpotifyConnectDevice
8492
+ for scDevice in result:
8493
+ if (device.Name == scDevice.Name):
8494
+ _logsi.LogVerbose("Spotify Connect active device detected: '%s' (%s)" % (device.Name, device.Id))
8495
+ scDevice.DeviceInfo.IsActiveDevice = True
8496
+ break
8164
8497
 
8165
8498
  # update cache.
8166
8499
  self._ConfigurationCache[apiMethodName] = result
8167
8500
 
8501
+ # sort items on Name property, ascending order.
8502
+ if (len(result.Items) > 0) and (sortResult is True):
8503
+ result.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
8504
+
8168
8505
  # update last refresh date.
8169
8506
  result.DateLastRefreshed = datetime.utcnow().timestamp()
8170
8507
 
@@ -9728,7 +10065,8 @@ class SpotifyClient:
9728
10065
  Default is 0.50; value range is 0 - 10.
9729
10066
  resolveDeviceId (bool):
9730
10067
  True to resolve the supplied `deviceId` value; otherwise, False not resolve the `deviceId`
9731
- value as it has already been resolved.
10068
+ value as it has already been resolved.
10069
+ Default is True.
9732
10070
 
9733
10071
  Raises:
9734
10072
  SpotifyWebApiError:
@@ -9786,8 +10124,17 @@ class SpotifyClient:
9786
10124
 
9787
10125
  # ensure the specified device id / name is active and available, and
9788
10126
  # the user context is set correctly.
9789
- deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
9790
-
10127
+ scDevice = self.GetSpotifyConnectDevice(
10128
+ deviceId,
10129
+ refreshDeviceList=False,
10130
+ activateDevice=True,
10131
+ verifyUserContext=True)
10132
+
10133
+ # did we find the device?
10134
+ # if so, then use the found device id in case a device name was specified.
10135
+ if scDevice is not None:
10136
+ deviceId = scDevice.Id
10137
+
9791
10138
  # build spotify web api request parameters.
9792
10139
  reqData:dict = \
9793
10140
  {
@@ -9860,7 +10207,8 @@ class SpotifyClient:
9860
10207
  Default is 0.50; value range is 0 - 10.
9861
10208
  resolveDeviceId (bool):
9862
10209
  True to resolve the supplied `deviceId` value; otherwise, False not resolve the `deviceId`
9863
- value as it has already been resolved.
10210
+ value as it has already been resolved.
10211
+ Default is True.
9864
10212
  limitTotal (int):
9865
10213
  The maximum number of items to retrieve from favorites.
9866
10214
  Default: 200
@@ -9918,7 +10266,16 @@ class SpotifyClient:
9918
10266
 
9919
10267
  # ensure the specified device id / name is active and available, and
9920
10268
  # the user context is set correctly.
9921
- deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
10269
+ scDevice = self.GetSpotifyConnectDevice(
10270
+ deviceId,
10271
+ refreshDeviceList=False,
10272
+ activateDevice=True,
10273
+ verifyUserContext=True)
10274
+
10275
+ # did we find the device?
10276
+ # if so, then use the found device id in case a device name was specified.
10277
+ if scDevice is not None:
10278
+ deviceId = scDevice.Id
9922
10279
 
9923
10280
  # set desired shuffle mode.
9924
10281
  if shuffle is not None:
@@ -9984,7 +10341,8 @@ class SpotifyClient:
9984
10341
  Default is 0.50; value range is 0 - 10.
9985
10342
  resolveDeviceId (bool):
9986
10343
  True to resolve the supplied `deviceId` value; otherwise, False not resolve the `deviceId`
9987
- value as it has already been resolved.
10344
+ value as it has already been resolved.
10345
+ Default is True.
9988
10346
 
9989
10347
  Raises:
9990
10348
  SpotifyWebApiError:
@@ -10036,7 +10394,16 @@ class SpotifyClient:
10036
10394
 
10037
10395
  # ensure the specified device id / name is active and available, and
10038
10396
  # the user context is set correctly.
10039
- deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
10397
+ scDevice = self.GetSpotifyConnectDevice(
10398
+ deviceId,
10399
+ refreshDeviceList=False,
10400
+ activateDevice=True,
10401
+ verifyUserContext=True)
10402
+
10403
+ # did we find the device?
10404
+ # if so, then use the found device id in case a device name was specified.
10405
+ if scDevice is not None:
10406
+ deviceId = scDevice.Id
10040
10407
 
10041
10408
  # build spotify web api request parameters.
10042
10409
  reqData:dict = \
@@ -10455,6 +10822,9 @@ class SpotifyClient:
10455
10822
  delay:float=0.25
10456
10823
  ) -> str:
10457
10824
  """
10825
+ This method has been deprecated.
10826
+ Use the `GetSpotifyConnectDevice` method instead.
10827
+
10458
10828
  Resolves a Spotify Connect device identifier from a specified device id, name, alias id,
10459
10829
  or alias name. This will ensure that the device id can be found on the network, as well
10460
10830
  as connect to the device if necessary with the current user context.
@@ -10508,13 +10878,6 @@ class SpotifyClient:
10508
10878
  `SpotifyConnectUsername` was not supplied on the class constructor. If the user context is to be
10509
10879
  switched, then a Disconnect will be issued if a user context is active on the device followed by
10510
10880
  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
10881
  """
10519
10882
  apiMethodName:str = 'PlayerResolveDeviceId'
10520
10883
  apiMethodParms:SIMethodParmListContext = None
@@ -10522,6 +10885,9 @@ class SpotifyClient:
10522
10885
 
10523
10886
  try:
10524
10887
 
10888
+ # TODO method is deprecated as of 2024/08/15, v1.0.80.
10889
+ # remove in a future release.
10890
+
10525
10891
  # trace.
10526
10892
  apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
10527
10893
  apiMethodParms.AppendKeyValue("deviceValue", deviceValue)
@@ -10530,163 +10896,24 @@ class SpotifyClient:
10530
10896
  apiMethodParms.AppendKeyValue("delay", delay)
10531
10897
  _logsi.LogMethodParmList(SILevel.Verbose, "Resolving Spotify Connect Player Device Id", apiMethodParms)
10532
10898
 
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")
10899
+ # trace.
10900
+ _logsi.LogWarning("SpotifyWebApiPython `PlayerResolveDeviceId` method is deprecated, and will be removed in a future release; use the `GetSpotifyConnectDevice` instead.")
10901
+
10902
+ # call newer method to resolve the device id.
10903
+ scDevice:SpotifyConnectDevice = self.GetSpotifyConnectDevice(
10904
+ deviceValue=deviceValue,
10905
+ verifyUserContext=verifyUserContext,
10906
+ verifyTimeout=verifyTimeout,
10907
+ refreshDeviceList=True,
10908
+ activateDevice=True,
10909
+ delay=delay)
10910
+
10911
+ # if device not found then we are done.
10912
+ if scDevice is None:
10536
10913
  return None
10537
10914
 
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
10915
+ # otherwise return the device id value.
10916
+ return scDevice.Id
10690
10917
 
10691
10918
  except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
10692
10919
  except SpotifyApiError: raise # pass handled exceptions on thru
@@ -11003,7 +11230,7 @@ class SpotifyClient:
11003
11230
  deviceId:str=None,
11004
11231
  play:bool=True,
11005
11232
  delay:float=0.50,
11006
- resolveDeviceId:bool=True,
11233
+ refreshDeviceList:bool=False,
11007
11234
  ) -> None:
11008
11235
  """
11009
11236
  Transfer playback to a new Spotify Connect device and optionally begin playback.
@@ -11025,9 +11252,9 @@ class SpotifyClient:
11025
11252
  This delay will give the spotify web api time to process the change before
11026
11253
  another command is issued.
11027
11254
  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.
11255
+ refreshDeviceList (bool):
11256
+ True to refresh the Spotify Connect device list; otherwise, False to use the
11257
+ Spotify Connect device list cache.
11031
11258
 
11032
11259
  Raises:
11033
11260
  SpotifyWebApiError:
@@ -11049,6 +11276,7 @@ class SpotifyClient:
11049
11276
  """
11050
11277
  apiMethodName:str = 'PlayerTransferPlayback'
11051
11278
  apiMethodParms:SIMethodParmListContext = None
11279
+ scDevice:SpotifyConnectDevice = None
11052
11280
 
11053
11281
  try:
11054
11282
 
@@ -11057,18 +11285,32 @@ class SpotifyClient:
11057
11285
  apiMethodParms.AppendKeyValue("deviceId", deviceId)
11058
11286
  apiMethodParms.AppendKeyValue("play", play)
11059
11287
  apiMethodParms.AppendKeyValue("delay", delay)
11060
- apiMethodParms.AppendKeyValue("resolveDeviceId", resolveDeviceId)
11288
+ apiMethodParms.AppendKeyValue("refreshDeviceList", refreshDeviceList)
11061
11289
  _logsi.LogMethodParmList(SILevel.Verbose, "Transfer playback to a new Spotify Connect device", apiMethodParms)
11062
11290
 
11063
11291
  # validations.
11064
11292
  delay = validateDelay(delay, 0.50, 10)
11065
11293
 
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)
11294
+ # ensure the specified device id / name is active and available, and
11295
+ # the user context is set correctly.
11296
+ scDevice = self.GetSpotifyConnectDevice(
11297
+ deviceId,
11298
+ verifyUserContext=True,
11299
+ refreshDeviceList=refreshDeviceList,
11300
+ activateDevice=True)
11301
+
11302
+ # did we find the device?
11303
+ # if so, then use the found device id in case a device name was specified.
11304
+ if scDevice is not None:
11305
+
11306
+ deviceId = scDevice.Id
11307
+
11308
+ # is this a Sonos device?
11309
+ # if so, then control was automatically transferred by activating the device.
11310
+ # it's a restricted device, so the Spotify Web API cannot control it!
11311
+ if scDevice.DeviceInfo.IsBrandSonos:
11312
+ _logsi.LogVerbose('Sonos device detected; bypassing call to Spotify Web API Transfer Playback endpoint')
11313
+ return
11072
11314
 
11073
11315
  # build spotify web api request parameters.
11074
11316
  reqData:dict = \