spotifywebapiPython 1.0.61__py3-none-any.whl → 1.0.63__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of spotifywebapiPython might be problematic. Click here for more details.

Files changed (25) hide show
  1. {spotifywebapiPython-1.0.61.dist-info → spotifywebapiPython-1.0.63.dist-info}/METADATA +1 -1
  2. {spotifywebapiPython-1.0.61.dist-info → spotifywebapiPython-1.0.63.dist-info}/RECORD +25 -25
  3. spotifywebapipython/const.py +1 -1
  4. spotifywebapipython/docs/__init__.html +15 -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 +33 -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 +2 -2
  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/spotifyconnectdevices.py +24 -2
  18. spotifywebapipython/sautils.py +22 -0
  19. spotifywebapipython/spotifyclient.py +33 -18
  20. spotifywebapipython/spotifydiscovery.py +28 -8
  21. spotifywebapipython/zeroconfapi/zeroconfconnect.py +135 -68
  22. spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +20 -3
  23. {spotifywebapiPython-1.0.61.dist-info → spotifywebapiPython-1.0.63.dist-info}/LICENSE +0 -0
  24. {spotifywebapiPython-1.0.61.dist-info → spotifywebapiPython-1.0.63.dist-info}/WHEEL +0 -0
  25. {spotifywebapiPython-1.0.61.dist-info → spotifywebapiPython-1.0.63.dist-info}/top_level.txt +0 -0
@@ -18,6 +18,7 @@ from ..spotifywebapierror import SpotifyWebApiError
18
18
  from ..const import (
19
19
  TRACE_MSG_DELAY_DEVICE,
20
20
  )
21
+ from ..sautils import passwordMaskString
21
22
 
22
23
  # get smartinspect logger reference; create a new session for this module name.
23
24
  from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SISourceId
@@ -142,7 +143,7 @@ class ZeroconfConnect:
142
143
  return self._Version
143
144
 
144
145
 
145
- def _CheckResponseForErrors(self, response:Response, methodName:str) -> None:
146
+ def _CheckResponseForErrors(self, response:Response, methodName:str, endpoint:str) -> None:
146
147
  """
147
148
  Checks the Spotify Zeroconf API response for errors.
148
149
 
@@ -157,6 +158,8 @@ class ZeroconfConnect:
157
158
  Spotify Web API http response object.
158
159
  methodName (str):
159
160
  method name that made the request (for trace purposes).
161
+ endpoint (str):
162
+ Endpoint that was requested (for trace purposes).
160
163
 
161
164
  Raises:
162
165
  SpotifyZeroconfApiError:
@@ -164,7 +167,6 @@ class ZeroconfConnect:
164
167
  """
165
168
  responseData:dict = None
166
169
  responseUTF8:str = None
167
- contentType:str = None
168
170
 
169
171
  try:
170
172
 
@@ -172,25 +174,11 @@ class ZeroconfConnect:
172
174
  if _logsi.IsOn(SILevel.Debug):
173
175
  _logsi.LogObject(SILevel.Debug, 'ZeroconfConnect http response object - type="%s", module="%s"' % (type(response).__name__, type(response).__module__), response)
174
176
 
175
- # safely get the response url value.
176
- # for some reason, the 'url' attribute is not present sometimes if a redirect occurs on the request.
177
- responseUrl:str = None
178
- if hasattr(response, 'url'):
179
- responseUrl = response.url
180
- elif hasattr(response, '_request_url'):
181
- responseUrl = response._request_url
182
- else:
183
- try:
184
- responseUrl = response.geturl()
185
- except Exception:
186
- _logsi.LogWarning('HTTPResponse method "geturl()" could not be called - defaulting to "unknown response url"')
187
- responseUrl = 'Unknown response url'
188
-
189
177
  # trace.
190
178
  if _logsi.IsOn(SILevel.Debug):
191
- _logsi.LogObject(SILevel.Debug, "ZeroconfConnect http response [%s-%s]: '%s' (response)" % (response.status_code, response.reason, responseUrl), response)
179
+ _logsi.LogObject(SILevel.Debug, "ZeroconfConnect http response [%s-%s]: '%s' (response)" % (response.status_code, response.reason, endpoint), response)
192
180
  if (response.headers):
193
- _logsi.LogCollection(SILevel.Debug, "ZeroconfConnect http response [%s-%s]: '%s' (headers)" % (response.status_code, response.reason, responseUrl), response.headers.items())
181
+ _logsi.LogCollection(SILevel.Debug, "ZeroconfConnect http response [%s-%s]: '%s' (headers)" % (response.status_code, response.reason, endpoint), response.headers.items())
194
182
 
195
183
  # do we have response data?
196
184
  if (response.content is not None) and (len(response.content) > 0):
@@ -203,11 +191,11 @@ class ZeroconfConnect:
203
191
 
204
192
  if _logsi.IsOn(SILevel.Verbose):
205
193
  if isinstance(responseData, dict):
206
- _logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json dict)" % (response.status_code, response.reason, responseUrl), responseData)
194
+ _logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json dict)" % (response.status_code, response.reason, endpoint), responseData)
207
195
  elif isinstance(responseData, list):
208
- _logsi.LogArray(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json array)" % (response.status_code, response.reason, responseUrl), responseData)
196
+ _logsi.LogArray(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json array)" % (response.status_code, response.reason, endpoint), responseData)
209
197
  else:
210
- _logsi.LogObject(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json object)" % (response.status_code, response.reason, responseUrl), responseData)
198
+ _logsi.LogObject(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json object)" % (response.status_code, response.reason, endpoint), responseData)
211
199
  pass
212
200
 
213
201
  else:
@@ -225,7 +213,7 @@ class ZeroconfConnect:
225
213
  # if json conversion failed, then convert to utf-8 response.
226
214
  if response.content is not None:
227
215
  responseUTF8 = response.content.decode('utf-8')
228
- _logsi.LogText(SILevel.Error, "ZeroconfConnect http response [%s-%s]: '%s' (utf-8)" % (response.status_code, response.reason, responseUrl), responseUTF8)
216
+ _logsi.LogText(SILevel.Error, "ZeroconfConnect http response [%s-%s]: '%s' (utf-8)" % (response.status_code, response.reason, endpoint), responseUTF8)
229
217
 
230
218
  # at this point we don't know what Spotify Web Api returned, so let's
231
219
  # just raise a new exception with the non-JSON response data.
@@ -234,21 +222,44 @@ class ZeroconfConnect:
234
222
  errCode:str = None
235
223
  errMessage:str = None
236
224
 
237
- # if response is not in the 2xx range then it's an error, even
238
- # though no json 'error' response body was returned! this is
239
- # usually due to a '405 - method not allowed' or '403-Forbidden',
240
- # or '503 Server Error' response.
241
- if response.status_code > 299:
242
-
243
- errCode = response.status_code
244
- errMessage = response.reason
245
-
246
- if responseUTF8 is not None:
247
- errMessage = responseUTF8
225
+ # at this point we know that the response contains JSON data, as
226
+ # we would have raised an exception before this if we could not
227
+ # convert the response to JSON!
248
228
 
249
- raise SpotifyZeroconfApiError(errCode, errMessage, methodName, response.reason, _logsi)
229
+ # now it's a matter of interpreting the JSON `Status` response to determine
230
+ # if the request failed or not. the following documents the Spotify
231
+ # Zeroconf API http status codes and JSON status codes.
232
+
233
+ # taken from: https://developer.spotify.com/documentation/commercial-hardware/implementation/guides/zeroconf
234
+ # HTTP Status codes
235
+ # The following table shows the different responses returned by the HTTP server to a client's request:
236
+
237
+ # Status name Status HTTP Status Status string Response to Usage
238
+ # Ok 101 200 OK or ERROR-OK All Successful operation
239
+ # Bad 102 400 ERROR-BAD-REQUEST All Web server problem or critically malformed request
240
+ # Unknown 103 500 ERROR-UNKNOWN All Fallback when no other error applies
241
+ # NotImplemented 104 501 ERROR-NOT-IMPLEMENTED All Server does not implement this feature
242
+ # LoginFailed 202 200 ERROR-LOGIN-FAILED addUser Spotify returned error when trying to login
243
+ # MissingAction 301 400 ERROR-MISSING-ACTION All Web request has no action parameter
244
+ # InvalidAction 302 400 ERROR-INVALID-ACTION All Web request has unrecognized action parameter
245
+ # InvalidArguments 303 400 ERROR-INVALID-ARGUMENTS All Incorrect or insufficient arguments supplied for requested action
246
+ # SpotifyError 402 200 ERROR-SPOTIFY-ERROR All A Spotify API call returned an error not covered by other error messages
247
+
248
+ # process results.
249
+ result = ZeroconfResponse(root=responseData)
250
+
251
+ # if result is ok, then just return the response data.
252
+ if (result.Status == 101) \
253
+ or (result.StatusString == 'OK') \
254
+ or (result.StatusString == 'ERROR-OK'):
255
+ return responseData
256
+
257
+ # was an error status string returned?
258
+ # if so, then we will raise an exception.
259
+ if (result.StatusString.startswith('ERROR-')):
260
+ raise SpotifyZeroconfApiError(result.Status, result.ToString(), methodName, result.StatusString, _logsi)
250
261
 
251
- # no errors found - set message object response data.
262
+ # otherwise, let the calling function process the returned status.
252
263
  return responseData
253
264
 
254
265
 
@@ -319,8 +330,9 @@ class ZeroconfConnect:
319
330
  apiMethodParms.AppendKeyValue("Version", self._Version)
320
331
  apiMethodParms.AppendKeyValue("Uri", self._Uri)
321
332
  apiMethodParms.AppendKeyValue("username", username)
333
+ apiMethodParms.AppendKeyValue("password (with mask)", passwordMaskString(password))
322
334
  apiMethodParms.AppendKeyValue("delay", delay)
323
- _logsi.LogMethodParmList(SILevel.Verbose, "Connecting device to Spotify Connect using specified Username and Password", apiMethodParms)
335
+ _logsi.LogMethodParmList(SILevel.Verbose, "Connecting device to Spotify Connect using specified Username and Password (ip=%s)" % self._HostIpAddress, apiMethodParms)
324
336
 
325
337
  # validations.
326
338
  if (username is None) or (not isinstance(username,str)):
@@ -337,25 +349,36 @@ class ZeroconfConnect:
337
349
  builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
338
350
  blob = builder.build()
339
351
 
352
+ # set request endpoint.
353
+ endpoint:str = self.GetEndpoint('addUser')
354
+
355
+ # set request headers.
356
+ reqHeaders:dict = {
357
+ 'Content-Type': 'application/x-www-form-urlencoded',
358
+ 'Connection': 'close'
359
+ }
360
+ _logsi.LogDictionary(SILevel.Debug, "ZeroconfConnect http request: '%s' (headers)" % (endpoint), reqHeaders)
361
+
362
+ # set request parameters.
363
+ reqData={
364
+ 'action': 'addUser',
365
+ 'VERSION': self._Version,
366
+ 'userName': credentials.username.decode('ascii'),
367
+ 'clientKey': int_to_b64str(builder.dh_keys.public_key),
368
+ 'blob': blob
369
+ }
370
+ _logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (data)" % (endpoint), reqData)
371
+
340
372
  # execute spotify zeroconf api request.
341
373
  response = requests.post(
342
374
  self._Uri,
343
375
  timeout=10,
344
- headers={
345
- 'Content-Type': 'application/x-www-form-urlencoded',
346
- 'Connection': 'close'
347
- },
348
- data={
349
- 'action': 'addUser',
350
- 'VERSION': self._Version,
351
- 'userName': credentials.username.decode('ascii'),
352
- 'clientKey': int_to_b64str(builder.dh_keys.public_key),
353
- 'blob': blob
354
- }
376
+ headers=reqHeaders,
377
+ data=reqData # send data in POST request body
355
378
  )
356
379
 
357
380
  # check response for errors, and return json response.
358
- responseData:dict = self._CheckResponseForErrors(response, apiMethodName)
381
+ responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
359
382
 
360
383
  # process results.
361
384
  result = ZeroconfResponse(root=responseData)
@@ -434,27 +457,38 @@ class ZeroconfConnect:
434
457
  apiMethodParms.AppendKeyValue("Version", self._Version)
435
458
  apiMethodParms.AppendKeyValue("Uri", self._Uri)
436
459
  apiMethodParms.AppendKeyValue("delay", delay)
437
- _logsi.LogMethodParmList(SILevel.Verbose, "Disconnecting device from Spotify Connect", apiMethodParms)
460
+ _logsi.LogMethodParmList(SILevel.Verbose, "Disconnecting device from Spotify Connect (ip=%s)" % self._HostIpAddress, apiMethodParms)
438
461
 
439
462
  # validations.
440
463
  delay = validateDelay(delay, 0.50, 10)
464
+
465
+ # set request endpoint.
466
+ endpoint:str = self.GetEndpoint('resetUsers')
467
+
468
+ # set request headers.
469
+ reqHeaders:dict = {
470
+ 'Content-Type': 'application/x-www-form-urlencoded',
471
+ 'Connection': 'close'
472
+ }
473
+ _logsi.LogDictionary(SILevel.Debug, "ZeroconfConnect http request: '%s' (headers)" % (endpoint), reqHeaders)
474
+
475
+ # set request parameters.
476
+ reqData={
477
+ 'action': 'resetUsers',
478
+ 'VERSION': self._Version,
479
+ }
480
+ _logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (data)" % (endpoint), reqData)
441
481
 
442
482
  # execute spotify zeroconf api request.
443
483
  response = requests.post(
444
484
  self._Uri,
445
485
  timeout=10,
446
- headers={
447
- 'Content-Type': 'application/x-www-form-urlencoded',
448
- 'Connection': 'close'
449
- },
450
- data={
451
- 'action': 'resetUsers',
452
- 'VERSION': self._Version,
453
- }
486
+ headers=reqHeaders,
487
+ data=reqData # send data in POST request body
454
488
  )
455
489
 
456
490
  # check response for errors, and return json response.
457
- responseData:dict = self._CheckResponseForErrors(response, apiMethodName)
491
+ responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
458
492
 
459
493
  # process results.
460
494
  result = ZeroconfResponse(root=responseData)
@@ -506,6 +540,27 @@ class ZeroconfConnect:
506
540
  )
507
541
 
508
542
 
543
+ def GetEndpoint(
544
+ self,
545
+ action:str
546
+ ) -> str:
547
+ """
548
+ Gets a Spotify Zeroconf API endpoint uri.
549
+
550
+ Args:
551
+ action (str):
552
+ Action parameter value (e.g. "getInfo", "addUser", "resetUsers", etc).
553
+
554
+ Returns:
555
+ A string containing the endpoint.
556
+ """
557
+ return "{uri}?action={action}&version={version}".format(
558
+ uri=self._Uri,
559
+ action=action,
560
+ version=self._Version,
561
+ )
562
+
563
+
509
564
  def GetInformation(self) -> ZeroconfGetInfo:
510
565
  """
511
566
  Calls the `getInfo` Spotify Zeroconf API endpoint to return information about the device.
@@ -539,23 +594,35 @@ class ZeroconfConnect:
539
594
  apiMethodParms.AppendKeyValue("CPath", self._CPath)
540
595
  apiMethodParms.AppendKeyValue("Version", self._Version)
541
596
  apiMethodParms.AppendKeyValue("Uri", self._Uri)
542
- _logsi.LogMethodParmList(SILevel.Verbose, "Get information from Spotify Connect for device", apiMethodParms)
597
+ _logsi.LogMethodParmList(SILevel.Verbose, "Get information from Spotify Connect Zeroconf API (ip=%s)" % self._HostIpAddress, apiMethodParms)
598
+
599
+ # set request endpoint.
600
+ endpoint:str = self.GetEndpoint('getInfo')
543
601
 
602
+ # set request headers.
603
+ reqHeaders:dict = {
604
+ 'Content-Type': 'application/x-www-form-urlencoded',
605
+ 'Connection': 'close'
606
+ }
607
+ _logsi.LogDictionary(SILevel.Debug, "ZeroconfConnect http request: '%s' (headers)" % (endpoint), reqHeaders)
608
+
609
+ # set request parameters.
610
+ reqParams={
611
+ 'action': 'getInfo',
612
+ 'VERSION': self._Version,
613
+ }
614
+ _logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (params)" % (endpoint), reqParams)
615
+
544
616
  # execute spotify zeroconf api request.
545
617
  response = requests.get(
546
618
  self._Uri,
547
619
  timeout=10,
548
- headers={
549
- 'Content-Type': 'application/x-www-form-urlencoded',
550
- 'Connection': 'close'
551
- },
552
- params={
553
- 'action': 'getInfo',
554
- 'VERSION': self._Version,
555
- })
620
+ headers=reqHeaders,
621
+ params=reqParams
622
+ )
556
623
 
557
624
  # check response for errors, and return json response.
558
- responseData:dict = self._CheckResponseForErrors(response, apiMethodName)
625
+ responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
559
626
 
560
627
  # process results.
561
628
  result = ZeroconfGetInfo(root=responseData)
@@ -28,7 +28,7 @@ class ZeroconfGetInfo(ZeroconfResponse):
28
28
  self._AccountReq:str = None
29
29
  self._ActiveUser:str = ""
30
30
  self._Aliases:list[ZeroconfGetInfoAlias] = []
31
- self._Availability:str = None
31
+ self._Availability:str = ""
32
32
  self._BrandDisplayName:str = None
33
33
  self._ClientId:str = None
34
34
  self._DeviceId:str = None
@@ -58,7 +58,7 @@ class ZeroconfGetInfo(ZeroconfResponse):
58
58
 
59
59
  self._AccountReq = root.get('accountReq', None)
60
60
  self._ActiveUser = root.get('activeUser', "")
61
- self._Availability = root.get('availability', None)
61
+ self._Availability = root.get('availability', "")
62
62
  self._BrandDisplayName = root.get('brandDisplayName', None)
63
63
  self._ClientId = root.get('clientID', None)
64
64
  self._DeviceId = root.get('deviceID', None)
@@ -142,7 +142,12 @@ class ZeroconfGetInfo(ZeroconfResponse):
142
142
  @property
143
143
  def Availability(self) -> str:
144
144
  """
145
- The SpZeroConfVars availability field returned by SpZeroConfGetVars.
145
+ The SpZeroConfVars availability field returned by SpZeroConfGetVars.
146
+
147
+ The following are values that I have encountered thus far:
148
+ - "" = Device is available and ready for use.
149
+ - "UNAVAILABLE" - Device is unavailable, and should probably be rebooted.
150
+ - "NOT-LOADED" - Spotify SDK / API is not loaded.
146
151
  """
147
152
  return self._Availability
148
153
 
@@ -237,6 +242,18 @@ class ZeroconfGetInfo(ZeroconfResponse):
237
242
  return (len(self._Aliases) > 0)
238
243
 
239
244
 
245
+ @property
246
+ def IsAvailable(self) -> bool:
247
+ """
248
+ Returns True if the device is available; otherwise, False.
249
+
250
+ Determination is made based upon the `Availability` property value.
251
+ """
252
+ if (self._Availability is None) or (self._Availability == ""):
253
+ return True
254
+ return False
255
+
256
+
240
257
  @property
241
258
  def LibraryVersion(self) -> str:
242
259
  """