spotifywebapiPython 1.0.64__py3-none-any.whl → 1.0.66__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 (22) hide show
  1. {spotifywebapiPython-1.0.64.dist-info → spotifywebapiPython-1.0.66.dist-info}/METADATA +1 -1
  2. {spotifywebapiPython-1.0.64.dist-info → spotifywebapiPython-1.0.66.dist-info}/RECORD +22 -22
  3. spotifywebapipython/const.py +1 -1
  4. spotifywebapipython/docs/__init__.html +14 -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 +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/zeroconfapi/zeroconfconnect.py +125 -49
  18. spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +9 -1
  19. spotifywebapipython/zeroconfapi/zeroconfresponse.py +12 -3
  20. {spotifywebapiPython-1.0.64.dist-info → spotifywebapiPython-1.0.66.dist-info}/LICENSE +0 -0
  21. {spotifywebapiPython-1.0.64.dist-info → spotifywebapiPython-1.0.66.dist-info}/WHEEL +0 -0
  22. {spotifywebapiPython-1.0.64.dist-info → spotifywebapiPython-1.0.66.dist-info}/top_level.txt +0 -0
@@ -68,10 +68,6 @@ class ZeroconfConnect:
68
68
  True if the host device utilizes HTTPS Secure Sockets Layer (SSL) support;
69
69
  otherwise, False to utilize HTTP.
70
70
  Default is False (HTTP).
71
-
72
- The URI value consists of an IP Address, port, CPath, and version value that are used to send
73
- requests to / receive responses from a headless Spotify Connect device. These values can be
74
- obtained from a Zeroconf discovery process.
75
71
  """
76
72
  # validations.
77
73
  if (version is None):
@@ -191,7 +187,7 @@ class ZeroconfConnect:
191
187
 
192
188
  if _logsi.IsOn(SILevel.Verbose):
193
189
  if isinstance(responseData, dict):
194
- _logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json dict)" % (response.status_code, response.reason, endpoint), responseData)
190
+ _logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json dict)" % (response.status_code, response.reason, endpoint), responseData, prettyPrint=True)
195
191
  elif isinstance(responseData, list):
196
192
  _logsi.LogArray(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json array)" % (response.status_code, response.reason, endpoint), responseData)
197
193
  else:
@@ -219,7 +215,6 @@ class ZeroconfConnect:
219
215
  # just raise a new exception with the non-JSON response data.
220
216
  raise SpotifyZeroconfApiError(response.status_code, responseUTF8, methodName, response.reason, _logsi)
221
217
 
222
- errCode:str = None
223
218
  errMessage:str = None
224
219
 
225
220
  # at this point we know that the response contains JSON data, as
@@ -232,14 +227,15 @@ class ZeroconfConnect:
232
227
 
233
228
  # taken from: https://developer.spotify.com/documentation/commercial-hardware/implementation/guides/zeroconf
234
229
  # HTTP Status codes
230
+ # Note It is permissible to send HTTP status code 200 in all cases, as long as a valid JSON reply is returned that contains the status, statusString, and spotifyError strings.
235
231
  # The following table shows the different responses returned by the HTTP server to a client's request:
236
-
237
232
  # Status name Status HTTP Status Status string Response to Usage
238
233
  # Ok 101 200 OK or ERROR-OK All Successful operation
239
234
  # Bad 102 400 ERROR-BAD-REQUEST All Web server problem or critically malformed request
240
235
  # Unknown 103 500 ERROR-UNKNOWN All Fallback when no other error applies
241
236
  # NotImplemented 104 501 ERROR-NOT-IMPLEMENTED All Server does not implement this feature
242
237
  # LoginFailed 202 200 ERROR-LOGIN-FAILED addUser Spotify returned error when trying to login
238
+ # InvalidPublicKey 203 xxx ERROR-INVALID-PUBLICKEY addUser ZeroConf login failed due to an invalid public key
243
239
  # MissingAction 301 400 ERROR-MISSING-ACTION All Web request has no action parameter
244
240
  # InvalidAction 302 400 ERROR-INVALID-ACTION All Web request has unrecognized action parameter
245
241
  # InvalidArguments 303 400 ERROR-INVALID-ARGUMENTS All Incorrect or insufficient arguments supplied for requested action
@@ -267,6 +263,7 @@ class ZeroconfConnect:
267
263
  self,
268
264
  username:str,
269
265
  password:str,
266
+ loginId:str=None,
270
267
  delay:float=0.50
271
268
  ) -> ZeroconfResponse:
272
269
  """
@@ -276,11 +273,15 @@ class ZeroconfConnect:
276
273
 
277
274
  Args:
278
275
  username (str):
279
- Spotify Connect user name to login with.
276
+ Spotify Connect user name to login with (e.g. "yourspotifyusername").
280
277
  This MUST match the account name (or one of them) that was used to configure Spotify Connect
281
278
  on the manufacturer device.
282
279
  password (str):
283
280
  Spotify Connect user password to login with.
281
+ loginId (str):
282
+ Spotify Connect login id to login with (e.g. "31l77fd87g8h9j00k89f07jf87ge").
283
+ This is also known as the canonical user id value.
284
+ This MUST be the value that relates to the `username` argument.
284
285
  delay (float):
285
286
  Time delay (in seconds) to wait AFTER issuing the command to the device.
286
287
  This delay will give the spotify zeroconf api time to process the change before
@@ -297,6 +298,14 @@ class ZeroconfConnect:
297
298
  This will first issue a call to the `getInfo` Spotify Zeroconf API endpoint to retrieve the Spotify
298
299
  Connect device id associated with the device. It will then issue a call to the `addUser` Spotify Zeroconf
299
300
  API endpoint to add the user to the device.
301
+
302
+ Some Spotify Connect device types will be "woken up" with the initial `addUser` request; when this happens,
303
+ the initial request will return a 203 status (ERROR-INVALID-PUBLICKEY), and return a valid public key in the
304
+ response. This public key is then used to submit another `addUser` request to connect to the device with the
305
+ newly returned public key. When this happens, a `ZeroconfResponse` object is returned with the result of
306
+ the final `addUser` request. If only one `addUser` request is processed, then a `ZeroconfGetInfo` object is
307
+ returned with the result of the `addUser` request. Note that `ZeroconfGetInfo' inherits from `ZeroconfResponse`,
308
+ so you can always treat the result of this method as a `ZeroconfResponse` object.
300
309
 
301
310
  The login (on the device) is performed asynchronously, so the return result only indicates whether the library
302
311
  is able to perform the login attempt. You should issue a call to the Spotify Web API `Get Available Devices`
@@ -329,6 +338,7 @@ class ZeroconfConnect:
329
338
  apiMethodParms.AppendKeyValue("CPath", self._CPath)
330
339
  apiMethodParms.AppendKeyValue("Version", self._Version)
331
340
  apiMethodParms.AppendKeyValue("Uri", self._Uri)
341
+ apiMethodParms.AppendKeyValue("loginId", loginId)
332
342
  apiMethodParms.AppendKeyValue("username", username)
333
343
  apiMethodParms.AppendKeyValue("password (with mask)", passwordMaskString(password))
334
344
  apiMethodParms.AppendKeyValue("delay", delay)
@@ -343,49 +353,55 @@ class ZeroconfConnect:
343
353
 
344
354
  # get the current device id from the device via Spotify ZeroConf API `getInfo` endpoint.
345
355
  info:ZeroconfGetInfo = self.GetInformation()
346
-
347
- # formulate the blob.
348
- credentials:Credentials = Credentials(username, password)
349
- builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
350
- blob = builder.build()
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
356
 
372
- # execute spotify zeroconf api request.
373
- response = requests.post(
374
- self._Uri,
375
- timeout=10,
376
- headers=reqHeaders,
377
- data=reqData # send data in POST request body
378
- )
379
-
380
- # check response for errors, and return json response.
381
- responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
357
+ # execute the Spotify Zeroconf API addUser request.
358
+ responseData:dict = self._ConnectAddUser(
359
+ info,
360
+ username,
361
+ password,
362
+ loginId,
363
+ apiMethodName)
382
364
 
383
365
  # process results.
384
- result = ZeroconfResponse(root=responseData)
366
+ # we will process the results with a `ZeroconfGetInfo` object, in case a publicKey was returned.
367
+ result = ZeroconfGetInfo(root=responseData)
385
368
 
369
+ # does result contain a public key?
370
+ # some spotify connect devices need to be "woken up" with the initial `addUser` request.
371
+ # when this happens, the initial request will return a 203 status (ERROR-INVALID-PUBLICKEY), and
372
+ # return a valid public key in the response. we can then take this public key and try the
373
+ # `addUser` request again.
374
+ if (result.Status == 203) and (result.SpotifyError == 0): # and (result.StatusString == "ERROR-INVALID-PUBLICKEY"):
375
+
376
+ # trace.
377
+ _logsi.LogObject(SILevel.Verbose, '%s result (%s)' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
378
+ _logsi.LogVerbose("Spotify Connect addUser request returned an ERROR-INVALID-PUBLICKEY response for Device id '%s'; addUser will be retried with the returned public key" % (info.DeviceId))
379
+
380
+ # give the device a little time to wake up.
381
+ WAKEUP_DELAY:float = 0.25
382
+ _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % WAKEUP_DELAY)
383
+ time.sleep(WAKEUP_DELAY)
384
+
385
+ # move the newly returned public key to the getInfo results.
386
+ # try the addUser request again, this time with a (hopefully) valid public key.
387
+ info.PublicKey = result.PublicKey
388
+ self._ConnectAddUser(
389
+ info,
390
+ username,
391
+ password,
392
+ loginId,
393
+ apiMethodName)
394
+
395
+ # process results.
396
+ result = ZeroconfResponse(root=responseData)
397
+
386
398
  # trace.
387
- _logsi.LogObject(SILevel.Verbose, '%s result - type="%s"' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
388
-
399
+ _logsi.LogObject(SILevel.Verbose, '%s result (%s)' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
400
+
401
+ # if result status is not ok, then raise an exception.
402
+ if (result.Status != 101):
403
+ raise SpotifyZeroconfApiError(result.Status, result.ToString(), apiMethodName, result.StatusString, _logsi)
404
+
389
405
  # give spotify zeroconf api time to process the change.
390
406
  if delay > 0:
391
407
  _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % delay)
@@ -411,6 +427,56 @@ class ZeroconfConnect:
411
427
  _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
412
428
 
413
429
 
430
+ def _ConnectAddUser(
431
+ self,
432
+ info:ZeroconfGetInfo,
433
+ username:str,
434
+ password:str,
435
+ loginId:str,
436
+ apiMethodName:str,
437
+ ) -> dict:
438
+ """
439
+ """
440
+ # formulate the blob.
441
+ credentials:Credentials = Credentials(username, password)
442
+ builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
443
+ blob = builder.build()
444
+
445
+ # set request endpoint.
446
+ endpoint:str = self.GetEndpoint('addUser')
447
+
448
+ # set request headers.
449
+ reqHeaders:dict = {
450
+ 'Content-Type': 'application/x-www-form-urlencoded',
451
+ 'Connection': 'close'
452
+ }
453
+ _logsi.LogDictionary(SILevel.Debug, "ZeroconfConnect http request: '%s' (headers)" % (endpoint), reqHeaders)
454
+
455
+ # set request parameters.
456
+ reqData={
457
+ 'action': 'addUser',
458
+ 'VERSION': self._Version,
459
+ 'userName': credentials.username.decode('ascii'), # user name (e.g. "youremail@mail.com")
460
+ 'clientKey': int_to_b64str(builder.dh_keys.public_key),
461
+ 'blob': blob,
462
+ 'tokenType': 'default', # NOTE - not the same as info.TokenType!
463
+ 'loginId': loginId or credentials.username.decode('ascii'), # canonical login id (e.g. "31l77fd87g8h9j00k89f07jf87ge")
464
+ }
465
+ _logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (data)" % (endpoint), reqData)
466
+
467
+ # execute spotify zeroconf api request.
468
+ response = requests.post(
469
+ self._Uri,
470
+ timeout=10,
471
+ headers=reqHeaders,
472
+ data=reqData # send data in POST request body
473
+ )
474
+
475
+ # check response for initial errors, and return json response.
476
+ responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
477
+ return responseData
478
+
479
+
414
480
  def Disconnect(
415
481
  self,
416
482
  delay:float=0.50
@@ -487,14 +553,18 @@ class ZeroconfConnect:
487
553
  data=reqData # send data in POST request body
488
554
  )
489
555
 
490
- # check response for errors, and return json response.
556
+ # check response for initial errors, and return json response.
491
557
  responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
492
558
 
493
559
  # process results.
494
560
  result = ZeroconfResponse(root=responseData)
495
561
 
496
562
  # trace.
497
- _logsi.LogObject(SILevel.Verbose, '%s result - type="%s"' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
563
+ _logsi.LogObject(SILevel.Verbose, '%s result (%s)' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
564
+
565
+ # if result status is not ok, then raise an exception.
566
+ if (result.Status != 101):
567
+ raise SpotifyZeroconfApiError(result.Status, result.ToString(), apiMethodName, result.StatusString, _logsi)
498
568
 
499
569
  # give spotify zeroconf api time to process the change.
500
570
  if delay > 0:
@@ -621,7 +691,7 @@ class ZeroconfConnect:
621
691
  params=reqParams
622
692
  )
623
693
 
624
- # check response for errors, and return json response.
694
+ # check response for initial errors, and return json response.
625
695
  responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
626
696
 
627
697
  # process results.
@@ -629,6 +699,12 @@ class ZeroconfConnect:
629
699
 
630
700
  # trace.
631
701
  _logsi.LogObject(SILevel.Verbose, '%s result (%s) - "%s" (%s)' % (apiMethodName, type(result).__name__, result.RemoteName, result.DeviceId), result, excludeNonPublic=True)
702
+
703
+ # if result status is not ok, then raise an exception.
704
+ if (result.Status != 101):
705
+ raise SpotifyZeroconfApiError(result.Status, result.ToString(), apiMethodName, result.StatusString, _logsi)
706
+
707
+ # return result to caller.
632
708
  return result
633
709
 
634
710
  except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
@@ -286,6 +286,14 @@ class ZeroconfGetInfo(ZeroconfResponse):
286
286
  """
287
287
  return self._PublicKey
288
288
 
289
+ @PublicKey.setter
290
+ def PublicKey(self, value:str):
291
+ """
292
+ Sets the PublicKey property value.
293
+ """
294
+ if isinstance(value, str):
295
+ self._PublicKey = value
296
+
289
297
 
290
298
  @property
291
299
  def RemoteName(self) -> str:
@@ -343,7 +351,7 @@ class ZeroconfGetInfo(ZeroconfResponse):
343
351
  @property
344
352
  def TokenType(self) -> str:
345
353
  """
346
- Token type provided by the client (e.g. "accesstoken").
354
+ Token type provided by the client (e.g. "accesstoken", "authorization_code", etc).
347
355
  """
348
356
  return self._TokenType
349
357
 
@@ -29,11 +29,14 @@ class ZeroconfResponse:
29
29
 
30
30
  else:
31
31
 
32
+ # some devices return the `status` and `spotifyError` values as string,
33
+ # while others return them as numeric; use the property setters to
34
+ # convert them to numeric if they are strings that contain a numeric value.
32
35
  self._ResponseSource = root.get('responseSource', None)
33
- self._SpotifyError = root.get('spotifyError', None)
34
- self._Status = root.get('status', None)
36
+ self.SpotifyError = root.get('spotifyError', None)
37
+ self.Status = root.get('status', None)
35
38
  self._StatusString = root.get('statusString', None)
36
-
39
+
37
40
 
38
41
  def __repr__(self) -> str:
39
42
  return self.ToString()
@@ -65,6 +68,9 @@ class ZeroconfResponse:
65
68
  """
66
69
  if isinstance(value, int):
67
70
  self._SpotifyError = value
71
+ elif isinstance(value, str):
72
+ if value.isnumeric():
73
+ self._SpotifyError = int(value)
68
74
 
69
75
 
70
76
  @property
@@ -81,6 +87,9 @@ class ZeroconfResponse:
81
87
  """
82
88
  if isinstance(value, int):
83
89
  self._Status = value
90
+ elif isinstance(value, str):
91
+ if value.isnumeric():
92
+ self._Status = int(value)
84
93
 
85
94
 
86
95
  @property