spotifywebapiPython 1.0.48__py3-none-any.whl → 1.0.49__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.49.dist-info}/METADATA +1 -1
  2. {spotifywebapiPython-1.0.48.dist-info → spotifywebapiPython-1.0.49.dist-info}/RECORD +24 -24
  3. spotifywebapipython/const.py +1 -1
  4. spotifywebapipython/docs/__init__.html +13 -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 +303 -3
  14. spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +15 -72
  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 +497 -8
  19. spotifywebapipython/spotifydiscovery.py +27 -83
  20. spotifywebapipython/zeroconfapi/zeroconfconnect.py +10 -0
  21. spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +21 -3
  22. {spotifywebapiPython-1.0.48.dist-info → spotifywebapiPython-1.0.49.dist-info}/LICENSE +0 -0
  23. {spotifywebapiPython-1.0.48.dist-info → spotifywebapiPython-1.0.49.dist-info}/WHEEL +0 -0
  24. {spotifywebapiPython-1.0.48.dist-info → spotifywebapiPython-1.0.49.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._Zeroconf = 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._Zeroconf = 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':
@@ -5875,6 +5934,166 @@ class SpotifyClient:
5875
5934
  _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
5876
5935
 
5877
5936
 
5937
+ def PlayerActivateDevices(
5938
+ self,
5939
+ verifyUserContext:bool=False,
5940
+ delay:float=0.50,
5941
+ ) -> None:
5942
+ """
5943
+ Activates all Spotify Connect player devices, and (optionally) switches the active user
5944
+ context to the current user context.
5945
+
5946
+ Args:
5947
+ verifyUserContext (bool):
5948
+ If True, the active user context of the resolved device is checked to ensure it
5949
+ matches the user context specified on the class constructor.
5950
+ If False, the user context will not be checked.
5951
+ Default is False.
5952
+ delay (float):
5953
+ Time delay (in seconds) to wait AFTER issuing the final Connect command (if necessary).
5954
+ This delay will give the spotify web api time to process the device list change before
5955
+ another command is issued.
5956
+ Default is 0.50; value range is 0 - 10.
5957
+
5958
+ Raises:
5959
+ SpotifyWebApiError:
5960
+ If the Spotify Web API request was for a non-authorization service
5961
+ and the response contains error information.
5962
+ SpotifyZeroconfApiError:
5963
+ If any Spotify Zeroconf API request failed.
5964
+ If the `spotifyConnectUsername` aregument was not specified for the class contructor.
5965
+ SpotifApiError:
5966
+ If the method fails for any other reason.
5967
+
5968
+ A Zeroconf discovery process is initiated to search for all Spotify Connect devices connected
5969
+ to the local network. A Spotify Zeroconf API `getInfo` call is then issued for each device
5970
+ that was found to retrieve device information. A Spotify Zeroconf API `addUser` call is also
5971
+ issued for each device that does not have an active user context established, OR if the currently
5972
+ active user context does not match the `SpotifyConnectUsername` argument specified on the class constructor.
5973
+
5974
+ The user context switch is bypassed if the `verifyUserContext` argument is False. If the user context
5975
+ is to be switched, then a Disconnect will be issued if a user context is active on the device followed
5976
+ by a Connect to the user context specified on the class constructor.
5977
+
5978
+ <details>
5979
+ <summary>Sample Code</summary>
5980
+ ```python
5981
+ .. include:: ../docs/include/samplecode/SpotifyClient/PlayerActivateDevices.py
5982
+ ```
5983
+ </details>
5984
+ """
5985
+ apiMethodName:str = 'PlayerActivateDevices'
5986
+ apiMethodParms:SIMethodParmListContext = None
5987
+ result:str = ""
5988
+
5989
+ MSG_RESULT:str = '- Device Name="%s", ID="%s", ActiveUser="%s", Status=%s'
5990
+
5991
+ try:
5992
+
5993
+ # trace.
5994
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
5995
+ apiMethodParms.AppendKeyValue("verifyUserContext", verifyUserContext)
5996
+ apiMethodParms.AppendKeyValue("delay", delay)
5997
+ _logsi.LogMethodParmList(SILevel.Verbose, "Activating Spotify Connect Player Devices", apiMethodParms)
5998
+
5999
+ # validation - ensure spotify connect user context was specified on the class constructor.
6000
+ if (self._SpotifyConnectUsername is None):
6001
+ raise SpotifyZeroconfApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % (apiMethodName, 'SpotifyConnectUsername'), logsi=_logsi)
6002
+
6003
+ # validations.
6004
+ delay = self._ValidateDelay(delay, 0.50, 10)
6005
+ if (not isinstance(verifyUserContext, bool)):
6006
+ verifyUserContext = False
6007
+
6008
+ deviceActiveUser:str = None
6009
+ deviceIdResult:str = None
6010
+
6011
+ # discover Spotify Connect devices on the network, waiting up to the specified timeout
6012
+ # for all devices to be discovered. this will return all Spotify Connect devices that
6013
+ # are currently powered on, and are registered to Zeroconf / mDNS.
6014
+ _logsi.LogVerbose("Discovering Spotify Connect devices on the local network")
6015
+ discovery:SpotifyDiscovery = SpotifyDiscovery(self._Zeroconf, printToConsole=False)
6016
+ discovery.DiscoverDevices(timeout=self._SpotifyConnectDiscoveryTimeout)
6017
+
6018
+ # process all discovered devices.
6019
+ discoverResult:ZeroconfDiscoveryResult
6020
+ for discoverResult in discovery.DiscoveryResults:
6021
+
6022
+ # get the id from the device via the zeroconf API getInfo endpoint, as the id is not
6023
+ # returned in the zeroconf discovery result.
6024
+ zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpv4Address,
6025
+ discoverResult.HostIpPort,
6026
+ discoverResult.SpotifyConnectCPath,
6027
+ useSSL=False)
6028
+ info:ZeroconfGetInfo = zconn.GetInformation()
6029
+
6030
+ # store the currently active user of the device, in case we need to switch users later on.
6031
+ deviceActiveUser = info.ActiveUser.lower()
6032
+ deviceIdResult = info.DeviceId
6033
+
6034
+ status:str = "INACTIVE"
6035
+ if (info.HasActiveUser):
6036
+ status = "ACTIVE, "
6037
+
6038
+ # did we resolve the device id?
6039
+ if deviceIdResult is not None:
6040
+
6041
+ # does our Spotify Connect user account need to take control of the device?
6042
+ # if not, then we are done.
6043
+ if (deviceActiveUser == self._SpotifyConnectUsername.lower()): # or (deviceActiveUser == self.UserProfile.Id.lower()):
6044
+ _logsi.LogVerbose("Spotify Connect user context '%s' was verified for Device id '%s'; switch not necessary" % (deviceActiveUser, deviceIdResult))
6045
+ status = status + "user context switch not needed"
6046
+
6047
+ # did caller request user context switch bypass?
6048
+ # if so, then just return the device id.
6049
+ elif (info.HasActiveUser) and (not verifyUserContext):
6050
+ _logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as caller chose to bypass user context switch" % (deviceActiveUser, deviceIdResult))
6051
+ status = status + "caller bypassed user context switch"
6052
+
6053
+ else:
6054
+ zcfResult:ZeroconfResponse
6055
+
6056
+ # if a different user context has control of the device then we need to disconnect
6057
+ # the current user context before connecting a different user.
6058
+ if (info.HasActiveUser):
6059
+ _logsi.LogVerbose("Spotify Connect user context '%s' is being disconnected for Device id '%s'" % (deviceActiveUser, deviceIdResult))
6060
+ zcfResult = zconn.Disconnect()
6061
+ status = status + 'disconnected from user context "%s";' % deviceActiveUser
6062
+
6063
+ # connect the device to OUR Spotify Connect user context.
6064
+ # note that the result here only indicates that the connect was submitted - NOT that it was successful!
6065
+ _logsi.LogVerbose("Spotify Connect user context '%s' is being connected for Device id '%s'" % (self._SpotifyConnectUsername, deviceIdResult))
6066
+ zcfResult = zconn.Connect(self._SpotifyConnectUsername, self._SpotifyConnectPassword)
6067
+ status = status + 'connected to user context "%s"' % self._SpotifyConnectUsername
6068
+
6069
+ # trace.
6070
+ _logsi.LogVerbose("Spotify Connect user context switch from '%s' to '%s' was requested for Device id '%s'" % (deviceActiveUser, self._SpotifyConnectUsername, deviceIdResult))
6071
+
6072
+ # give spotify web api time to process the change and add the device to the device list.
6073
+ if delay > 0:
6074
+ _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % delay)
6075
+ time.sleep(delay)
6076
+
6077
+ # append device status to results.
6078
+ result = "%s%s\n" % (result, MSG_RESULT % (info.RemoteName, info.DeviceId, info.ActiveUser, status))
6079
+
6080
+ # return results.
6081
+ _logsi.LogText(SILevel.Verbose, "Spotify Connect Player Device activation results", result)
6082
+ return result
6083
+
6084
+ except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
6085
+ except SpotifyApiError: raise # pass handled exceptions on thru
6086
+ except Exception as ex:
6087
+
6088
+ # format unhandled exception.
6089
+ raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
6090
+
6091
+ finally:
6092
+
6093
+ # trace.
6094
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
6095
+
6096
+
5878
6097
  def PlayerConvertDeviceNameToId(self,
5879
6098
  value:str,
5880
6099
  refresh:bool=False
@@ -5944,12 +6163,12 @@ class SpotifyClient:
5944
6163
 
5945
6164
  try:
5946
6165
 
5947
- # is this a hex string value?
6166
+ # is this a hex string value (e.g. device id)?
5948
6167
  intValue:int = int(str(value), 16)
5949
6168
 
5950
6169
  # if it IS a hex string, then it should have been converted to a number.
5951
6170
  # 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
6171
+ # note that we will NOT perform a lookup in the device list .
5953
6172
  _logsi.LogVerbose("Device id '%s' was specified, and will be used as-is" % value)
5954
6173
  if intValue is not None:
5955
6174
  return value
@@ -9012,6 +9231,35 @@ class SpotifyClient:
9012
9231
  _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
9013
9232
 
9014
9233
 
9234
+ @staticmethod
9235
+ def IsDeviceId(value:str) -> bool:
9236
+ """
9237
+ Get the assumed device value format.
9238
+
9239
+ Args:
9240
+ value (str):
9241
+ The id or name of a Spotify Connect device.
9242
+ Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
9243
+ Example: `Web Player (Chrome)`
9244
+
9245
+ Returns:
9246
+ True if the specified value is a device id format;
9247
+ otherwise, False to indicate it is a device name format.
9248
+
9249
+ No exceptions are raised with this method.
9250
+ """
9251
+ result:bool = False
9252
+ try:
9253
+ # if it is a hex string, then it is assumed to be a device id.
9254
+ intValue:int = int(str(value), 16)
9255
+ result = True
9256
+ except Exception:
9257
+ # otherwise, assume it's a device name.
9258
+ result = False
9259
+
9260
+ return result
9261
+
9262
+
9015
9263
  def PlayerMediaPause(self,
9016
9264
  deviceId:str=None,
9017
9265
  delay:float=0.50
@@ -9199,7 +9447,11 @@ class SpotifyClient:
9199
9447
  delay = self._ValidateDelay(delay, 0.50, 10)
9200
9448
 
9201
9449
  # check for device name; convert to an id if a name was supplied.
9202
- deviceId = self.PlayerConvertDeviceNameToId(deviceId)
9450
+ #deviceId = self.PlayerConvertDeviceNameToId(deviceId)
9451
+
9452
+ # ensure the specified device id / name is active and available, and
9453
+ # the user context is set correctly.
9454
+ deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
9203
9455
 
9204
9456
  # build spotify web api request parameters.
9205
9457
  reqData:dict = \
@@ -9315,7 +9567,11 @@ class SpotifyClient:
9315
9567
  arrUris.append(trackSaved.Track.Uri)
9316
9568
 
9317
9569
  # check for device name; convert to an id if a name was supplied.
9318
- deviceId = self.PlayerConvertDeviceNameToId(deviceId)
9570
+ #deviceId = self.PlayerConvertDeviceNameToId(deviceId)
9571
+
9572
+ # ensure the specified device id / name is active and available, and
9573
+ # the user context is set correctly.
9574
+ deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
9319
9575
 
9320
9576
  # set desired shuffle mode.
9321
9577
  self.PlayerSetShuffleMode(shuffle, deviceId, delay)
@@ -9420,7 +9676,11 @@ class SpotifyClient:
9420
9676
  arrUris[idx] = arrUris[idx].strip()
9421
9677
 
9422
9678
  # check for device name; convert to an id if a name was supplied.
9423
- deviceId = self.PlayerConvertDeviceNameToId(deviceId)
9679
+ #deviceId = self.PlayerConvertDeviceNameToId(deviceId)
9680
+
9681
+ # ensure the specified device id / name is active and available, and
9682
+ # the user context is set correctly.
9683
+ deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
9424
9684
 
9425
9685
  # build spotify web api request parameters.
9426
9686
  reqData:dict = \
@@ -9830,6 +10090,231 @@ class SpotifyClient:
9830
10090
  _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
9831
10091
 
9832
10092
 
10093
+ def PlayerResolveDeviceId(
10094
+ self,
10095
+ deviceValue:str,
10096
+ verifyUserContext:bool=True,
10097
+ verifyTimeout:float=5.0,
10098
+ ) -> str:
10099
+ """
10100
+ Resolves a Spotify Connect device identifier from a specified device id, name, alias id,
10101
+ or alias name. This will ensure that the device id can be found on the network, as well
10102
+ as connect to the device if necessary with the current user context.
10103
+
10104
+ Args:
10105
+ deviceValue (str):
10106
+ The device id / name value to check.
10107
+ verifyUserContext (bool):
10108
+ If True, the active user context of the resolved device is checked to ensure it
10109
+ matches the user context specified on the class constructor.
10110
+ If False, the user context will not be checked.
10111
+ Default is True.
10112
+ verifyTimeout (float):
10113
+ Maximum time to wait (in seconds) for the device to become active in the Spotify
10114
+ Connect device list. This value is only used if a Connect command has to be
10115
+ issued to activate the device.
10116
+ Default is 5; value range is 0 - 10.
10117
+
10118
+ Returns:
10119
+ A device Id for the deviceValue if one could be resolved;
10120
+ otherwise, just the original deviceValue with the understanding that
10121
+ subsequent operations will probably fail since it's not in the device list.
10122
+
10123
+ Raises:
10124
+ SpotifyWebApiError:
10125
+ If the Spotify Web API request was for a non-authorization service
10126
+ and the response contains error information.
10127
+ SpotifyZeroconfApiError:
10128
+ If any Spotify Zeroconf API request failed.
10129
+ SpotifApiError:
10130
+ If the method fails for any other reason.
10131
+
10132
+ A Zeroconf discovery process is initiated to search for all Spotify Connect devices connected
10133
+ to the local network. A Spotify Zeroconf API `getInfo` call is then issued for each device
10134
+ that was found, searching for a match on the `deviceValue` argument. Depending on the format
10135
+ of the `deviceValue`, the search will match on either a Device ID or RemoteName as well as any
10136
+ alias ID's or Names that are in use (for multi-room configurations).
10137
+
10138
+ The device ID value is returned if a match is found; otherwise, null is returned.
10139
+
10140
+ A user context switch will also be performed if a device id is resolved AND the active user of the
10141
+ device does not match the `SpotifyConnectUsername` argument specified on the class constructor.
10142
+ The user context switch is bypassed if the `verifyUserContext` argument is False, or if the
10143
+ `SpotifyConnectUsername` was not supplied on the class constructor. If the user context is to be
10144
+ switched, then a Disconnect will be issued if a user context is active on the device followed by
10145
+ a Connect to the user context specified on the class constructor.
10146
+
10147
+ <details>
10148
+ <summary>Sample Code</summary>
10149
+ ```python
10150
+ .. include:: ../docs/include/samplecode/SpotifyClient/PlayerResolveDeviceId.py
10151
+ ```
10152
+ </details>
10153
+ """
10154
+ apiMethodName:str = 'PlayerResolveDeviceId'
10155
+ apiMethodParms:SIMethodParmListContext = None
10156
+ deviceIdResult:str = None
10157
+
10158
+ try:
10159
+
10160
+ # trace.
10161
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
10162
+ apiMethodParms.AppendKeyValue("deviceValue", deviceValue)
10163
+ apiMethodParms.AppendKeyValue("verifyUserContext", verifyUserContext)
10164
+ apiMethodParms.AppendKeyValue("verifyTimeout", verifyTimeout)
10165
+ _logsi.LogMethodParmList(SILevel.Verbose, "Resolving Spotify Connect Player Device Id", apiMethodParms)
10166
+
10167
+ # if device value not specified then we are done.
10168
+ if (deviceValue is None) or (len(deviceValue.strip()) == 0):
10169
+ _logsi.LogVerbose("deviceValue not specified; nothing to do")
10170
+ return None
10171
+
10172
+ # validations.
10173
+ verifyTimeout = self._ValidateDelay(verifyTimeout, 5, 10)
10174
+ if (not isinstance(verifyUserContext, bool)):
10175
+ verifyUserContext = True
10176
+
10177
+ # determine if the device value specified is a name (e.g. "Bose-ST10-1") or
10178
+ # an id (e.g. "30fbc80e35598f3c242f2120413c943dfd9715fe").
10179
+ isDeviceId:bool = SpotifyClient.IsDeviceId(deviceValue)
10180
+ deviceValueCompare:str = deviceValue.lower()
10181
+ deviceActiveUser:str = None
10182
+
10183
+ # discover Spotify Connect devices on the network, waiting up to the specified timeout
10184
+ # for all devices to be discovered. this will return all Spotify Connect devices that
10185
+ # are currently powered on, and are registered to Zeroconf / mDNS.
10186
+ _logsi.LogVerbose("Discovering Spotify Connect devices on the local network")
10187
+ discovery:SpotifyDiscovery = SpotifyDiscovery(self._Zeroconf, printToConsole=False)
10188
+ discovery.DiscoverDevices(timeout=self._SpotifyConnectDiscoveryTimeout)
10189
+
10190
+ # process all discovered devices.
10191
+ discoverResult:ZeroconfDiscoveryResult
10192
+ for discoverResult in discovery.DiscoveryResults:
10193
+
10194
+ # get the id from the device via the zeroconf API getInfo endpoint, as the id is not
10195
+ # returned in the zeroconf discovery result.
10196
+ zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpv4Address,
10197
+ discoverResult.HostIpPort,
10198
+ discoverResult.SpotifyConnectCPath,
10199
+ useSSL=False)
10200
+ info:ZeroconfGetInfo = zconn.GetInformation()
10201
+
10202
+ # store the currently active user of the device, in case we need to switch users later on.
10203
+ deviceActiveUser = info.ActiveUser.lower()
10204
+
10205
+ # are aliases being used (RemoteName is null if so)?
10206
+ if info.RemoteName is None:
10207
+
10208
+ # if aliases are defined for the device, then we have to check each alias for a match.
10209
+ infoAlias:ZeroconfGetInfoAlias
10210
+ for infoAlias in info.Aliases:
10211
+
10212
+ # if input deviceValue is a device id then compare the id; otherwise, compare the name.
10213
+ if isDeviceId:
10214
+ if (infoAlias.Id.lower() == deviceValueCompare):
10215
+ _logsi.LogVerbose("Device value '%s' was matched to device Alias Id '%s' (%s)" % (deviceValue, infoAlias.Name, infoAlias.Id))
10216
+ deviceIdResult = infoAlias.Id
10217
+ break # alias processing loop
10218
+ else:
10219
+ if (infoAlias.Name.lower() == deviceValueCompare):
10220
+ _logsi.LogVerbose("Device value '%s' was matched to device Alias Name '%s' (%s)" % (deviceValue, infoAlias.Name, infoAlias.Id))
10221
+ deviceIdResult = infoAlias.Id
10222
+ break # alias processing loop
10223
+
10224
+ else:
10225
+
10226
+ # if input deviceValue is a device id then compare the id; otherwise, compare the name.
10227
+ if isDeviceId:
10228
+ if (info.DeviceId.lower() == deviceValueCompare):
10229
+ _logsi.LogVerbose("Device value '%s' was matched to device Id '%s' (%s)" % (deviceValue, info.RemoteName, info.DeviceId))
10230
+ deviceIdResult = info.DeviceId
10231
+ else:
10232
+ if (info.RemoteName.lower() == deviceValueCompare):
10233
+ _logsi.LogVerbose("Device value '%s' was matched to device RemoteName '%s' (%s)" % (deviceValue, info.RemoteName, info.DeviceId))
10234
+ deviceIdResult = info.DeviceId
10235
+
10236
+ # did we resolve the device id?
10237
+ if deviceIdResult is not None:
10238
+
10239
+ # did caller request user context switch bypass?
10240
+ # if so, then just return the device id.
10241
+ if (not verifyUserContext):
10242
+ _logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as user chose to bypass user context switch" % (deviceActiveUser, deviceIdResult))
10243
+ break
10244
+
10245
+ # was a Spotify Connect user account specified on the class constructor?
10246
+ # if not, then do not switch the user context.
10247
+ if (self._SpotifyConnectUsername is None):
10248
+ _logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as a SpotifyConnectUsername parameter was not supplied" % (deviceActiveUser, deviceIdResult))
10249
+ break
10250
+
10251
+ # does our Spotify Connect user account need to take control of the device?
10252
+ # if not, then we are done.
10253
+ if (deviceActiveUser == self._SpotifyConnectUsername.lower()) or (deviceActiveUser == self.UserProfile.Id.lower()):
10254
+ _logsi.LogVerbose("Spotify Connect user context '%s' was verified for Device id '%s'; switch not necessary" % (deviceActiveUser, deviceIdResult))
10255
+ break
10256
+
10257
+ zcfResult:ZeroconfResponse
10258
+
10259
+ # if a different user context has control of the device then we need to disconnect
10260
+ # the current user context before connecting a different user.
10261
+ if (info.HasActiveUser):
10262
+ _logsi.LogVerbose("Spotify Connect user context '%s' is being disconnected for Device id '%s'" % (deviceActiveUser, deviceIdResult))
10263
+ zcfResult = zconn.Disconnect()
10264
+
10265
+ # connect the device to OUR Spotify Connect user context.
10266
+ # note that the result here only indicates that the connect was submitted - NOT that it was successful!
10267
+ _logsi.LogVerbose("Spotify Connect user context '%s' is being connected for Device id '%s'" % (self._SpotifyConnectUsername, deviceIdResult))
10268
+ zcfResult = zconn.Connect(self._SpotifyConnectUsername, self._SpotifyConnectPassword)
10269
+
10270
+ # trace.
10271
+ _logsi.LogVerbose("Spotify Connect user context switch from '%s' to '%s' was requested for Device id '%s'" % (deviceActiveUser, self._SpotifyConnectUsername, deviceIdResult))
10272
+
10273
+ loopTotalDelay:float = 0
10274
+ LOOP_DELAY:float = 0.25
10275
+ while True:
10276
+
10277
+ # wait just a bit between active device list queries.
10278
+ _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % LOOP_DELAY)
10279
+ time.sleep(LOOP_DELAY)
10280
+ loopTotalDelay = loopTotalDelay + LOOP_DELAY
10281
+
10282
+ # check for the device in the active device list.
10283
+ # if we find it, then we are done.
10284
+ device:Device = self.GetPlayerDevice(deviceIdResult, True)
10285
+ if device is not None:
10286
+ _logsi.LogVerbose("Spotify Connect Device id '%s' is now in the active device list; device found within %f seconds of Connect" % (deviceIdResult, loopTotalDelay))
10287
+ break
10288
+
10289
+ # only check so many times before we give up;
10290
+ # we will keep checking until we find the device, or we exceed the verifyTimeout value.
10291
+ if (loopTotalDelay > verifyTimeout):
10292
+ _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))
10293
+ break
10294
+
10295
+ # end the device processing loop.
10296
+ break
10297
+
10298
+ # did we resolve the device id?
10299
+ if deviceIdResult is None:
10300
+ _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))
10301
+
10302
+ # return the resolved device id.
10303
+ return deviceIdResult
10304
+
10305
+ except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
10306
+ except SpotifyApiError: raise # pass handled exceptions on thru
10307
+ except Exception as ex:
10308
+
10309
+ # format unhandled exception.
10310
+ raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
10311
+
10312
+ finally:
10313
+
10314
+ # trace.
10315
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
10316
+
10317
+
9833
10318
  def PlayerSetRepeatMode(self,
9834
10319
  state:str='off',
9835
10320
  deviceId:str=None,
@@ -10187,7 +10672,11 @@ class SpotifyClient:
10187
10672
  delay = self._ValidateDelay(delay, 0.50, 10)
10188
10673
 
10189
10674
  # check for device name; convert to an id if a name was supplied.
10190
- deviceId = self.PlayerConvertDeviceNameToId(deviceId)
10675
+ #deviceId = self.PlayerConvertDeviceNameToId(deviceId)
10676
+
10677
+ # ensure the specified device id / name is active and available, and
10678
+ # the user context is set correctly.
10679
+ deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
10191
10680
 
10192
10681
  # build spotify web api request parameters.
10193
10682
  reqData:dict = \