spotifywebapiPython 1.0.132__py3-none-any.whl → 1.0.134__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.132.dist-info → spotifywebapiPython-1.0.134.dist-info}/METADATA +1 -1
- {spotifywebapiPython-1.0.132.dist-info → spotifywebapiPython-1.0.134.dist-info}/RECORD +23 -23
- spotifywebapipython/const.py +1 -1
- spotifywebapipython/docs/__init__.html +14 -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 -2
- spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +40 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +2 -2
- spotifywebapipython/models/zeroconfdiscoveryresult.py +21 -0
- spotifywebapipython/spotifyclient.py +36 -13
- spotifywebapipython/spotifydiscovery.py +207 -45
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +3 -3
- {spotifywebapiPython-1.0.132.dist-info → spotifywebapiPython-1.0.134.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.132.dist-info → spotifywebapiPython-1.0.134.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.132.dist-info → spotifywebapiPython-1.0.134.dist-info}/top_level.txt +0 -0
|
@@ -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:
|
|
@@ -405,7 +405,7 @@ class ZeroconfConnect:
|
|
|
405
405
|
apiMethodParms.AppendKeyValue("username", username)
|
|
406
406
|
apiMethodParms.AppendKeyValue("password (with mask)", passwordMaskString(password))
|
|
407
407
|
apiMethodParms.AppendKeyValue("delay", delay)
|
|
408
|
-
_logsi.LogMethodParmList(SILevel.Verbose, "Connecting device to Spotify Connect (ip=%s)" % self._HostIpAddress, apiMethodParms)
|
|
408
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Connecting device to Spotify Connect (ip=%s:%s)" % (self._HostIpAddress, self._HostIpPort), apiMethodParms)
|
|
409
409
|
|
|
410
410
|
# validations.
|
|
411
411
|
delay = validateDelay(delay, 0.50, 10)
|
|
@@ -536,7 +536,7 @@ class ZeroconfConnect:
|
|
|
536
536
|
apiMethodParms.AppendKeyValue("info.TokenType", info.TokenType)
|
|
537
537
|
apiMethodParms.AppendKeyValue("info.BrandDisplayName", info.BrandDisplayName)
|
|
538
538
|
apiMethodParms.AppendKeyValue("info.ModelDisplayName", info.ModelDisplayName)
|
|
539
|
-
_logsi.LogMethodParmList(SILevel.Verbose, "Issuing Spotify Connect Zeroconf addUser request (ip=%s)" % self._HostIpAddress, apiMethodParms)
|
|
539
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Issuing Spotify Connect Zeroconf addUser request (ip=%s:%s)" % (self._HostIpAddress, self._HostIpPort), apiMethodParms)
|
|
540
540
|
|
|
541
541
|
# set request endpoint.
|
|
542
542
|
endpoint:str = self.GetEndpoint('addUser')
|
|
@@ -1022,7 +1022,7 @@ class ZeroconfConnect:
|
|
|
1022
1022
|
apiMethodParms.AppendKeyValue("Uri", self._Uri)
|
|
1023
1023
|
apiMethodParms.AppendKeyValue("delay", delay)
|
|
1024
1024
|
apiMethodParms.AppendKeyValue("ignoreStatusResult", ignoreStatusResult)
|
|
1025
|
-
_logsi.LogMethodParmList(SILevel.Verbose, "Disconnecting device from Spotify Connect (ip=%s)" % self._HostIpAddress, apiMethodParms)
|
|
1025
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Disconnecting device from Spotify Connect (ip=%s:%s)" % (self._HostIpAddress, self._HostIpPort), apiMethodParms)
|
|
1026
1026
|
|
|
1027
1027
|
# validations.
|
|
1028
1028
|
delay = validateDelay(delay, 0.50, 10)
|
|
File without changes
|
|
File without changes
|
{spotifywebapiPython-1.0.132.dist-info → spotifywebapiPython-1.0.134.dist-info}/top_level.txt
RENAMED
|
File without changes
|