spotifywebapiPython 1.0.48__py3-none-any.whl → 1.0.50__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.48.dist-info → spotifywebapiPython-1.0.50.dist-info}/METADATA +1 -1
  2. {spotifywebapiPython-1.0.48.dist-info → spotifywebapiPython-1.0.50.dist-info}/RECORD +24 -24
  3. spotifywebapipython/const.py +1 -1
  4. spotifywebapipython/docs/__init__.html +20 -2
  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 +319 -3
  14. spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +20 -61
  15. spotifywebapipython/docs/spotifywebapiPython/spotifywebapiauthenticationerror.html +2 -2
  16. spotifywebapipython/docs/spotifywebapiPython/spotifywebapierror.html +2 -2
  17. spotifywebapipython/models/userprofile.py +24 -0
  18. spotifywebapipython/spotifyclient.py +505 -8
  19. spotifywebapipython/spotifydiscovery.py +32 -80
  20. spotifywebapipython/zeroconfapi/zeroconfconnect.py +10 -0
  21. spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +21 -3
  22. {spotifywebapiPython-1.0.48.dist-info → spotifywebapiPython-1.0.50.dist-info}/LICENSE +0 -0
  23. {spotifywebapiPython-1.0.48.dist-info → spotifywebapiPython-1.0.50.dist-info}/WHEEL +0 -0
  24. {spotifywebapiPython-1.0.48.dist-info → spotifywebapiPython-1.0.50.dist-info}/top_level.txt +0 -0
@@ -9,6 +9,7 @@ from typing import Tuple, Callable
9
9
  from urllib3 import PoolManager, Timeout, HTTPResponse
10
10
  from urllib.parse import urlencode
11
11
  from lxml.etree import fromstring, Element
12
+ from zeroconf import Zeroconf
12
13
 
13
14
  # our package imports.
14
15
  from .oauthcli import AuthClient
@@ -19,8 +20,11 @@ from .saappmessages import SAAppMessages
19
20
  from .spotifyapierror import SpotifyApiError
20
21
  from .spotifyapimessage import SpotifyApiMessage
21
22
  from .spotifyauthtoken import SpotifyAuthToken
23
+ from .spotifydiscovery import SpotifyDiscovery
22
24
  from .spotifywebapiauthenticationerror import SpotifyWebApiAuthenticationError
23
25
  from .spotifywebapierror import SpotifyWebApiError
26
+ from .zeroconfapi.zeroconfconnect import ZeroconfConnect, ZeroconfGetInfo, ZeroconfResponse, SpotifyZeroconfApiError
27
+ from .zeroconfapi.zeroconfgetinfoalias import ZeroconfGetInfoAlias
24
28
  from .const import (
25
29
  SPOTIFY_API_AUTHORIZE_URL,
26
30
  SPOTIFY_API_TOKEN_URL,
@@ -71,7 +75,11 @@ class SpotifyClient:
71
75
  def __init__(self,
72
76
  manager:PoolManager=None,
73
77
  tokenStorageDir:str=None,
74
- tokenUpdater:Callable=None
78
+ tokenUpdater:Callable=None,
79
+ zeroconfClient:Zeroconf=None,
80
+ spotifyConnectUsername:str=None,
81
+ spotifyConnectPassword:str=None,
82
+ spotifyConnectDiscoveryTimeout:float=2.0
75
83
  ) -> None:
76
84
  """
77
85
  Initializes a new instance of the class.
@@ -87,14 +95,58 @@ class SpotifyClient:
87
95
  A method to call when a token needs to be refreshed by an external provider.
88
96
  The defined method is called with no parameters, and should return a token dictionary.
89
97
  Default is null.
90
- """
98
+ zeroconfClient (Zeroconf)
99
+ A Zeroconf client instance that will be used to discover Spotify Connect devices,
100
+ or null to create a new instance of Zeroconf.
101
+ Default is null.
102
+ spotifyConnectUsername (str):
103
+ Spotify Connect user name used to login to a Spotify Connect device.
104
+ This MUST match the account name (or one of them) that was used to configure Spotify Connect
105
+ on a manufacturer device.
106
+ spotifyConnectPassword (str):
107
+ Spotify Connect password used to login to a Spotify Connect device.
108
+ This value is required if a non-null value was specified for the `spotifyConnectUsername` argument.
109
+ spotifyConnectDiscoveryTimeout (float):
110
+ Maximum amount of time to wait (in seconds) for the Spotify Connect discovery to complete.
111
+ Default is 2 seconds.
112
+
113
+ The `spotifyConnectUsername` and `spotifyConnectPassword` arguments are only used when a
114
+ Spotify Connect account switch is performed on a selected player device. Note that these credentials
115
+ are NOT used to access the Spotify Web API (the Spotify Web API token is used for that).
116
+ Please see the `PlayerResolveDeviceId` method for more details.
117
+
118
+ Note that if you don't have a password setup for your Spotify account (e.g. you utilize the "Continue with Google"
119
+ or other non-password methods for login), then you will need to define a "device password" in order to use the
120
+ ZeroConf Connect service; use the [Spotify Set Device Password](https://www.spotify.com/uk/account/set-device-password/)
121
+ page to define a device password. You will then use your Spotify username and the device password as your
122
+ Spotify Connect credentials.
123
+ """
124
+ # validations.
125
+ if zeroconfClient is not None and (not isinstance(zeroconfClient, Zeroconf)):
126
+ raise SpotifyApiError(SAAppMessages.ARGUMENT_TYPE_ERROR % ("__init__", 'zeroconfClient', 'Zeroconf', type(zeroconfClient).__name__), logsi=_logsi)
127
+
128
+ # timeout value needs to be a float.
129
+ if (spotifyConnectDiscoveryTimeout is None):
130
+ spotifyConnectDiscoveryTimeout = 2.0
131
+ if (not isinstance(spotifyConnectDiscoveryTimeout, int) and (not isinstance(spotifyConnectDiscoveryTimeout, float))):
132
+ spotifyConnectDiscoveryTimeout = 2.0
133
+
134
+ # password is required if username was specified.
135
+ if spotifyConnectUsername is not None:
136
+ if (spotifyConnectPassword is None) or (not isinstance(spotifyConnectPassword,str)):
137
+ raise SpotifyApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % ("__init__", 'spotifyConnectPassword'), logsi=_logsi)
138
+
91
139
  self._AuthToken:SpotifyAuthToken = None
92
140
  self._AuthClient:AuthClient = None
93
141
  self._ConfigurationCache:dict = {}
94
142
  self._Manager:PoolManager = manager
143
+ self._SpotifyConnectUsername:str = spotifyConnectUsername
144
+ self._SpotifyConnectPassword:str = spotifyConnectPassword
145
+ self._SpotifyConnectDiscoveryTimeout:float = float(spotifyConnectDiscoveryTimeout)
95
146
  self._TokenStorageDir:str = tokenStorageDir
96
147
  self._TokenUpdater:Callable = tokenUpdater
97
148
  self._UserProfile:UserProfile = None
149
+ self._ZeroconfClient = zeroconfClient
98
150
 
99
151
  # if pool manager instance is none or not a PoolManager instance, then create one.
100
152
  # we increase the maximum number of connections to keep in the pool (maxsize=) to avoid the following warnings:
@@ -109,6 +161,13 @@ class SpotifyClient:
109
161
  maxsize=30, # maximum number of connections to keep in the pool.
110
162
  block=True # limit number of connections to the device.
111
163
  )
164
+
165
+ # create the zeroconf client if one was not specified.
166
+ if zeroconfClient is None:
167
+ _logsi.LogVerbose("Creating new Zeroconf instance for discovery")
168
+ self._ZeroconfClient = Zeroconf()
169
+ else:
170
+ _logsi.LogObject(SILevel.Verbose, "Using existing Zeroconf instance for discovery", zeroconfClient)
112
171
 
113
172
 
114
173
  def __enter__(self) -> 'SpotifyClient':
@@ -240,6 +299,14 @@ class SpotifyClient:
240
299
  return self._UserProfile
241
300
 
242
301
 
302
+ @property
303
+ def ZeroconfClient(self) -> Zeroconf:
304
+ """
305
+ Zeroconf client instance that will be used to discover Spotify Connect devices.
306
+ """
307
+ return self._ZeroconfClient
308
+
309
+
243
310
  def _CheckForNextPageWithOffset(self,
244
311
  pageObj:PageObject,
245
312
  resultItemsCount,
@@ -5875,6 +5942,166 @@ class SpotifyClient:
5875
5942
  _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
5876
5943
 
5877
5944
 
5945
+ def PlayerActivateDevices(
5946
+ self,
5947
+ verifyUserContext:bool=False,
5948
+ delay:float=0.50,
5949
+ ) -> None:
5950
+ """
5951
+ Activates all Spotify Connect player devices, and (optionally) switches the active user
5952
+ context to the current user context.
5953
+
5954
+ Args:
5955
+ verifyUserContext (bool):
5956
+ If True, the active user context of the resolved device is checked to ensure it
5957
+ matches the user context specified on the class constructor.
5958
+ If False, the user context will not be checked.
5959
+ Default is False.
5960
+ delay (float):
5961
+ Time delay (in seconds) to wait AFTER issuing the final Connect command (if necessary).
5962
+ This delay will give the spotify web api time to process the device list change before
5963
+ another command is issued.
5964
+ Default is 0.50; value range is 0 - 10.
5965
+
5966
+ Raises:
5967
+ SpotifyWebApiError:
5968
+ If the Spotify Web API request was for a non-authorization service
5969
+ and the response contains error information.
5970
+ SpotifyZeroconfApiError:
5971
+ If any Spotify Zeroconf API request failed.
5972
+ If the `spotifyConnectUsername` aregument was not specified for the class contructor.
5973
+ SpotifApiError:
5974
+ If the method fails for any other reason.
5975
+
5976
+ A Zeroconf discovery process is initiated to search for all Spotify Connect devices connected
5977
+ to the local network. A Spotify Zeroconf API `getInfo` call is then issued for each device
5978
+ that was found to retrieve device information. A Spotify Zeroconf API `addUser` call is also
5979
+ issued for each device that does not have an active user context established, OR if the currently
5980
+ active user context does not match the `SpotifyConnectUsername` argument specified on the class constructor.
5981
+
5982
+ The user context switch is bypassed if the `verifyUserContext` argument is False. If the user context
5983
+ is to be switched, then a Disconnect will be issued if a user context is active on the device followed
5984
+ by a Connect to the user context specified on the class constructor.
5985
+
5986
+ <details>
5987
+ <summary>Sample Code</summary>
5988
+ ```python
5989
+ .. include:: ../docs/include/samplecode/SpotifyClient/PlayerActivateDevices.py
5990
+ ```
5991
+ </details>
5992
+ """
5993
+ apiMethodName:str = 'PlayerActivateDevices'
5994
+ apiMethodParms:SIMethodParmListContext = None
5995
+ result:str = ""
5996
+
5997
+ MSG_RESULT:str = '- Device Name="%s", ID="%s", ActiveUser="%s", Status=%s'
5998
+
5999
+ try:
6000
+
6001
+ # trace.
6002
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
6003
+ apiMethodParms.AppendKeyValue("verifyUserContext", verifyUserContext)
6004
+ apiMethodParms.AppendKeyValue("delay", delay)
6005
+ _logsi.LogMethodParmList(SILevel.Verbose, "Activating Spotify Connect Player Devices", apiMethodParms)
6006
+
6007
+ # validation - ensure spotify connect user context was specified on the class constructor.
6008
+ if (self._SpotifyConnectUsername is None):
6009
+ raise SpotifyZeroconfApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % (apiMethodName, 'SpotifyConnectUsername'), logsi=_logsi)
6010
+
6011
+ # validations.
6012
+ delay = self._ValidateDelay(delay, 0.50, 10)
6013
+ if (not isinstance(verifyUserContext, bool)):
6014
+ verifyUserContext = False
6015
+
6016
+ deviceActiveUser:str = None
6017
+ deviceIdResult:str = None
6018
+
6019
+ # discover Spotify Connect devices on the network, waiting up to the specified timeout
6020
+ # for all devices to be discovered. this will return all Spotify Connect devices that
6021
+ # are currently powered on, and are registered to Zeroconf / mDNS.
6022
+ _logsi.LogVerbose("Discovering Spotify Connect devices on the local network")
6023
+ discovery:SpotifyDiscovery = SpotifyDiscovery(self._ZeroconfClient, printToConsole=False)
6024
+ discovery.DiscoverDevices(timeout=self._SpotifyConnectDiscoveryTimeout)
6025
+
6026
+ # process all discovered devices.
6027
+ discoverResult:ZeroconfDiscoveryResult
6028
+ for discoverResult in discovery.DiscoveryResults:
6029
+
6030
+ # get the id from the device via the zeroconf API getInfo endpoint, as the id is not
6031
+ # returned in the zeroconf discovery result.
6032
+ zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpv4Address,
6033
+ discoverResult.HostIpPort,
6034
+ discoverResult.SpotifyConnectCPath,
6035
+ useSSL=False)
6036
+ info:ZeroconfGetInfo = zconn.GetInformation()
6037
+
6038
+ # store the currently active user of the device, in case we need to switch users later on.
6039
+ deviceActiveUser = info.ActiveUser.lower()
6040
+ deviceIdResult = info.DeviceId
6041
+
6042
+ status:str = "INACTIVE"
6043
+ if (info.HasActiveUser):
6044
+ status = "ACTIVE, "
6045
+
6046
+ # did we resolve the device id?
6047
+ if deviceIdResult is not None:
6048
+
6049
+ # does our Spotify Connect user account need to take control of the device?
6050
+ # if not, then we are done.
6051
+ if (deviceActiveUser == self._SpotifyConnectUsername.lower()): # or (deviceActiveUser == self.UserProfile.Id.lower()):
6052
+ _logsi.LogVerbose("Spotify Connect user context '%s' was verified for Device id '%s'; switch not necessary" % (deviceActiveUser, deviceIdResult))
6053
+ status = status + "user context switch not needed"
6054
+
6055
+ # did caller request user context switch bypass?
6056
+ # if so, then just return the device id.
6057
+ elif (info.HasActiveUser) and (not verifyUserContext):
6058
+ _logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as caller chose to bypass user context switch" % (deviceActiveUser, deviceIdResult))
6059
+ status = status + "caller bypassed user context switch"
6060
+
6061
+ else:
6062
+ zcfResult:ZeroconfResponse
6063
+
6064
+ # if a different user context has control of the device then we need to disconnect
6065
+ # the current user context before connecting a different user.
6066
+ if (info.HasActiveUser):
6067
+ _logsi.LogVerbose("Spotify Connect user context '%s' is being disconnected for Device id '%s'" % (deviceActiveUser, deviceIdResult))
6068
+ zcfResult = zconn.Disconnect()
6069
+ status = status + 'disconnected from user context "%s";' % deviceActiveUser
6070
+
6071
+ # connect the device to OUR Spotify Connect user context.
6072
+ # note that the result here only indicates that the connect was submitted - NOT that it was successful!
6073
+ _logsi.LogVerbose("Spotify Connect user context '%s' is being connected for Device id '%s'" % (self._SpotifyConnectUsername, deviceIdResult))
6074
+ zcfResult = zconn.Connect(self._SpotifyConnectUsername, self._SpotifyConnectPassword)
6075
+ status = status + 'connected to user context "%s"' % self._SpotifyConnectUsername
6076
+
6077
+ # trace.
6078
+ _logsi.LogVerbose("Spotify Connect user context switch from '%s' to '%s' was requested for Device id '%s'" % (deviceActiveUser, self._SpotifyConnectUsername, deviceIdResult))
6079
+
6080
+ # give spotify web api time to process the change and add the device to the device list.
6081
+ if delay > 0:
6082
+ _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % delay)
6083
+ time.sleep(delay)
6084
+
6085
+ # append device status to results.
6086
+ result = "%s%s\n" % (result, MSG_RESULT % (info.RemoteName, info.DeviceId, info.ActiveUser, status))
6087
+
6088
+ # return results.
6089
+ _logsi.LogText(SILevel.Verbose, "Spotify Connect Player Device activation results", result)
6090
+ return result
6091
+
6092
+ except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
6093
+ except SpotifyApiError: raise # pass handled exceptions on thru
6094
+ except Exception as ex:
6095
+
6096
+ # format unhandled exception.
6097
+ raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
6098
+
6099
+ finally:
6100
+
6101
+ # trace.
6102
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
6103
+
6104
+
5878
6105
  def PlayerConvertDeviceNameToId(self,
5879
6106
  value:str,
5880
6107
  refresh:bool=False
@@ -5944,12 +6171,12 @@ class SpotifyClient:
5944
6171
 
5945
6172
  try:
5946
6173
 
5947
- # is this a hex string value?
6174
+ # is this a hex string value (e.g. device id)?
5948
6175
  intValue:int = int(str(value), 16)
5949
6176
 
5950
6177
  # if it IS a hex string, then it should have been converted to a number.
5951
6178
  # at this point we can assume it's a device id. in this case, just return it.
5952
- # note that we will NOT perform a lookup in the device list
6179
+ # note that we will NOT perform a lookup in the device list .
5953
6180
  _logsi.LogVerbose("Device id '%s' was specified, and will be used as-is" % value)
5954
6181
  if intValue is not None:
5955
6182
  return value
@@ -9012,6 +9239,35 @@ class SpotifyClient:
9012
9239
  _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
9013
9240
 
9014
9241
 
9242
+ @staticmethod
9243
+ def IsDeviceId(value:str) -> bool:
9244
+ """
9245
+ Get the assumed device value format.
9246
+
9247
+ Args:
9248
+ value (str):
9249
+ The id or name of a Spotify Connect device.
9250
+ Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
9251
+ Example: `Web Player (Chrome)`
9252
+
9253
+ Returns:
9254
+ True if the specified value is a device id format;
9255
+ otherwise, False to indicate it is a device name format.
9256
+
9257
+ No exceptions are raised with this method.
9258
+ """
9259
+ result:bool = False
9260
+ try:
9261
+ # if it is a hex string, then it is assumed to be a device id.
9262
+ intValue:int = int(str(value), 16)
9263
+ result = True
9264
+ except Exception:
9265
+ # otherwise, assume it's a device name.
9266
+ result = False
9267
+
9268
+ return result
9269
+
9270
+
9015
9271
  def PlayerMediaPause(self,
9016
9272
  deviceId:str=None,
9017
9273
  delay:float=0.50
@@ -9199,7 +9455,11 @@ class SpotifyClient:
9199
9455
  delay = self._ValidateDelay(delay, 0.50, 10)
9200
9456
 
9201
9457
  # check for device name; convert to an id if a name was supplied.
9202
- deviceId = self.PlayerConvertDeviceNameToId(deviceId)
9458
+ #deviceId = self.PlayerConvertDeviceNameToId(deviceId)
9459
+
9460
+ # ensure the specified device id / name is active and available, and
9461
+ # the user context is set correctly.
9462
+ deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
9203
9463
 
9204
9464
  # build spotify web api request parameters.
9205
9465
  reqData:dict = \
@@ -9315,7 +9575,11 @@ class SpotifyClient:
9315
9575
  arrUris.append(trackSaved.Track.Uri)
9316
9576
 
9317
9577
  # check for device name; convert to an id if a name was supplied.
9318
- deviceId = self.PlayerConvertDeviceNameToId(deviceId)
9578
+ #deviceId = self.PlayerConvertDeviceNameToId(deviceId)
9579
+
9580
+ # ensure the specified device id / name is active and available, and
9581
+ # the user context is set correctly.
9582
+ deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
9319
9583
 
9320
9584
  # set desired shuffle mode.
9321
9585
  self.PlayerSetShuffleMode(shuffle, deviceId, delay)
@@ -9420,7 +9684,11 @@ class SpotifyClient:
9420
9684
  arrUris[idx] = arrUris[idx].strip()
9421
9685
 
9422
9686
  # check for device name; convert to an id if a name was supplied.
9423
- deviceId = self.PlayerConvertDeviceNameToId(deviceId)
9687
+ #deviceId = self.PlayerConvertDeviceNameToId(deviceId)
9688
+
9689
+ # ensure the specified device id / name is active and available, and
9690
+ # the user context is set correctly.
9691
+ deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
9424
9692
 
9425
9693
  # build spotify web api request parameters.
9426
9694
  reqData:dict = \
@@ -9830,6 +10098,231 @@ class SpotifyClient:
9830
10098
  _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
9831
10099
 
9832
10100
 
10101
+ def PlayerResolveDeviceId(
10102
+ self,
10103
+ deviceValue:str,
10104
+ verifyUserContext:bool=True,
10105
+ verifyTimeout:float=5.0,
10106
+ ) -> str:
10107
+ """
10108
+ Resolves a Spotify Connect device identifier from a specified device id, name, alias id,
10109
+ or alias name. This will ensure that the device id can be found on the network, as well
10110
+ as connect to the device if necessary with the current user context.
10111
+
10112
+ Args:
10113
+ deviceValue (str):
10114
+ The device id / name value to check.
10115
+ verifyUserContext (bool):
10116
+ If True, the active user context of the resolved device is checked to ensure it
10117
+ matches the user context specified on the class constructor.
10118
+ If False, the user context will not be checked.
10119
+ Default is True.
10120
+ verifyTimeout (float):
10121
+ Maximum time to wait (in seconds) for the device to become active in the Spotify
10122
+ Connect device list. This value is only used if a Connect command has to be
10123
+ issued to activate the device.
10124
+ Default is 5; value range is 0 - 10.
10125
+
10126
+ Returns:
10127
+ A device Id for the deviceValue if one could be resolved;
10128
+ otherwise, just the original deviceValue with the understanding that
10129
+ subsequent operations will probably fail since it's not in the device list.
10130
+
10131
+ Raises:
10132
+ SpotifyWebApiError:
10133
+ If the Spotify Web API request was for a non-authorization service
10134
+ and the response contains error information.
10135
+ SpotifyZeroconfApiError:
10136
+ If any Spotify Zeroconf API request failed.
10137
+ SpotifApiError:
10138
+ If the method fails for any other reason.
10139
+
10140
+ A Zeroconf discovery process is initiated to search for all Spotify Connect devices connected
10141
+ to the local network. A Spotify Zeroconf API `getInfo` call is then issued for each device
10142
+ that was found, searching for a match on the `deviceValue` argument. Depending on the format
10143
+ of the `deviceValue`, the search will match on either a Device ID or RemoteName as well as any
10144
+ alias ID's or Names that are in use (for multi-room configurations).
10145
+
10146
+ The device ID value is returned if a match is found; otherwise, null is returned.
10147
+
10148
+ A user context switch will also be performed if a device id is resolved AND the active user of the
10149
+ device does not match the `SpotifyConnectUsername` argument specified on the class constructor.
10150
+ The user context switch is bypassed if the `verifyUserContext` argument is False, or if the
10151
+ `SpotifyConnectUsername` was not supplied on the class constructor. If the user context is to be
10152
+ switched, then a Disconnect will be issued if a user context is active on the device followed by
10153
+ a Connect to the user context specified on the class constructor.
10154
+
10155
+ <details>
10156
+ <summary>Sample Code</summary>
10157
+ ```python
10158
+ .. include:: ../docs/include/samplecode/SpotifyClient/PlayerResolveDeviceId.py
10159
+ ```
10160
+ </details>
10161
+ """
10162
+ apiMethodName:str = 'PlayerResolveDeviceId'
10163
+ apiMethodParms:SIMethodParmListContext = None
10164
+ deviceIdResult:str = None
10165
+
10166
+ try:
10167
+
10168
+ # trace.
10169
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
10170
+ apiMethodParms.AppendKeyValue("deviceValue", deviceValue)
10171
+ apiMethodParms.AppendKeyValue("verifyUserContext", verifyUserContext)
10172
+ apiMethodParms.AppendKeyValue("verifyTimeout", verifyTimeout)
10173
+ _logsi.LogMethodParmList(SILevel.Verbose, "Resolving Spotify Connect Player Device Id", apiMethodParms)
10174
+
10175
+ # if device value not specified then we are done.
10176
+ if (deviceValue is None) or (len(deviceValue.strip()) == 0):
10177
+ _logsi.LogVerbose("deviceValue not specified; nothing to do")
10178
+ return None
10179
+
10180
+ # validations.
10181
+ verifyTimeout = self._ValidateDelay(verifyTimeout, 5, 10)
10182
+ if (not isinstance(verifyUserContext, bool)):
10183
+ verifyUserContext = True
10184
+
10185
+ # determine if the device value specified is a name (e.g. "Bose-ST10-1") or
10186
+ # an id (e.g. "30fbc80e35598f3c242f2120413c943dfd9715fe").
10187
+ isDeviceId:bool = SpotifyClient.IsDeviceId(deviceValue)
10188
+ deviceValueCompare:str = deviceValue.lower()
10189
+ deviceActiveUser:str = None
10190
+
10191
+ # discover Spotify Connect devices on the network, waiting up to the specified timeout
10192
+ # for all devices to be discovered. this will return all Spotify Connect devices that
10193
+ # are currently powered on, and are registered to Zeroconf / mDNS.
10194
+ _logsi.LogVerbose("Discovering Spotify Connect devices on the local network")
10195
+ discovery:SpotifyDiscovery = SpotifyDiscovery(self._ZeroconfClient, printToConsole=False)
10196
+ discovery.DiscoverDevices(timeout=self._SpotifyConnectDiscoveryTimeout)
10197
+
10198
+ # process all discovered devices.
10199
+ discoverResult:ZeroconfDiscoveryResult
10200
+ for discoverResult in discovery.DiscoveryResults:
10201
+
10202
+ # get the id from the device via the zeroconf API getInfo endpoint, as the id is not
10203
+ # returned in the zeroconf discovery result.
10204
+ zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpv4Address,
10205
+ discoverResult.HostIpPort,
10206
+ discoverResult.SpotifyConnectCPath,
10207
+ useSSL=False)
10208
+ info:ZeroconfGetInfo = zconn.GetInformation()
10209
+
10210
+ # store the currently active user of the device, in case we need to switch users later on.
10211
+ deviceActiveUser = info.ActiveUser.lower()
10212
+
10213
+ # are aliases being used (RemoteName is null if so)?
10214
+ if info.RemoteName is None:
10215
+
10216
+ # if aliases are defined for the device, then we have to check each alias for a match.
10217
+ infoAlias:ZeroconfGetInfoAlias
10218
+ for infoAlias in info.Aliases:
10219
+
10220
+ # if input deviceValue is a device id then compare the id; otherwise, compare the name.
10221
+ if isDeviceId:
10222
+ if (infoAlias.Id.lower() == deviceValueCompare):
10223
+ _logsi.LogVerbose("Device value '%s' was matched to device Alias Id '%s' (%s)" % (deviceValue, infoAlias.Name, infoAlias.Id))
10224
+ deviceIdResult = infoAlias.Id
10225
+ break # alias processing loop
10226
+ else:
10227
+ if (infoAlias.Name.lower() == deviceValueCompare):
10228
+ _logsi.LogVerbose("Device value '%s' was matched to device Alias Name '%s' (%s)" % (deviceValue, infoAlias.Name, infoAlias.Id))
10229
+ deviceIdResult = infoAlias.Id
10230
+ break # alias processing loop
10231
+
10232
+ else:
10233
+
10234
+ # if input deviceValue is a device id then compare the id; otherwise, compare the name.
10235
+ if isDeviceId:
10236
+ if (info.DeviceId.lower() == deviceValueCompare):
10237
+ _logsi.LogVerbose("Device value '%s' was matched to device Id '%s' (%s)" % (deviceValue, info.RemoteName, info.DeviceId))
10238
+ deviceIdResult = info.DeviceId
10239
+ else:
10240
+ if (info.RemoteName.lower() == deviceValueCompare):
10241
+ _logsi.LogVerbose("Device value '%s' was matched to device RemoteName '%s' (%s)" % (deviceValue, info.RemoteName, info.DeviceId))
10242
+ deviceIdResult = info.DeviceId
10243
+
10244
+ # did we resolve the device id?
10245
+ if deviceIdResult is not None:
10246
+
10247
+ # did caller request user context switch bypass?
10248
+ # if so, then just return the device id.
10249
+ if (not verifyUserContext):
10250
+ _logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as user chose to bypass user context switch" % (deviceActiveUser, deviceIdResult))
10251
+ break
10252
+
10253
+ # was a Spotify Connect user account specified on the class constructor?
10254
+ # if not, then do not switch the user context.
10255
+ if (self._SpotifyConnectUsername is None):
10256
+ _logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as a SpotifyConnectUsername parameter was not supplied" % (deviceActiveUser, deviceIdResult))
10257
+ break
10258
+
10259
+ # does our Spotify Connect user account need to take control of the device?
10260
+ # if not, then we are done.
10261
+ if (deviceActiveUser == self._SpotifyConnectUsername.lower()) or (deviceActiveUser == self.UserProfile.Id.lower()):
10262
+ _logsi.LogVerbose("Spotify Connect user context '%s' was verified for Device id '%s'; switch not necessary" % (deviceActiveUser, deviceIdResult))
10263
+ break
10264
+
10265
+ zcfResult:ZeroconfResponse
10266
+
10267
+ # if a different user context has control of the device then we need to disconnect
10268
+ # the current user context before connecting a different user.
10269
+ if (info.HasActiveUser):
10270
+ _logsi.LogVerbose("Spotify Connect user context '%s' is being disconnected for Device id '%s'" % (deviceActiveUser, deviceIdResult))
10271
+ zcfResult = zconn.Disconnect()
10272
+
10273
+ # connect the device to OUR Spotify Connect user context.
10274
+ # note that the result here only indicates that the connect was submitted - NOT that it was successful!
10275
+ _logsi.LogVerbose("Spotify Connect user context '%s' is being connected for Device id '%s'" % (self._SpotifyConnectUsername, deviceIdResult))
10276
+ zcfResult = zconn.Connect(self._SpotifyConnectUsername, self._SpotifyConnectPassword)
10277
+
10278
+ # trace.
10279
+ _logsi.LogVerbose("Spotify Connect user context switch from '%s' to '%s' was requested for Device id '%s'" % (deviceActiveUser, self._SpotifyConnectUsername, deviceIdResult))
10280
+
10281
+ loopTotalDelay:float = 0
10282
+ LOOP_DELAY:float = 0.25
10283
+ while True:
10284
+
10285
+ # wait just a bit between active device list queries.
10286
+ _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % LOOP_DELAY)
10287
+ time.sleep(LOOP_DELAY)
10288
+ loopTotalDelay = loopTotalDelay + LOOP_DELAY
10289
+
10290
+ # check for the device in the active device list.
10291
+ # if we find it, then we are done.
10292
+ device:Device = self.GetPlayerDevice(deviceIdResult, True)
10293
+ if device is not None:
10294
+ _logsi.LogVerbose("Spotify Connect Device id '%s' is now in the active device list; device found within %f seconds of Connect" % (deviceIdResult, loopTotalDelay))
10295
+ break
10296
+
10297
+ # only check so many times before we give up;
10298
+ # we will keep checking until we find the device, or we exceed the verifyTimeout value.
10299
+ if (loopTotalDelay > verifyTimeout):
10300
+ _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))
10301
+ break
10302
+
10303
+ # end the device processing loop.
10304
+ break
10305
+
10306
+ # did we resolve the device id?
10307
+ if deviceIdResult is None:
10308
+ _logsi.LogWarning("Device value '%s' could not be resolved to an active Spotify Connect device, which denotes subsequent operations for the device will probably fail; please ensure that the specified device is available on the network" % (deviceValue))
10309
+
10310
+ # return the resolved device id.
10311
+ return deviceIdResult
10312
+
10313
+ except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
10314
+ except SpotifyApiError: raise # pass handled exceptions on thru
10315
+ except Exception as ex:
10316
+
10317
+ # format unhandled exception.
10318
+ raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
10319
+
10320
+ finally:
10321
+
10322
+ # trace.
10323
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
10324
+
10325
+
9833
10326
  def PlayerSetRepeatMode(self,
9834
10327
  state:str='off',
9835
10328
  deviceId:str=None,
@@ -10187,7 +10680,11 @@ class SpotifyClient:
10187
10680
  delay = self._ValidateDelay(delay, 0.50, 10)
10188
10681
 
10189
10682
  # check for device name; convert to an id if a name was supplied.
10190
- deviceId = self.PlayerConvertDeviceNameToId(deviceId)
10683
+ #deviceId = self.PlayerConvertDeviceNameToId(deviceId)
10684
+
10685
+ # ensure the specified device id / name is active and available, and
10686
+ # the user context is set correctly.
10687
+ deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
10191
10688
 
10192
10689
  # build spotify web api request parameters.
10193
10690
  reqData:dict = \