spotifywebapiPython 1.0.68__py3-none-any.whl → 1.0.70__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.68.dist-info → spotifywebapiPython-1.0.70.dist-info}/METADATA +1 -1
  2. {spotifywebapiPython-1.0.68.dist-info → spotifywebapiPython-1.0.70.dist-info}/RECORD +24 -24
  3. spotifywebapipython/const.py +3 -3
  4. spotifywebapipython/docs/__init__.html +16 -2
  5. spotifywebapipython/docs/index.html +2 -2
  6. spotifywebapipython/docs/search.js +1 -1
  7. spotifywebapipython/docs/spotifywebapiPython/const.html +5 -5
  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 +20 -14
  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/userprofile.py +2 -2
  18. spotifywebapipython/models/zeroconfdiscoveryresult.py +3 -3
  19. spotifywebapipython/spotifyclient.py +13 -7
  20. spotifywebapipython/zeroconfapi/blobbuilder.py +3 -0
  21. spotifywebapipython/zeroconfapi/zeroconfconnect.py +124 -77
  22. {spotifywebapiPython-1.0.68.dist-info → spotifywebapiPython-1.0.70.dist-info}/LICENSE +0 -0
  23. {spotifywebapiPython-1.0.68.dist-info → spotifywebapiPython-1.0.70.dist-info}/WHEEL +0 -0
  24. {spotifywebapiPython-1.0.68.dist-info → spotifywebapiPython-1.0.70.dist-info}/top_level.txt +0 -0
@@ -132,9 +132,9 @@ class ZeroconfConnect:
132
132
  @property
133
133
  def Version(self) -> str:
134
134
  """
135
- Spotify Connect Zeroconf API version number that the device supports (e.g. "2.10.0").
135
+ Spotify Connect Zeroconf API version number that the device supports (e.g. "", "1.0", "2.10.0", etc.).
136
136
  """
137
- return self._Version
137
+ return self._Version or ''
138
138
 
139
139
 
140
140
  def _CheckResponseForErrors(self, response:Response, methodName:str, endpoint:str) -> None:
@@ -190,7 +190,6 @@ class ZeroconfConnect:
190
190
  _logsi.LogArray(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json array)" % (response.status_code, response.reason, endpoint), responseData)
191
191
  else:
192
192
  _logsi.LogObject(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json object)" % (response.status_code, response.reason, endpoint), responseData)
193
- pass
194
193
 
195
194
  else:
196
195
 
@@ -213,8 +212,6 @@ class ZeroconfConnect:
213
212
  # just raise a new exception with the non-JSON response data.
214
213
  raise SpotifyZeroconfApiError(response.status_code, responseUTF8, methodName, response.reason, _logsi)
215
214
 
216
- errMessage:str = None
217
-
218
215
  # at this point we know that the response contains JSON data, as
219
216
  # we would have raised an exception before this if we could not
220
217
  # convert the response to JSON!
@@ -239,20 +236,6 @@ class ZeroconfConnect:
239
236
  # InvalidArguments 303 400 ERROR-INVALID-ARGUMENTS All Incorrect or insufficient arguments supplied for requested action
240
237
  # SpotifyError 402 200 ERROR-SPOTIFY-ERROR All A Spotify API call returned an error not covered by other error messages
241
238
 
242
- # # process results.
243
- # result = ZeroconfResponse(root=responseData)
244
-
245
- # # if result is ok, then just return the response data.
246
- # if (result.Status == 101) \
247
- # or (result.StatusString == 'OK') \
248
- # or (result.StatusString == 'ERROR-OK'):
249
- # return responseData
250
-
251
- # # was an error status string returned?
252
- # # if so, then we will raise an exception.
253
- # if (result.StatusString.startswith('ERROR-')):
254
- # raise SpotifyZeroconfApiError(result.Status, result.ToString(), methodName, result.StatusString, _logsi)
255
-
256
239
  # let the calling function process the returned status.
257
240
  return responseData
258
241
 
@@ -352,10 +335,11 @@ class ZeroconfConnect:
352
335
  # get the current device id from the device via Spotify ZeroConf API `getInfo` endpoint.
353
336
  info:ZeroconfGetInfo = self.GetInformation()
354
337
 
355
- # if the public key value is "INVALID" then issue a disconnect to reset the user context.
356
- if (info.PublicKey == 'SU5WQUxJRA=='): # base64 encoded "INVALID"
357
- _logsi.LogVerbose("Spotify Connect publicKey value is 'INVALID' for Device id '%s'; calling Disconnect to reset the user context" % (info.DeviceId))
358
- self.Disconnect(ignoreStatusResult=True)
338
+ # are we including origin device information (e.g. deviceName, deviceId)?
339
+ # TODO - maybe tailor this to info.BrandDisplayName and info.ModelDisplayName instead?
340
+ includeOriginDeviceInfo:bool = False
341
+ if (info.PublicKey == 'SU5WQUxJRA==') and (info.Availability == 'NOT-LOADED'):
342
+ includeOriginDeviceInfo = True
359
343
 
360
344
  # execute the Spotify Zeroconf API addUser request.
361
345
  responseData:dict = self._ConnectAddUser(
@@ -363,41 +347,65 @@ class ZeroconfConnect:
363
347
  username,
364
348
  password,
365
349
  loginId,
366
- apiMethodName)
350
+ apiMethodName,
351
+ includeOriginDeviceInfo=includeOriginDeviceInfo
352
+ )
367
353
 
368
354
  # process results.
369
355
  # we will process the results with a `ZeroconfGetInfo` object, in case a publicKey was returned.
370
356
  result = ZeroconfGetInfo(root=responseData)
371
357
 
372
- # does result contain a public key?
373
- # some spotify connect devices need to be "woken up" with the initial `addUser` request.
374
- # when this happens, the initial request will return a 203 status (ERROR-INVALID-PUBLICKEY), and
375
- # return a valid public key in the response. we can then take this public key and try the
376
- # `addUser` request again.
358
+ # is the device fully available? The info.Availability property can be used to determine this.
359
+ # if the device is fully available (Availability=""), then the addUser request should return
360
+ # immediately - hopefully with a 0 status (OK) to denote the request was successul.
361
+ # if the device is not fully available (Availability="NOT-LOADED"), then the addUser request will
362
+ # probably return a 203 status (ERROR-INVALID-PUBLICKEY) along with a new PublicKey value.
363
+ # when this happens, we need to wait for the device to become fully available before we retry the
364
+ # addUser request again with the new public key.
365
+
366
+ # did we get a new PublicKey value?
377
367
  if (result.Status == 203) and (result.StatusString == "ERROR-INVALID-PUBLICKEY"):
378
368
 
379
369
  # trace.
380
370
  _logsi.LogObject(SILevel.Verbose, '%s result (%s)' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
381
371
  _logsi.LogVerbose("Spotify Connect addUser request returned an ERROR-INVALID-PUBLICKEY response for Device id '%s'; addUser will be retried with the returned publicKey" % (info.DeviceId))
382
372
 
383
- # give the device a little time to wake up.
384
- WAKEUP_DELAY:float = 0.25
385
- _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % WAKEUP_DELAY)
386
- time.sleep(WAKEUP_DELAY)
373
+ # wait for the device to be fully loaded.
374
+ # once this happens, the device Availability state will change from "NOT-LOADED" to "".
375
+ loopTotalDelay:float = 0
376
+ LOOP_DELAY:float = 0.25
377
+ LOOP_TIMEOUT:float = 5.0
378
+ while True:
379
+
380
+ # wait just a bit between device info queries.
381
+ _logsi.LogVerbose("Delaying for %s seconds to allow Spotify Connect device id '%s' to become available (currently '%s')" % (LOOP_DELAY, info.DeviceId, info.Availability))
382
+ time.sleep(LOOP_DELAY)
383
+ loopTotalDelay = loopTotalDelay + LOOP_DELAY
384
+
385
+ # get device information; if availability status changes then we are done.
386
+ info:ZeroconfGetInfo = self.GetInformation()
387
+ if info.Availability != "NOT-LOADED":
388
+ _logsi.LogVerbose("Spotify Connect Device id '%s' availability status changed from '%s' to '%s' within %f seconds of initial addUser request" % (info.DeviceId, info.Availability, info.Availability, loopTotalDelay))
389
+ break
390
+
391
+ # only check so many times before we give up;
392
+ if (loopTotalDelay > LOOP_TIMEOUT):
393
+ _logsi.LogWarning("Timed out waiting for Spotify Connect device id '%s' availability to change from '%s'; gave up after %f seconds from initial addUser request" % (info.DeviceId, info.Availability, loopTotalDelay))
394
+ break
387
395
 
388
- # move the newly returned public key to the getInfo results.
389
- # try the addUser request again, this time with a (hopefully) valid public key.
390
- info.PublicKey = result.PublicKey
396
+ # now that the device is (hopefully) fully available, try the addUser request again.
391
397
  self._ConnectAddUser(
392
398
  info,
393
399
  username,
394
400
  password,
395
401
  loginId,
396
- apiMethodName)
402
+ apiMethodName,
403
+ includeOriginDeviceInfo=includeOriginDeviceInfo
404
+ )
397
405
 
398
406
  # process results.
399
407
  result = ZeroconfResponse(root=responseData)
400
-
408
+
401
409
  # trace.
402
410
  _logsi.LogObject(SILevel.Verbose, '%s result (%s)' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
403
411
 
@@ -437,48 +445,87 @@ class ZeroconfConnect:
437
445
  password:str,
438
446
  loginId:str,
439
447
  apiMethodName:str,
448
+ includeOriginDeviceInfo:bool=False,
440
449
  ) -> dict:
441
450
  """
451
+ Execute the `addUser` request.
442
452
  """
443
- # formulate the blob.
444
- credentials:Credentials = Credentials(username, password)
445
- builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
446
- blob = builder.build()
453
+ apiMethodName:str = '_ConnectAddUser'
454
+ apiMethodParms:SIMethodParmListContext = None
447
455
 
448
- # set request endpoint.
449
- endpoint:str = self.GetEndpoint('addUser')
456
+ try:
450
457
 
451
- # set request headers.
452
- reqHeaders:dict = {
453
- 'Content-Type': 'application/x-www-form-urlencoded',
454
- 'Connection': 'close'
455
- }
456
- _logsi.LogDictionary(SILevel.Debug, "ZeroconfConnect http request: '%s' (headers)" % (endpoint), reqHeaders)
458
+ # trace.
459
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
460
+ apiMethodParms.AppendKeyValue("includeOriginDeviceInfo", includeOriginDeviceInfo)
461
+ _logsi.LogMethodParmList(SILevel.Verbose, "Issuing Spotify Connect Zeroconf addUser request (ip=%s)" % self._HostIpAddress, apiMethodParms)
462
+
463
+ # validations.
464
+ if (includeOriginDeviceInfo is None):
465
+ includeOriginDeviceInfo = False
457
466
 
458
- # set request parameters.
459
- reqData={
460
- 'action': 'addUser',
461
- 'version': self._Version or '',
462
- 'userName': credentials.username.decode('ascii'), # user name (e.g. "youremail@mail.com")
463
- 'clientKey': int_to_b64str(builder.dh_keys.public_key),
464
- 'blob': blob,
465
- 'tokenType': 'default', # NOTE - not the same as info.TokenType!
466
- # not sure if this is required or not, so commenting out for now.
467
- #'loginId': loginId or '', # canonical login id (e.g. "31l77fd87g8h9j00k89f07jf87ge")
468
- }
469
- _logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (data)" % (endpoint), reqData)
470
-
471
- # execute spotify zeroconf api request.
472
- response = requests.post(
473
- self._Uri,
474
- timeout=10,
475
- headers=reqHeaders,
476
- data=reqData # send data in POST request body
477
- )
467
+ # formulate the blob.
468
+ credentials:Credentials = Credentials(username, password)
469
+ builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
470
+ blob = builder.build()
478
471
 
479
- # check response for initial errors, and return json response.
480
- responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
481
- return responseData
472
+ # set request endpoint.
473
+ endpoint:str = self.GetEndpoint('addUser')
474
+
475
+ # set request headers.
476
+ reqHeaders:dict = {
477
+ 'Content-Type': 'application/x-www-form-urlencoded',
478
+ 'Connection': 'close'
479
+ }
480
+ _logsi.LogDictionary(SILevel.Debug, "ZeroconfConnect http request: '%s' (headers)" % (endpoint), reqHeaders)
481
+
482
+ # set request parameters.
483
+ reqData={
484
+ 'action': 'addUser',
485
+ 'version': self.Version,
486
+ 'tokenType': 'default', # NOTE - not the same as info.TokenType!
487
+ 'loginId': loginId or '', # canonical login id (e.g. "31l77fd87g8h9j00k89f07jf87ge")
488
+ 'userName': credentials.username.decode('ascii'), # user name (e.g. "youremail@mail.com")
489
+ 'clientKey': int_to_b64str(builder.dh_keys.public_key),
490
+ 'blob': blob,
491
+ }
492
+
493
+ # are we including origin device information (e.g. deviceName, deviceId)?
494
+ # the `deviceName` and `deviceId` keys are specified by some manufacturers for the
495
+ # `addUser` action. I am not sure if they are required or not, as they are not part
496
+ # of the Spotify Zeroconf API specification for the `addUser` action.
497
+ if (includeOriginDeviceInfo):
498
+ _logsi.LogVerbose("Origin deviceName and deviceId will be included with the addUser request for deviceId '%s'" % (info.DeviceId))
499
+ reqData['deviceName'] = builder._OriginDeviceName
500
+ reqData['deviceId'] = builder._OriginDeviceId
501
+
502
+ # if PublicKey="INVALID" and availability="NOT-LOADED" then adjust the addUser
503
+ # request to remove the blob and clientKey values. this will inform the device to
504
+ # send us back a PublicKey value, which can be used on a subsequent addUser request.
505
+ if (info.PublicKey == 'SU5WQUxJRA==') and (info.Availability == 'NOT-LOADED'):
506
+ _logsi.LogVerbose("Spotify Connect PublicKey='INVALID' and Availability='NOT-LOADED' detected for Device id '%s'; removing blob and clientKey values" % (info.DeviceId))
507
+ reqData['blob'] = ''
508
+ reqData['clientKey'] = ''
509
+
510
+ # trace.
511
+ _logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (data)" % (endpoint), reqData)
512
+
513
+ # execute spotify zeroconf api request.
514
+ response = requests.post(
515
+ self._Uri,
516
+ timeout=10,
517
+ headers=reqHeaders,
518
+ data=reqData # send data in POST request body
519
+ )
520
+
521
+ # check response for initial errors, and return json response.
522
+ responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
523
+ return responseData
524
+
525
+ finally:
526
+
527
+ # trace.
528
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
482
529
 
483
530
 
484
531
  def Disconnect(
@@ -554,7 +601,7 @@ class ZeroconfConnect:
554
601
  # set request parameters.
555
602
  reqData={
556
603
  'action': 'resetUsers',
557
- 'version': self._Version or '',
604
+ 'version': self.Version,
558
605
  }
559
606
  _logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (data)" % (endpoint), reqData)
560
607
 
@@ -641,7 +688,7 @@ class ZeroconfConnect:
641
688
  return "{uri}?action={action}&version={version}".format(
642
689
  uri=self._Uri,
643
690
  action=action,
644
- version=self._Version or '',
691
+ version=self.Version,
645
692
  )
646
693
 
647
694
 
@@ -693,7 +740,7 @@ class ZeroconfConnect:
693
740
  # set request parameters.
694
741
  reqParams={
695
742
  'action': 'getInfo',
696
- 'version': self._Version or '',
743
+ 'version': self.Version,
697
744
  }
698
745
  _logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (params)" % (endpoint), reqParams)
699
746
 
@@ -752,5 +799,5 @@ class ZeroconfConnect:
752
799
 
753
800
  msg = '%s\n URI="%s"' % (msg, str(self._Uri))
754
801
  if self._CPath is not None: msg = '%s\n CPath="%s"' % (msg, str(self._CPath))
755
- if self._Version is not None: msg = '%s\n Version="%s"' % (msg, str(self._Version))
802
+ if self._Version is not None: msg = '%s\n Version="%s"' % (msg, str(self.Version))
756
803
  return msg