spotifywebapiPython 1.0.149__py3-none-any.whl → 1.0.151__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 (27) hide show
  1. {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/METADATA +2 -1
  2. {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/RECORD +27 -27
  3. spotifywebapipython/const.py +6 -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 +20 -3
  8. spotifywebapipython/docs/spotifywebapipython/saappmessages.html +2 -2
  9. spotifywebapipython/docs/spotifywebapipython/sautils.html +65 -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 +42 -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/docs/spotifywebapipython/spotifywebplayertoken.html +2 -2
  18. spotifywebapipython/models/spotifyconnectdevice.py +13 -1
  19. spotifywebapipython/models/zeroconfdiscoveryresult.py +1 -1
  20. spotifywebapipython/sautils.py +50 -0
  21. spotifywebapipython/spotifyclient.py +494 -169
  22. spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +107 -19
  23. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +1 -1
  24. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +1 -0
  25. {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/LICENSE +0 -0
  26. {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/WHEEL +0 -0
  27. {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/top_level.txt +0 -0
@@ -6,6 +6,7 @@ from datetime import datetime
6
6
  import hashlib
7
7
  from pychromecast import CastBrowser, CastInfo, Chromecast, get_chromecast_from_cast_info
8
8
  import socket
9
+ from soco import SoCo
9
10
  import threading
10
11
  import time
11
12
  from uuid import UUID
@@ -101,6 +102,7 @@ class SpotifyConnectDirectoryTask(threading.Thread):
101
102
  self._CastBrowser:CastBrowser = None
102
103
  self._InitialDiscoveryTimeout = initialDiscoveryTimeout
103
104
  self._IsStopRequested:bool = False
105
+ self._SonosPlayers:dict = {}
104
106
  self._SpotifyClientInstance = spotifyClientInstance
105
107
  self._SpotifyConnectBrowser:ServiceBrowser = None
106
108
  self._SpotifyConnectDevices:SpotifyConnectDevices = SpotifyConnectDevices()
@@ -150,8 +152,8 @@ class SpotifyConnectDirectoryTask(threading.Thread):
150
152
 
151
153
  # clear event notifiers.
152
154
  self.WaitForInitComplete.clear()
153
- self.WaitForActivationComplete.clear()
154
- self.WaitForTransferComplete.clear()
155
+ self.WaitForActivationComplete.set() # indicate activation is complete (initial state).
156
+ self.WaitForTransferComplete.set() # indicate transfer is complete (initial state).
155
157
 
156
158
 
157
159
  @property
@@ -278,7 +280,7 @@ class SpotifyConnectDirectoryTask(threading.Thread):
278
280
  # trace - dump devices discovered initially.
279
281
  if (_logsi.IsOn(SILevel.Verbose)):
280
282
 
281
- # use lock, as zeroconf tasks could still be updating the device list.
283
+ # use lock, as zeroconf tasks could be updating the device list.
282
284
  with self._Zeroconf_Lock:
283
285
 
284
286
  # trace.
@@ -457,6 +459,16 @@ class SpotifyConnectDirectoryTask(threading.Thread):
457
459
  if (len(self._CastBrowser.devices) == 0):
458
460
  raise SpotifyApiError("No available Chromecast devices detected", logsi=_logsi)
459
461
 
462
+ # are we waiting on a previous request to finish?
463
+ if (not self.WaitForActivationComplete.is_set):
464
+ raise SpotifyApiError("Waiting for previous Chromecast device activation to complete; try again in a few seconds", logsi=_logsi)
465
+ if (not self.WaitForTransferComplete.is_set):
466
+ raise SpotifyApiError("Waiting for previous Chromecast device playback transfer to complete; try again in a few seconds", logsi=_logsi)
467
+
468
+ # clear event notifiers.
469
+ self.WaitForActivationComplete.clear()
470
+ self.WaitForTransferComplete.clear()
471
+
460
472
  # if Spotify Cast App is active on a cast device then request that it stop.
461
473
  castAppTask:SpotifyConnectZeroconfCastAppTask = self._CastAppTasks.get(scDevice.DiscoveryResult.Key, None)
462
474
  if (castAppTask is not None):
@@ -471,6 +483,12 @@ class SpotifyConnectDirectoryTask(threading.Thread):
471
483
  _logsi.LogVerbose("%s - Getting Chromecast information for device: \"%s\"" % (self.name, deviceName))
472
484
  castInfo:CastInfo = self._CastBrowser.devices[UUID(scDevice.DiscoveryResult.Key)]
473
485
 
486
+ # syncronize access via lock, as we are accessing the collection.
487
+ with self._SpotifyConnectDevices_RLock:
488
+
489
+ # reset device zeroconf response, as we are re-using an existing object.
490
+ scDevice.ZeroconfResponseInfo = ZeroconfResponse()
491
+
474
492
  # connect to the device and build a Chromecast instance.
475
493
  castDevice:Chromecast = get_chromecast_from_cast_info(
476
494
  cast_info=castInfo,
@@ -503,20 +521,27 @@ class SpotifyConnectDirectoryTask(threading.Thread):
503
521
  self._CastAppTasks[str(castInfo.uuid)] = castAppTask
504
522
 
505
523
  # wait for the launched spotify app to activate the user (or fail).
506
- # this occurs when we receive an `addUserResponse` or `addUserError` zeroconf response message.
524
+ # this occurs when we receive any of the following zeroconf response
525
+ # messages: `addUserResponse`, `addUserError`, `getInfoError`.
507
526
 
508
527
  counter = 0
509
528
  while counter < (timeoutActivation + 1):
510
529
  if (self.WaitForActivationComplete.wait(1)):
511
- scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
530
+ # syncronize access via lock, as we are accessing the collection.
531
+ with self._SpotifyConnectDevices_RLock:
532
+ scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
512
533
  if (scDevice is not None):
513
534
  response = scDevice.ZeroconfResponseInfo
514
535
  if (response.ResponseSource == TYPE_ADD_USER_RESPONSE):
536
+ # indicate device was reconnected.
537
+ scDevice.WasReConnected = True
515
538
  break
516
539
  raise SpotifyApiError("Spotify Cast App could not be activated on Chromecast device \"%s\": %s" % (deviceName, response.ToString(False)), logsi=_logsi)
517
540
  raise SpotifyApiError("Spotify Cast App could not activated on Chromecast device: unknown error", logsi=_logsi)
518
541
  if (counter >= timeoutActivation):
519
- scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
542
+ # syncronize access via lock, as we are accessing the collection.
543
+ with self._SpotifyConnectDevices_RLock:
544
+ scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
520
545
  if (scDevice is not None):
521
546
  raise SpotifyApiError("Spotify Cast App activation timeout (%s seconds) was exceeded while trying to activate Chromecast device \"%s\"" % (timeoutActivation, deviceName), logsi=_logsi)
522
547
  counter += 1
@@ -527,13 +552,18 @@ class SpotifyConnectDirectoryTask(threading.Thread):
527
552
  # was transfer playback specified?
528
553
  if (transferPlayback == True):
529
554
 
530
- # wait for the launched spotify app to activate the user (or fail).
531
- # this occurs when we receive an `addUserResponse` or `addUserError` zeroconf response message.
555
+ # note that we only wait if we are transferring playback.
556
+
557
+ # wait for the launched spotify app to transfer playback (or fail).
558
+ # this occurs when we receive any of the following zeroconf response
559
+ # messages: `transferSuccess`, `transferError`.
532
560
 
533
561
  counter = 0
534
562
  while counter < (timeoutTransfer + 1):
535
563
  if (self.WaitForTransferComplete.wait(1)):
536
- scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
564
+ # syncronize access via lock, as we are accessing the collection.
565
+ with self._SpotifyConnectDevices_RLock:
566
+ scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
537
567
  if (scDevice is not None):
538
568
  response = scDevice.ZeroconfResponseInfo
539
569
  if (response.ResponseSource == TYPE_TRANSFER_SUCCESS_RESPONSE):
@@ -541,7 +571,9 @@ class SpotifyConnectDirectoryTask(threading.Thread):
541
571
  raise SpotifyApiError("Spotify Cast App failed to receive playback transfer on Chromecast device \"%s\": %s" % (deviceName, response.ToString(False)), logsi=_logsi)
542
572
  raise SpotifyApiError("Spotify Cast App failed to receive playback transfer on Chromecast device: unknown error", logsi=_logsi)
543
573
  if (counter >= timeoutTransfer):
544
- scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
574
+ # syncronize access via lock, as we are accessing the collection.
575
+ with self._SpotifyConnectDevices_RLock:
576
+ scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
545
577
  if (scDevice is not None):
546
578
  raise SpotifyApiError("Spotify Cast App transfer playback timeout (%s seconds) was exceeded while waiting for transfer of playback on Chromecast device \"%s\"" % (timeoutTransfer, deviceName), logsi=_logsi)
547
579
  counter += 1
@@ -557,6 +589,11 @@ class SpotifyConnectDirectoryTask(threading.Thread):
557
589
 
558
590
  finally:
559
591
 
592
+ # reset activation and transfer wait events to initial state for next time;
593
+ # just in case they were not set in the wait logic above (e.g. error condition).
594
+ self.WaitForActivationComplete.set()
595
+ self.WaitForTransferComplete.set()
596
+
560
597
  # trace.
561
598
  _logsi.LeaveMethod(SILevel.Debug)
562
599
 
@@ -922,6 +959,50 @@ class SpotifyConnectDirectoryTask(threading.Thread):
922
959
  return result
923
960
 
924
961
 
962
+ def GetSonosPlayer(
963
+ self,
964
+ device:SpotifyConnectDevice,
965
+ ) -> SoCo:
966
+ """
967
+ Returns the Sonos Controller instance for the specified Spotify Connect device.
968
+ Returns the Sonos player instance for the specified Spotify Connect Sonos device.
969
+
970
+ Args:
971
+ device (SpotifyConnectDevice):
972
+ Spotify Connect device instance used to identify the Sonos player.
973
+
974
+ Returns:
975
+ The Sonos Controller instance for the specified Spotify Connect device.
976
+
977
+ Raises:
978
+ SpotifyApiError:
979
+ If a Sonos Controller instance could not be found for the device id.
980
+
981
+ You should invoke "if (scDevice.IsSonos):" logic prior to calling this method, as
982
+ a Sonos Controller instance will not be created for non-Sonos devices and an exception
983
+ will be raised!
984
+ """
985
+ apiMethodName:str = "GetSonosPlayer"
986
+
987
+ # syncronize access via lock, as we are accessing the collection.
988
+ with self._SpotifyConnectDevices_RLock:
989
+
990
+ result:SpotifyConnectDevice = None
991
+
992
+ # validations.
993
+ if (device is None):
994
+ raise SpotifyApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % (apiMethodName, 'device'), logsi=_logsi)
995
+
996
+ # get Sonos Controller instance for device name.
997
+ sonosPlayer:SoCo = self._SonosPlayers.get(device.DiscoveryResult.HostIpAddress, None)
998
+
999
+ # if not found then it's an error.
1000
+ if (sonosPlayer is None):
1001
+ raise SpotifyApiError("Could not find Sonos Controller instance for device: %s" % (device.Title), None, logsi=_logsi)
1002
+
1003
+ return sonosPlayer
1004
+
1005
+
925
1006
  def GetSpotifyDeviceIDFromName(
926
1007
  self,
927
1008
  name:str
@@ -1416,14 +1497,6 @@ class SpotifyConnectDirectoryTask(threading.Thread):
1416
1497
  This method will be called in a thread-safe manner, as the caller is using
1417
1498
  the `_Zeroconf_Lock` object to control access.
1418
1499
  """
1419
-
1420
- # TODO - research this some more!
1421
- # saw the following logged to the console about 4 minutes after I unplugged the speaker!
1422
- # it also DID NOT show up in the SI trace!
1423
- # "[Nest Audio 01(192.168.1.95):8009] Failed to connect. No retries"
1424
- # ??? we don't want to remove the device, since it's just disconnecting and not necessarily
1425
- # removing itself from the network.
1426
-
1427
1500
  # syncronize access via lock, as we are accessing the collection.
1428
1501
  with self._SpotifyConnectDevices_RLock:
1429
1502
 
@@ -1548,7 +1621,7 @@ class SpotifyConnectDirectoryTask(threading.Thread):
1548
1621
  A `ZeroconfResponse` instance that contains the zeroconf response data.
1549
1622
 
1550
1623
  This method allows us to update the SpotifyConnectDevice entry with the Spotify Connect
1551
- Zeroconf response information.
1624
+ Zeroconf response information.
1552
1625
  """
1553
1626
  # syncronize access via lock, as we are accessing the collection.
1554
1627
  with self._SpotifyConnectDevices_RLock:
@@ -1711,6 +1784,21 @@ class SpotifyConnectDirectoryTask(threading.Thread):
1711
1784
  if (len(self._SpotifyConnectDevices.Items) > 0):
1712
1785
  self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
1713
1786
 
1787
+ try:
1788
+
1789
+ # if new device is a Sonos, then create a Sonos Controller instance for the device.
1790
+ # use device ip address as the key, as Spotify Web API reports id=null for restricted devices in playerstate!
1791
+ if (scDevice.IsSonos):
1792
+ _logsi.LogVerbose("Sonos device detected; creating Sonos Controller instance for device: %s (ip=%s)" % (scDevice.Title, zeroconfDiscoveryResult.HostIpAddress))
1793
+ self._SonosPlayers[zeroconfDiscoveryResult.HostIpAddress] = SoCo(zeroconfDiscoveryResult.HostIpAddress)
1794
+
1795
+ except:
1796
+
1797
+ # trace.
1798
+ _logsi.LogException("%s - Could not create Sonos Control API reference: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
1799
+
1800
+ # ignore exceptions, as there is nothing we can do at this point.
1801
+
1714
1802
  # raise event.
1715
1803
  self._RaiseDeviceAdded(scDevice)
1716
1804
 
@@ -284,7 +284,7 @@ class SpotifyConnectZeroconfCastAppTask(threading.Thread):
284
284
  while counter < (timeout + 1):
285
285
  if (self._SpotifyConnectZeroconfCastController.waitPlaybackTransfer.wait(1)):
286
286
  if (self._SpotifyConnectZeroconfCastController.isPlaybackTransferError):
287
- raise SpotifyConnectZeroconfPlaybackTransferError("%s - playback transfer error TODO" % (self.name))
287
+ raise SpotifyConnectZeroconfPlaybackTransferError("%s - playback transfer error: %s" % (self.name, str(self.ZeroconfResponseObject)))
288
288
  break
289
289
  if (counter >= timeout):
290
290
  raise SpotifyConnectZeroconfPlaybackTransferError("%s - Timed out waiting for playback transfer to device" % (self.name))
@@ -94,6 +94,7 @@ class SpotifyConnectZeroconfCastListener(AbstractCastListener):
94
94
  _logsi.LogObject(SILevel.Debug, "Chromecast Zeroconf service details: \"%s\" (%s) (CastInfo object)" % (castInfo.friendly_name, serviceName), castInfo)
95
95
 
96
96
  # TODO we may need to expand this check based on cast_type value ...
97
+
97
98
  # Regular chromecast, supports video/audio
98
99
  #CAST_TYPE_CHROMECAST = "cast"
99
100
  # Cast Audio device, supports only audio