spotifywebapiPython 1.0.131__py3-none-any.whl → 1.0.133__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.131.dist-info → spotifywebapiPython-1.0.133.dist-info}/METADATA +1 -1
- {spotifywebapiPython-1.0.131.dist-info → spotifywebapiPython-1.0.133.dist-info}/RECORD +24 -24
- spotifywebapipython/const.py +1 -1
- spotifywebapipython/docs/__init__.html +16 -2
- spotifywebapipython/docs/index.html +2 -2
- spotifywebapipython/docs/search.js +1 -1
- spotifywebapipython/docs/spotifywebapipython/const.html +3 -3
- spotifywebapipython/docs/spotifywebapipython/saappmessages.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/sautils.html +2 -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 +2 -77
- spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +40 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +2 -2
- spotifywebapipython/models/spotifyconnectdevice.py +12 -0
- spotifywebapipython/models/zeroconfdiscoveryresult.py +41 -1
- spotifywebapipython/spotifyclient.py +58 -116
- spotifywebapipython/spotifydiscovery.py +207 -45
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +1 -1
- {spotifywebapiPython-1.0.131.dist-info → spotifywebapiPython-1.0.133.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.131.dist-info → spotifywebapiPython-1.0.133.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.131.dist-info → spotifywebapiPython-1.0.133.dist-info}/top_level.txt +0 -0
|
@@ -22,6 +22,8 @@ class ZeroconfDiscoveryResult:
|
|
|
22
22
|
self._HostIpAddresses:list = []
|
|
23
23
|
self._HostIpPort:int = None
|
|
24
24
|
self._HostTTL:int = None
|
|
25
|
+
self._IsChromeCast:bool = False
|
|
26
|
+
self._Id:str = None
|
|
25
27
|
self._Key:str = None
|
|
26
28
|
self._Name:str = None
|
|
27
29
|
self._OtherTTL:int = None
|
|
@@ -162,6 +164,24 @@ class ZeroconfDiscoveryResult:
|
|
|
162
164
|
self._HostTTL = value
|
|
163
165
|
|
|
164
166
|
|
|
167
|
+
@property
|
|
168
|
+
def Id(self) -> str:
|
|
169
|
+
"""
|
|
170
|
+
Result ID (e.g. "Bose-ST10-1" (192.168.1.81:8200)).
|
|
171
|
+
|
|
172
|
+
This is a helper property, and not part of the Zeroconf interface.
|
|
173
|
+
"""
|
|
174
|
+
return self._Id
|
|
175
|
+
|
|
176
|
+
@Id.setter
|
|
177
|
+
def Id(self, value:str):
|
|
178
|
+
"""
|
|
179
|
+
Sets the Id property value.
|
|
180
|
+
"""
|
|
181
|
+
if isinstance(value, str):
|
|
182
|
+
self._Id = value
|
|
183
|
+
|
|
184
|
+
|
|
165
185
|
@property
|
|
166
186
|
def IsDynamicDevice(self) -> bool:
|
|
167
187
|
"""
|
|
@@ -172,7 +192,23 @@ class ZeroconfDiscoveryResult:
|
|
|
172
192
|
process, but still exist in the player device list. These are usually Spotify Connect
|
|
173
193
|
web or mobile players with temporary device id's.
|
|
174
194
|
"""
|
|
175
|
-
return (self._HostIpPort == 0)
|
|
195
|
+
return (self._HostIpPort == 0) and (self._ServiceType is None)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def IsChromeCast(self) -> bool:
|
|
200
|
+
"""
|
|
201
|
+
True if the device is a Google ChromeCast device; otherwise, False.
|
|
202
|
+
"""
|
|
203
|
+
return self._IsChromeCast
|
|
204
|
+
|
|
205
|
+
@IsChromeCast.setter
|
|
206
|
+
def IsChromeCast(self, value:bool):
|
|
207
|
+
"""
|
|
208
|
+
Sets the IsChromeCast property value.
|
|
209
|
+
"""
|
|
210
|
+
if isinstance(value, bool):
|
|
211
|
+
self._IsChromeCast = value
|
|
176
212
|
|
|
177
213
|
|
|
178
214
|
@property
|
|
@@ -429,6 +465,7 @@ class ZeroconfDiscoveryResult:
|
|
|
429
465
|
"""
|
|
430
466
|
result:dict = \
|
|
431
467
|
{
|
|
468
|
+
'Id': self._Id,
|
|
432
469
|
'DeviceName': self._DeviceName,
|
|
433
470
|
'Domain': self._Domain,
|
|
434
471
|
'HostIpAddress': self.HostIpAddress,
|
|
@@ -436,6 +473,7 @@ class ZeroconfDiscoveryResult:
|
|
|
436
473
|
'HostIpPort': self._HostIpPort,
|
|
437
474
|
'HostTTL': self._HostTTL,
|
|
438
475
|
'IsDynamicDevice': self.IsDynamicDevice,
|
|
476
|
+
'IsChromeCast': self._IsChromeCast,
|
|
439
477
|
'Key': self._Key,
|
|
440
478
|
'Name': self._Name,
|
|
441
479
|
'Priority': self._Priority,
|
|
@@ -473,12 +511,14 @@ class ZeroconfDiscoveryResult:
|
|
|
473
511
|
zeroconfApiEndpointResetUsers:str = self.ZeroconfApiEndpointResetUsers
|
|
474
512
|
|
|
475
513
|
# build result.
|
|
514
|
+
if self._Id is not None: msg = '%s\n Id="%s"' % (msg, str(self._Id))
|
|
476
515
|
if self._DeviceName is not None: msg = '%s\n DeviceName="%s"' % (msg, str(self._DeviceName))
|
|
477
516
|
if self._Domain is not None: msg = '%s\n Domain="%s"' % (msg, str(self._Domain))
|
|
478
517
|
if self._HostIpAddresses is not None: msg = '%s\n HostIpAddresses Count=%s' % (msg, str(len(self._HostIpAddresses)))
|
|
479
518
|
if self.HostIpAddress is not None: msg = '%s\n HostIpAddress="%s"' % (msg, str(self.HostIpAddress))
|
|
480
519
|
if self._HostIpPort is not None: msg = '%s\n HostIpPort="%s"' % (msg, str(self._HostIpPort))
|
|
481
520
|
if self._HostTTL is not None: msg = '%s\n HostTTL="%s"' % (msg, str(self._HostTTL))
|
|
521
|
+
if self._IsChromeCast is not None: msg = '%s\n IsChromeCast="%s"' % (msg, str(self._IsChromeCast))
|
|
482
522
|
if self._Key is not None: msg = '%s\n Key="%s"' % (msg, str(self._Key))
|
|
483
523
|
if self._Name is not None: msg = '%s\n Name="%s"' % (msg, str(self._Name))
|
|
484
524
|
if self._Priority is not None: msg = '%s\n Priority="%s"' % (msg, str(self._Priority))
|
|
@@ -49,6 +49,11 @@ from .const import (
|
|
|
49
49
|
CACHE_SOURCE_CACHED:str = "cached"
|
|
50
50
|
CACHE_SOURCE_CURRENT:str = "current"
|
|
51
51
|
|
|
52
|
+
DELAY_DISCONNECT:float = 0.350
|
|
53
|
+
"""
|
|
54
|
+
Time to wait after a Spotify Connect Disconnect command is issued (350ms).
|
|
55
|
+
"""
|
|
56
|
+
|
|
52
57
|
SPOTIFY_DJ_PLAYLIST_ID = "37i9dqzf1eykqdzj48dyyq"
|
|
53
58
|
|
|
54
59
|
SPOTIFY_ONLINE_LINK_PREFIX = "https://open.spotify.com"
|
|
@@ -6944,10 +6949,41 @@ class SpotifyClient:
|
|
|
6944
6949
|
# Spotify Connect Zeroconf `resetUsers` endpoint is not implemented and will
|
|
6945
6950
|
# generate a 404 request response.
|
|
6946
6951
|
if (info.BrandDisplayName != 'librespot'):
|
|
6952
|
+
|
|
6947
6953
|
_logsi.LogVerbose("Issuing Disconnect to Spotify Connect device '%s' for current user context '%s'" % (scDevice.Title, deviceActiveUser))
|
|
6948
6954
|
zcfResult = zconn.Disconnect(delay)
|
|
6949
6955
|
status = status + 'disconnected from user context "%s"; ' % deviceActiveUser
|
|
6956
|
+
|
|
6957
|
+
# delay just a little to give the device time to process the disconnect.
|
|
6958
|
+
# some devices only support a single connection on the spotify connect
|
|
6959
|
+
# zeroconf webserver, so we have to give it a little time to process the
|
|
6960
|
+
# disconnect command before we try to call the Connect method.
|
|
6961
|
+
_logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % DELAY_DISCONNECT)
|
|
6962
|
+
time.sleep(DELAY_DISCONNECT)
|
|
6963
|
+
|
|
6964
|
+
# some manufactureres will reset the device host ip port after a disconnect (e.g. "Denon", etc).
|
|
6965
|
+
# due to this, we must issue a call to zeroconf to re-discover the device,
|
|
6966
|
+
# and update the device results with the updated discovery details.
|
|
6967
|
+
|
|
6968
|
+
# re-discover Spotify Connect devices on the network.
|
|
6969
|
+
_logsi.LogVerbose("Rediscovering Spotify Connect devices on the local network (due to previous disconnect)")
|
|
6970
|
+
rediscovery:SpotifyDiscovery = SpotifyDiscovery(self._ZeroconfClient, printToConsole=False)
|
|
6971
|
+
rediscovery.DiscoverDevices(timeout=self._SpotifyConnectDiscoveryTimeout)
|
|
6972
|
+
|
|
6973
|
+
# process all rediscovered devices, and update selected device.
|
|
6974
|
+
rediscoverResult:ZeroconfDiscoveryResult
|
|
6975
|
+
for rediscoverResult in rediscovery.DiscoveryResults:
|
|
6976
|
+
if (rediscoverResult.Key == scDevice.DiscoveryResult.Key):
|
|
6977
|
+
if (rediscoverResult.HostIpAddress != scDevice.DiscoveryResult.HostIpAddress):
|
|
6978
|
+
_logsi.LogVerbose("Spotify Connect device HostIpAddress changed to \"%s\" from \"%s\" after disconnect" % (rediscoverResult.HostIpAddress, scDevice.DiscoveryResult.HostIpAddress))
|
|
6979
|
+
if (rediscoverResult.HostIpPort != scDevice.DiscoveryResult.HostIpPort):
|
|
6980
|
+
_logsi.LogVerbose("Spotify Connect device HostIpPort changed to \"%s\" from \"%s\" after disconnect" % (rediscoverResult.HostIpPort, scDevice.DiscoveryResult.HostIpPort))
|
|
6981
|
+
_logsi.LogVerbose("Updating Spotify Connect device \"%s\" DiscoveryResult instance with rediscovered properties" % (scDevice.Title))
|
|
6982
|
+
scDevice.DiscoveryResult = rediscoverResult
|
|
6983
|
+
break
|
|
6984
|
+
|
|
6950
6985
|
else:
|
|
6986
|
+
|
|
6951
6987
|
status = status + 'disconnect bypassed for librespot device; '
|
|
6952
6988
|
|
|
6953
6989
|
# connect the device to OUR Spotify Connect user context.
|
|
@@ -9288,8 +9324,6 @@ class SpotifyClient:
|
|
|
9288
9324
|
deviceResultId:str = None
|
|
9289
9325
|
deviceResult:SpotifyConnectDevice = None
|
|
9290
9326
|
|
|
9291
|
-
DELAY_DISCONNECT:float = 0.350
|
|
9292
|
-
|
|
9293
9327
|
try:
|
|
9294
9328
|
|
|
9295
9329
|
# trace.
|
|
@@ -9461,13 +9495,34 @@ class SpotifyClient:
|
|
|
9461
9495
|
|
|
9462
9496
|
_logsi.LogVerbose("Issuing Disconnect to Spotify Connect device '%s' for current user context '%s'" % (deviceResult.Title, deviceActiveUser))
|
|
9463
9497
|
zcfResult = zconn.Disconnect(delay)
|
|
9464
|
-
|
|
9498
|
+
|
|
9465
9499
|
# delay just a little to give the device time to process the disconnect.
|
|
9466
9500
|
# some devices only support a single connection on the spotify connect
|
|
9467
9501
|
# zeroconf webserver, so we have to give it a little time to process the
|
|
9468
9502
|
# disconnect command before we try to call the Connect method.
|
|
9469
9503
|
_logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % DELAY_DISCONNECT)
|
|
9470
9504
|
time.sleep(DELAY_DISCONNECT)
|
|
9505
|
+
|
|
9506
|
+
# some manufactureres will reset the device host ip port after a disconnect (e.g. "Denon", etc).
|
|
9507
|
+
# due to this, we must issue a call to zeroconf to re-discover the device,
|
|
9508
|
+
# and update the device results with the updated discovery details.
|
|
9509
|
+
|
|
9510
|
+
# re-discover Spotify Connect devices on the network
|
|
9511
|
+
_logsi.LogVerbose("Rediscovering Spotify Connect devices on the local network (due to previous disconnect)")
|
|
9512
|
+
rediscovery:SpotifyDiscovery = SpotifyDiscovery(self._ZeroconfClient, printToConsole=False)
|
|
9513
|
+
rediscovery.DiscoverDevices(timeout=self._SpotifyConnectDiscoveryTimeout)
|
|
9514
|
+
|
|
9515
|
+
# process all rediscovered devices, and update selected device.
|
|
9516
|
+
rediscoverResult:ZeroconfDiscoveryResult
|
|
9517
|
+
for rediscoverResult in rediscovery.DiscoveryResults:
|
|
9518
|
+
if (rediscoverResult.Key == scDevice.DiscoveryResult.Key):
|
|
9519
|
+
if (rediscoverResult.HostIpAddress != scDevice.DiscoveryResult.HostIpAddress):
|
|
9520
|
+
_logsi.LogVerbose("Spotify Connect device HostIpAddress changed to \"%s\" from \"%s\" after disconnect" % (rediscoverResult.HostIpAddress, scDevice.DiscoveryResult.HostIpAddress))
|
|
9521
|
+
if (rediscoverResult.HostIpPort != scDevice.DiscoveryResult.HostIpPort):
|
|
9522
|
+
_logsi.LogVerbose("Spotify Connect device HostIpPort changed to \"%s\" from \"%s\" after disconnect" % (rediscoverResult.HostIpPort, scDevice.DiscoveryResult.HostIpPort))
|
|
9523
|
+
_logsi.LogVerbose("Updating Spotify Connect device \"%s\" DiscoveryResult instance with rediscovered properties" % (scDevice.Title))
|
|
9524
|
+
scDevice.DiscoveryResult = rediscoverResult
|
|
9525
|
+
break
|
|
9471
9526
|
|
|
9472
9527
|
# connect the device to OUR Spotify Connect user context.
|
|
9473
9528
|
# note that the result here only indicates that the connect was submitted - NOT that it was successful!
|
|
@@ -12336,119 +12391,6 @@ class SpotifyClient:
|
|
|
12336
12391
|
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
12337
12392
|
|
|
12338
12393
|
|
|
12339
|
-
def PlayerResolveDeviceId(
|
|
12340
|
-
self,
|
|
12341
|
-
deviceValue:str,
|
|
12342
|
-
verifyUserContext:bool=True,
|
|
12343
|
-
verifyTimeout:float=5.0,
|
|
12344
|
-
delay:float=0.25
|
|
12345
|
-
) -> str:
|
|
12346
|
-
"""
|
|
12347
|
-
This method has been deprecated.
|
|
12348
|
-
Use the `GetSpotifyConnectDevice` method instead.
|
|
12349
|
-
|
|
12350
|
-
Resolves a Spotify Connect device identifier from a specified device id, name, alias id,
|
|
12351
|
-
or alias name. This will ensure that the device id can be found on the network, as well
|
|
12352
|
-
as connect to the device if necessary with the current user context.
|
|
12353
|
-
|
|
12354
|
-
Args:
|
|
12355
|
-
deviceValue (str):
|
|
12356
|
-
The device id / name value to check.
|
|
12357
|
-
verifyUserContext (bool):
|
|
12358
|
-
If True, the active user context of the resolved device is checked to ensure it
|
|
12359
|
-
matches the user context specified on the class constructor.
|
|
12360
|
-
If False, the user context will not be checked.
|
|
12361
|
-
Default is True.
|
|
12362
|
-
verifyTimeout (float):
|
|
12363
|
-
Maximum time to wait (in seconds) for the device to become active in the Spotify
|
|
12364
|
-
Connect device list. This value is only used if a Connect command has to be
|
|
12365
|
-
issued to activate the device.
|
|
12366
|
-
Default is 5; value range is 0 - 10.
|
|
12367
|
-
delay (float):
|
|
12368
|
-
Time delay (in seconds) to wait AFTER issuing any command to the device.
|
|
12369
|
-
This delay will give the spotify zeroconf api time to process the change before
|
|
12370
|
-
another command is issued.
|
|
12371
|
-
Default is 0.25; value range is 0 - 10.
|
|
12372
|
-
|
|
12373
|
-
Returns:
|
|
12374
|
-
A device Id for the deviceValue if one could be resolved;
|
|
12375
|
-
otherwise, a null value with the understanding that subsequent operations will probably
|
|
12376
|
-
fail since it's not in the device list.
|
|
12377
|
-
|
|
12378
|
-
Raises:
|
|
12379
|
-
SpotifyWebApiError:
|
|
12380
|
-
If the Spotify Web API request was for a non-authorization service
|
|
12381
|
-
and the response contains error information.
|
|
12382
|
-
SpotifyZeroconfApiError:
|
|
12383
|
-
If any Spotify Zeroconf API request failed.
|
|
12384
|
-
SpotifApiError:
|
|
12385
|
-
If the method fails for any other reason.
|
|
12386
|
-
|
|
12387
|
-
A Zeroconf discovery process is initiated to search for all Spotify Connect devices connected
|
|
12388
|
-
to the local network. A Spotify Zeroconf API `getInfo` call is then issued for each device
|
|
12389
|
-
that was found, searching for a match on the `deviceValue` argument. Depending on the format
|
|
12390
|
-
of the `deviceValue`, the search will match on either a Device ID or RemoteName as well as any
|
|
12391
|
-
alias ID's or Names that are in use (for multi-room configurations).
|
|
12392
|
-
|
|
12393
|
-
The device ID value is returned if a match is found or if the `device_value` argument is null;
|
|
12394
|
-
otherwise, a null value with the understanding that subsequent operations will probably fail
|
|
12395
|
-
since it's not in the device list.
|
|
12396
|
-
|
|
12397
|
-
A user context switch will also be performed if a device id is resolved AND the active user of the
|
|
12398
|
-
device does not match the `SpotifyConnectUsername` argument specified on the class constructor.
|
|
12399
|
-
The user context switch is bypassed if the `verifyUserContext` argument is False, or if the
|
|
12400
|
-
`SpotifyConnectUsername` was not supplied on the class constructor. If the user context is to be
|
|
12401
|
-
switched, then a Disconnect will be issued if a user context is active on the device followed by
|
|
12402
|
-
a Connect to the user context specified on the class constructor.
|
|
12403
|
-
"""
|
|
12404
|
-
apiMethodName:str = 'PlayerResolveDeviceId'
|
|
12405
|
-
apiMethodParms:SIMethodParmListContext = None
|
|
12406
|
-
deviceIdResult:str = None
|
|
12407
|
-
|
|
12408
|
-
try:
|
|
12409
|
-
|
|
12410
|
-
# TODO method is deprecated as of 2024/08/15, v1.0.80.
|
|
12411
|
-
# remove in a future release.
|
|
12412
|
-
|
|
12413
|
-
# trace.
|
|
12414
|
-
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
12415
|
-
apiMethodParms.AppendKeyValue("deviceValue", deviceValue)
|
|
12416
|
-
apiMethodParms.AppendKeyValue("verifyUserContext", verifyUserContext)
|
|
12417
|
-
apiMethodParms.AppendKeyValue("verifyTimeout", verifyTimeout)
|
|
12418
|
-
apiMethodParms.AppendKeyValue("delay", delay)
|
|
12419
|
-
_logsi.LogMethodParmList(SILevel.Verbose, "Resolving Spotify Connect Player Device Id", apiMethodParms)
|
|
12420
|
-
|
|
12421
|
-
# trace.
|
|
12422
|
-
_logsi.LogWarning("SpotifyWebApiPython `PlayerResolveDeviceId` method is deprecated, and will be removed in a future release; use the `GetSpotifyConnectDevice` instead.")
|
|
12423
|
-
|
|
12424
|
-
# call newer method to resolve the device id.
|
|
12425
|
-
scDevice:SpotifyConnectDevice = self.GetSpotifyConnectDevice(
|
|
12426
|
-
deviceValue=deviceValue,
|
|
12427
|
-
verifyTimeout=verifyTimeout,
|
|
12428
|
-
refreshDeviceList=True,
|
|
12429
|
-
activateDevice=True,
|
|
12430
|
-
delay=delay)
|
|
12431
|
-
|
|
12432
|
-
# if device not found then we are done.
|
|
12433
|
-
if scDevice is None:
|
|
12434
|
-
return None
|
|
12435
|
-
|
|
12436
|
-
# otherwise return the device id value.
|
|
12437
|
-
return scDevice.Id
|
|
12438
|
-
|
|
12439
|
-
except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
|
|
12440
|
-
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
12441
|
-
except Exception as ex:
|
|
12442
|
-
|
|
12443
|
-
# format unhandled exception.
|
|
12444
|
-
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
12445
|
-
|
|
12446
|
-
finally:
|
|
12447
|
-
|
|
12448
|
-
# trace.
|
|
12449
|
-
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
12394
|
def PlayerSetRepeatMode(self,
|
|
12453
12395
|
state:str='off',
|
|
12454
12396
|
deviceId:str=None,
|
|
@@ -3,6 +3,7 @@ try:
|
|
|
3
3
|
from queue import Queue, Empty
|
|
4
4
|
except ImportError:
|
|
5
5
|
from Queue import Queue, Empty
|
|
6
|
+
import threading
|
|
6
7
|
import time
|
|
7
8
|
from zeroconf import Zeroconf, ServiceBrowser, ServiceInfo, ServiceStateChange, IPVersion
|
|
8
9
|
|
|
@@ -66,6 +67,7 @@ class SpotifyDiscovery:
|
|
|
66
67
|
self._DiscoveryResults:list[ZeroconfDiscoveryResult] = []
|
|
67
68
|
self._PrintToConsole:bool = printToConsole
|
|
68
69
|
self._ZeroconfClient = zeroconfClient
|
|
70
|
+
self._Zeroconf_Lock = threading.Lock()
|
|
69
71
|
|
|
70
72
|
|
|
71
73
|
def __getitem__(self, key):
|
|
@@ -89,6 +91,17 @@ class SpotifyDiscovery:
|
|
|
89
91
|
return self.ToString()
|
|
90
92
|
|
|
91
93
|
|
|
94
|
+
def ContainsId(self, value:str) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Returns True if the `DiscoveryResults` collection contains the specified id value;
|
|
97
|
+
otherwise, False.
|
|
98
|
+
"""
|
|
99
|
+
item:ZeroconfDiscoveryResult = self.GetResultById(value)
|
|
100
|
+
if item is not None:
|
|
101
|
+
return True
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
92
105
|
@property
|
|
93
106
|
def DiscoveredDeviceNames(self) -> dict:
|
|
94
107
|
"""
|
|
@@ -113,6 +126,27 @@ class SpotifyDiscovery:
|
|
|
113
126
|
return self._DiscoveryResults
|
|
114
127
|
|
|
115
128
|
|
|
129
|
+
def GetResultById(self, value:str) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Returns a `ZeroconfDiscoveryResult` instance if the `DiscoveryResults` collection contains
|
|
132
|
+
the specified device id value; otherwise, None.
|
|
133
|
+
"""
|
|
134
|
+
result:ZeroconfDiscoveryResult = None
|
|
135
|
+
if value is None:
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
# convert case for comparison.
|
|
139
|
+
value = value.lower()
|
|
140
|
+
|
|
141
|
+
# process all discovered devices.
|
|
142
|
+
item:ZeroconfDiscoveryResult
|
|
143
|
+
for item in self._DiscoveryResults:
|
|
144
|
+
if (item.Id.lower() == value):
|
|
145
|
+
result = item
|
|
146
|
+
break
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
|
|
116
150
|
@property
|
|
117
151
|
def ZeroconfClient(self) -> Zeroconf:
|
|
118
152
|
"""
|
|
@@ -121,38 +155,41 @@ class SpotifyDiscovery:
|
|
|
121
155
|
return self._ZeroconfClient
|
|
122
156
|
|
|
123
157
|
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
158
|
+
def _GetZeroconfDiscoveryResult(
|
|
159
|
+
self,
|
|
160
|
+
zeroconf:Zeroconf,
|
|
161
|
+
serviceType:str,
|
|
162
|
+
serviceName:str,
|
|
163
|
+
serviceStateChange:ServiceStateChange,
|
|
164
|
+
serviceStateChangeDesc:str
|
|
165
|
+
) -> ZeroconfDiscoveryResult:
|
|
130
166
|
"""
|
|
131
|
-
|
|
132
|
-
removed, or updated). In our case, we only care about devices that were added.
|
|
133
|
-
|
|
134
|
-
IMPORTANT - This method is executed on a different thread, so be careful about
|
|
135
|
-
multi-threaded operations!
|
|
167
|
+
Builds a `ZeroconfDiscoveryResult` instance from Zeroconf Service Information data.
|
|
136
168
|
"""
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
_logsi.LogVerbose("Discovered Spotify Connect device service: '%s' (%s:%s)" % (serviceName, serviceType, serviceStateChange))
|
|
169
|
+
result:ZeroconfDiscoveryResult = None
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
|
|
173
|
+
# trace.
|
|
174
|
+
_logsi.EnterMethod(SILevel.Debug)
|
|
144
175
|
|
|
176
|
+
# get device service information instance; if none, then we are done.
|
|
145
177
|
deviceName:str = (serviceName.split(".")[0])
|
|
146
|
-
serviceInfo:ServiceInfo = zeroconf.get_service_info(
|
|
178
|
+
serviceInfo:ServiceInfo = zeroconf.get_service_info(serviceType, serviceName)
|
|
147
179
|
if serviceInfo is None:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
180
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification ignored: \"%s\" (%s:%s) - no serviceinfo data" % (serviceName, serviceType, serviceStateChange))
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
# get list of displayable (parsed) ipv4 addresses; if none, then we are done.
|
|
151
184
|
ipAddressList = serviceInfo.parsed_addresses(IPVersion.V4Only)
|
|
152
185
|
if (ipAddressList is None):
|
|
153
|
-
|
|
186
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification ignored: \"%s\" (%s:%s) - no IP address list" % (serviceName, serviceType, serviceStateChange))
|
|
187
|
+
return result
|
|
154
188
|
|
|
155
|
-
#
|
|
189
|
+
# trace.
|
|
190
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf service details: \"%s\" %s (serviceinfo object)" % (deviceName, ipAddressList), serviceInfo)
|
|
191
|
+
|
|
192
|
+
# create new discovery result instance.
|
|
156
193
|
result:ZeroconfDiscoveryResult = ZeroconfDiscoveryResult()
|
|
157
194
|
result.DeviceName = deviceName
|
|
158
195
|
result.Domain = '.local'
|
|
@@ -169,14 +206,8 @@ class SpotifyDiscovery:
|
|
|
169
206
|
result.ServiceInfo = serviceInfo
|
|
170
207
|
result.ServiceType = serviceType
|
|
171
208
|
result.Weight = serviceInfo.weight
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
deviceKey:str = "'%s' (%s:%i)" % (deviceName, result.HostIpAddress, result.HostIpPort)
|
|
175
|
-
_logsi.LogVerbose("Discovered Spotify Connect device: %s %s" % (deviceKey, result.HostIpAddresses))
|
|
176
|
-
if (self._PrintToConsole == True):
|
|
177
|
-
print("Discovered Spotify Connect device: %s [%s]" % (deviceKey, result.HostIpAddresses))
|
|
178
|
-
_logsi.LogObject(SILevel.Verbose, "Discovered Spotify Connect device ServiceInfo: %s" % (deviceKey), serviceInfo)
|
|
179
|
-
|
|
209
|
+
result.Id = "\"%s\" (%s:%s)" % (result.DeviceName, result.HostIpAddress, result.HostIpPort)
|
|
210
|
+
|
|
180
211
|
# process service info propertys.
|
|
181
212
|
# note that the property keys and values must first be decoded to utf-8 encoding.
|
|
182
213
|
if serviceInfo.properties is not None:
|
|
@@ -191,25 +222,156 @@ class SpotifyDiscovery:
|
|
|
191
222
|
result.SpotifyConnectCPath = valueStr
|
|
192
223
|
elif keyStr.lower() == 'version':
|
|
193
224
|
result.SpotifyConnectVersion = valueStr
|
|
194
|
-
_logsi.LogDictionary(SILevel.Verbose, "
|
|
225
|
+
_logsi.LogDictionary(SILevel.Verbose, "Spotify Connect Zeroconf service details: \"%s\" %s (serviceinfo properties)" % (deviceName, ipAddressList), dspProperties)
|
|
195
226
|
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
self._DiscoveredDeviceNames[deviceKey] = deviceName
|
|
227
|
+
# trace.
|
|
228
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf Discovery Result: %s (object)" % (result.Id), result, excludeNonPublic=True)
|
|
199
229
|
|
|
200
|
-
#
|
|
201
|
-
|
|
230
|
+
# return discovery result.
|
|
231
|
+
return result
|
|
202
232
|
|
|
233
|
+
except Exception as ex:
|
|
234
|
+
|
|
203
235
|
# trace.
|
|
204
|
-
_logsi.
|
|
236
|
+
_logsi.LogException("Could not load ZeroconfDiscoveryResult instance from Zeroconf serviceinfo object.", ex, logToSystemLogger=False)
|
|
237
|
+
|
|
238
|
+
# ignore exception
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
finally:
|
|
242
|
+
|
|
243
|
+
# trace.
|
|
244
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
205
245
|
|
|
206
|
-
elif (serviceStateChange is ServiceStateChange.Removed):
|
|
207
|
-
_logsi.LogVerbose("Discovered Spotify Connect device removal (ignored): '%s' (%s:%s)", serviceName, serviceType, serviceStateChange)
|
|
208
|
-
pass
|
|
209
246
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
247
|
+
def _OnServiceStateChange(
|
|
248
|
+
self,
|
|
249
|
+
zeroconf:Zeroconf,
|
|
250
|
+
service_type:str,
|
|
251
|
+
name:str,
|
|
252
|
+
state_change:ServiceStateChange
|
|
253
|
+
) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Called by the zeroconf ServiceBrowser when a service state has changed (e.g. added,
|
|
256
|
+
removed, or updated). In our case, we only care about devices that were added.
|
|
257
|
+
|
|
258
|
+
IMPORTANT - This method is executed on a different thread, so be careful about
|
|
259
|
+
multi-threaded operations!
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
# this method can be tested by issuing the following commands from an MS-DOS
|
|
263
|
+
# command prompt to simulate Zeroconf registration updates.
|
|
264
|
+
|
|
265
|
+
# - add a new Spotify Connect instance registration:
|
|
266
|
+
# > dns-sd -R "MyTestSC1" _spotify-connect._tcp. local 8080 cpath=/zcXXX version=1.0
|
|
267
|
+
|
|
268
|
+
# - update a new Spotify Connect instance registration;
|
|
269
|
+
# - if you change the name, service type, or port number, a NEW registration is created!
|
|
270
|
+
# - for updates, the only thing you can really update is the TXT record properties.
|
|
271
|
+
# > dns-sd -R "MyTestSC1" _spotify-connect._tcp. local 8080 cpath=/zcYYY version=1.0
|
|
272
|
+
|
|
273
|
+
# - remove an existing Spotify Connect instance registration;
|
|
274
|
+
# > the only way to remove an entry is to kill the process the entry was created under!
|
|
275
|
+
|
|
276
|
+
# enter the following section one thread at a time (threadsafe).
|
|
277
|
+
with self._Zeroconf_Lock:
|
|
278
|
+
|
|
279
|
+
# copy passed parameters to thread-safe storage.
|
|
280
|
+
serviceType:str = service_type
|
|
281
|
+
serviceName:str = name
|
|
282
|
+
serviceStateChange:ServiceStateChange = state_change
|
|
283
|
+
serviceStateChangeDesc:str = None
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
|
|
287
|
+
# trace.
|
|
288
|
+
_logsi.EnterMethod(SILevel.Debug)
|
|
289
|
+
|
|
290
|
+
# process by the state change value.
|
|
291
|
+
if (serviceStateChange is ServiceStateChange.Added) \
|
|
292
|
+
or (serviceStateChange is ServiceStateChange.Updated):
|
|
293
|
+
|
|
294
|
+
# set service state change description.
|
|
295
|
+
serviceStateChangeDesc = "Added"
|
|
296
|
+
if (serviceStateChange is ServiceStateChange.Updated):
|
|
297
|
+
serviceStateChangeDesc = "Updated"
|
|
298
|
+
|
|
299
|
+
# trace.
|
|
300
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, serviceStateChangeDesc))
|
|
301
|
+
|
|
302
|
+
# build discovery result instance from service state information.
|
|
303
|
+
# if nothing returned, then don't bother!
|
|
304
|
+
result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(zeroconf, serviceType, serviceName, serviceStateChange, serviceStateChangeDesc)
|
|
305
|
+
if (result is None):
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
# add device name to list of registered names.
|
|
309
|
+
self._DiscoveredDeviceNames[result.Id] = result.DeviceName
|
|
310
|
+
|
|
311
|
+
# add / update discovered results by a unique result ID value, which is comprised
|
|
312
|
+
# of the name, ip address, and ip port of the service. we use the unique ID value
|
|
313
|
+
# since some manufacturers use random ip port numbers, which will ensure that we
|
|
314
|
+
# remove (or update) the unique service entry. in other words, we can't just rely
|
|
315
|
+
# on the name since a manufacturer could re-register the same name using a different
|
|
316
|
+
# ip port value.
|
|
317
|
+
item:ZeroconfDiscoveryResult = self.GetResultById(result.Id)
|
|
318
|
+
if (item is None):
|
|
319
|
+
self._DiscoveryResults.append(result)
|
|
320
|
+
else:
|
|
321
|
+
item = result
|
|
322
|
+
|
|
323
|
+
# trace.
|
|
324
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
|
|
325
|
+
if (self._PrintToConsole == True):
|
|
326
|
+
print("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
|
|
327
|
+
|
|
328
|
+
elif (serviceStateChange is ServiceStateChange.Removed):
|
|
329
|
+
|
|
330
|
+
# set service state change description.
|
|
331
|
+
serviceStateChangeDesc = "Removed"
|
|
332
|
+
|
|
333
|
+
# trace.
|
|
334
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, serviceStateChangeDesc))
|
|
335
|
+
|
|
336
|
+
# build discovery result instance from service state information.
|
|
337
|
+
# if nothing returned, then don't bother!
|
|
338
|
+
result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(zeroconf, serviceType, serviceName, serviceStateChange, serviceStateChangeDesc)
|
|
339
|
+
if (result is None):
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
# remove device name from list of registered names.
|
|
343
|
+
self._DiscoveredDeviceNames.pop(result.Id, None)
|
|
344
|
+
|
|
345
|
+
# remove device from results collection;
|
|
346
|
+
# the reversed() function creates an iterator that traverses the list in reverse order.
|
|
347
|
+
# This ensures that removing an element doesn't affect the indices of the subsequent
|
|
348
|
+
# elements we're going to iterate over.
|
|
349
|
+
resultCompare:str = result.Id.lower()
|
|
350
|
+
for i in reversed(range(len(self._DiscoveryResults))):
|
|
351
|
+
if (self._DiscoveryResults[i].Id.lower() == resultCompare):
|
|
352
|
+
|
|
353
|
+
# remove discovery result.
|
|
354
|
+
result = self._DiscoveryResults.pop(i)
|
|
355
|
+
|
|
356
|
+
# trace.
|
|
357
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
|
|
358
|
+
if (self._PrintToConsole == True):
|
|
359
|
+
print("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
|
|
360
|
+
|
|
361
|
+
break
|
|
362
|
+
|
|
363
|
+
except Exception as ex:
|
|
364
|
+
|
|
365
|
+
# trace.
|
|
366
|
+
_logsi.LogException("Unhandled exception occured while processing Spotify Connect Zeroconf discovery service notification (%s)" % (serviceStateChange), ex, logToSystemLogger=False)
|
|
367
|
+
|
|
368
|
+
# ignore exception
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
finally:
|
|
372
|
+
|
|
373
|
+
# trace.
|
|
374
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
213
375
|
|
|
214
376
|
|
|
215
377
|
def DiscoverDevices(self, timeout:float=2) -> dict:
|
|
@@ -1214,7 +1214,7 @@ class ZeroconfConnect:
|
|
|
1214
1214
|
# try connecting to the device until we timeout or there are no more "connection refused" errors.
|
|
1215
1215
|
loopTotalDelay:float = 0
|
|
1216
1216
|
LOOP_DELAY:float = 0.25
|
|
1217
|
-
LOOP_TIMEOUT:float =
|
|
1217
|
+
LOOP_TIMEOUT:float = 2.0
|
|
1218
1218
|
while True:
|
|
1219
1219
|
|
|
1220
1220
|
try:
|
|
File without changes
|
|
File without changes
|
{spotifywebapiPython-1.0.131.dist-info → spotifywebapiPython-1.0.133.dist-info}/top_level.txt
RENAMED
|
File without changes
|