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.
- {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/METADATA +2 -1
- {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/RECORD +27 -27
- spotifywebapipython/const.py +6 -1
- spotifywebapipython/docs/__init__.html +15 -2
- spotifywebapipython/docs/index.html +2 -2
- spotifywebapipython/docs/search.js +1 -1
- spotifywebapipython/docs/spotifywebapipython/const.html +20 -3
- spotifywebapipython/docs/spotifywebapipython/saappmessages.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/sautils.html +65 -2
- spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +42 -2
- spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +2 -2
- spotifywebapipython/models/spotifyconnectdevice.py +13 -1
- spotifywebapipython/models/zeroconfdiscoveryresult.py +1 -1
- spotifywebapipython/sautils.py +50 -0
- spotifywebapipython/spotifyclient.py +494 -169
- spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +107 -19
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +1 -1
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +1 -0
- {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/WHEEL +0 -0
- {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.
|
|
154
|
-
self.WaitForTransferComplete.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
531
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
{spotifywebapiPython-1.0.149.dist-info → spotifywebapiPython-1.0.151.dist-info}/top_level.txt
RENAMED
|
File without changes
|