spotifywebapiPython 1.0.148__py3-none-any.whl → 1.0.150__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 (38) hide show
  1. {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.150.dist-info}/METADATA +5 -9
  2. {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.150.dist-info}/RECORD +38 -29
  3. spotifywebapipython/__init__.py +0 -2
  4. spotifywebapipython/const.py +1 -1
  5. spotifywebapipython/docs/__init__.html +25 -4
  6. spotifywebapipython/docs/index.html +26 -2
  7. spotifywebapipython/docs/search.js +1 -1
  8. spotifywebapipython/docs/spotifywebapipython/const.html +3 -3
  9. spotifywebapipython/docs/spotifywebapipython/saappmessages.html +2 -2
  10. spotifywebapipython/docs/spotifywebapipython/sautils.html +8 -8
  11. spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +2 -2
  12. spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +2 -2
  13. spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +2 -2
  14. spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +514 -741
  15. spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +3 -3
  16. spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +2 -2
  17. spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +2 -2
  18. spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +466 -0
  19. spotifywebapipython/models/spotifyconnectdevice.py +84 -4
  20. spotifywebapipython/models/spotifyconnectdevices.py +16 -307
  21. spotifywebapipython/models/zeroconfdiscoveryresult.py +1 -42
  22. spotifywebapipython/sautils.py +0 -1
  23. spotifywebapipython/spotifyclient.py +1380 -1834
  24. spotifywebapipython/spotifyconnect/__init__.py +19 -0
  25. spotifywebapipython/{spotifyconnectdeviceeventargs.py → spotifyconnect/spotifyconnectdeviceeventargs.py} +1 -1
  26. spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +1994 -0
  27. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +336 -0
  28. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +609 -0
  29. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +317 -0
  30. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +10 -0
  31. spotifywebapipython/spotifyconnect/spotifyconnectzeroconflistener.py +335 -0
  32. spotifywebapipython/spotifydiscovery.py +0 -1
  33. spotifywebapipython/spotifywebplayertoken.py +344 -0
  34. spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +4 -52
  35. spotifywebapipython/zeroconfapi/zeroconfresponse.py +8 -0
  36. {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.150.dist-info}/LICENSE +0 -0
  37. {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.150.dist-info}/WHEEL +0 -0
  38. {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.150.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1994 @@
1
+ # external package imports.
2
+ from abc import abstractmethod
3
+ from collections.abc import Mapping
4
+ import copy
5
+ from datetime import datetime
6
+ import hashlib
7
+ from pychromecast import CastBrowser, CastInfo, Chromecast, get_chromecast_from_cast_info
8
+ import socket
9
+ import threading
10
+ import time
11
+ from uuid import UUID
12
+ from zeroconf import Zeroconf, ServiceBrowser
13
+
14
+ # our package imports.
15
+ from .spotifyconnectzeroconfcastapptask import SpotifyConnectZeroconfCastAppTask
16
+ from .spotifyconnectzeroconfcastlistener import SpotifyConnectZeroconfCastListener
17
+ from .spotifyconnectdeviceeventargs import SpotifyConnectDeviceEventArgs
18
+ from .spotifyconnectzeroconflistener import ZEROCONF_SERVICETYPE_SPOTIFYCONNECT, SpotifyConnectZeroconfListener
19
+ from .spotifyconnectzeroconfcastcontroller import (
20
+ TYPE_ADD_USER_RESPONSE,
21
+ TYPE_ADD_USER_ERROR,
22
+ TYPE_GET_INFO_ERROR,
23
+ TYPE_TRANSFER_ERROR_RESPONSE,
24
+ TYPE_TRANSFER_SUCCESS_RESPONSE
25
+ )
26
+ # note - cannot reference `SpotifyClient` due to circular import!
27
+ # from spotifywebapipython import SpotifyClient
28
+ from spotifywebapipython import SpotifyApiError
29
+ from spotifywebapipython.models import Device, PlayerPlayState, SpotifyConnectDevices, SpotifyConnectDevice, ZeroconfDiscoveryResult
30
+ from spotifywebapipython.saappmessages import SAAppMessages
31
+ from spotifywebapipython.sautils import Event, validateDelay
32
+ from spotifywebapipython.zeroconfapi import ZeroconfGetInfo, ZeroconfConnect, ZeroconfResponse
33
+
34
+ # get smartinspect logger reference; create a new session for this module name.
35
+ from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SIColors
36
+ import logging
37
+
38
+ _logsi:SISession = SIAuto.Si.GetSession(__name__)
39
+ if (_logsi == None):
40
+ _logsi = SIAuto.Si.AddSession(__name__, True)
41
+ _logsi.SystemLogger = logging.getLogger(__name__)
42
+
43
+
44
+ class SpotifyConnectDirectoryTask(threading.Thread):
45
+ """
46
+ Spotify Connect Directory thread task.
47
+
48
+ This class discovers available Spotify Connect devices on the local network.
49
+ Discovered devices are then added to an internal list, which can be referenced
50
+ by clients.
51
+
52
+ The directory task is started immediately after the authorization token is
53
+ established in the SpotifyClient object.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ spotifyClientInstance, # :SpotifyClient,
59
+ zeroconfInstance:Zeroconf=None,
60
+ initialDiscoveryTimeout:float=3.0,
61
+ ) -> None:
62
+ """
63
+ Initializes a new instance of the class.
64
+
65
+ Args:
66
+ spotifyClientInstance (SpotifyClient):
67
+ SpotifyClient instance used to interface with the Spotify Web API.
68
+ zeroconfInstance (Zeroconf):
69
+ Zeroconf instance used to query zeroconf; otherwise, None to
70
+ create a new zeroconf instance.
71
+ initialDiscoveryTimeout (float):
72
+ Time (in seconds) to give child Zeroconf discovery threads time to process
73
+ initial Zeroconf service information changes.
74
+ Default is 3.
75
+
76
+ The `initialDiscoveryTimeout` argument gives the child Zeroconf discovery threads
77
+ time to process initial Zeroconf service information changes prior to setting the
78
+ `WaitForInitComplete` event as complete. This gives the directory thread task
79
+ time to discover all of the Spotify Connect devices currently attached to the
80
+ local network.
81
+ """
82
+ # invoke base class method.
83
+ super().__init__()
84
+
85
+ # note that the initialization method executes in the parent thread;
86
+ # the code in the `run` method executes on a child (daemon) thread.
87
+
88
+ # set thread name before we start logging.
89
+ self.name = "Spotify Connect Directory Task"
90
+
91
+ # validations.
92
+ if (spotifyClientInstance is None):
93
+ raise SpotifyApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % ("__init__", 'spotifyClientInstance'), logsi=_logsi)
94
+ if (spotifyClientInstance.SpotifyConnectLoginId is None) or (spotifyClientInstance.SpotifyConnectLoginId.strip() == ""):
95
+ raise SpotifyApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % ("__init__", 'spotifyClientInstance.SpotifyConnectLoginId'), logsi=_logsi)
96
+ if (not isinstance(initialDiscoveryTimeout, float)) and (not isinstance(initialDiscoveryTimeout, int)):
97
+ initialDiscoveryTimeout = 3.0
98
+
99
+ # initialize storage.
100
+ self._CastAppTasks:Mapping[str, SpotifyConnectZeroconfCastAppTask] = {}
101
+ self._CastBrowser:CastBrowser = None
102
+ self._InitialDiscoveryTimeout = initialDiscoveryTimeout
103
+ self._IsStopRequested:bool = False
104
+ self._SpotifyClientInstance = spotifyClientInstance
105
+ self._SpotifyConnectBrowser:ServiceBrowser = None
106
+ self._SpotifyConnectDevices:SpotifyConnectDevices = SpotifyConnectDevices()
107
+ self._SpotifyConnectDevices_RLock = threading.RLock() # re-entrant lock to sync access to devices collection.
108
+ self._ZeroconfInstance:Zeroconf = zeroconfInstance
109
+ self._Zeroconf_Lock = threading.Lock() # syncronous lock for zeroconf service state changes.
110
+
111
+ # define all events raised by this class.
112
+ self.DeviceAdded = Event()
113
+ """
114
+ Event raised when a Spotify Connect device entry has been added.
115
+ """
116
+
117
+ self.DeviceRemoved = Event()
118
+ """
119
+ Event raised when a Spotify Connect device entry has been removed.
120
+ """
121
+
122
+ self.DeviceUpdated = Event()
123
+ """
124
+ Event raised when a Spotify Connect device entry has been updated.
125
+ """
126
+
127
+ # define all threading events raised by this class.
128
+ self.WaitForInitComplete = threading.Event()
129
+ """
130
+ Event that will be posted when the task has been initialized, and is
131
+ waiting for commands or device notifications.
132
+ """
133
+
134
+ self.WaitForActivationComplete = threading.Event()
135
+ """
136
+ Event that will be posted when the task has completed the activation sequence for
137
+ the device, which is either a `addUserResponse` or `addUserError` notification.
138
+ """
139
+
140
+ self.WaitForTransferComplete = threading.Event()
141
+ """
142
+ Event that will be posted when the task has completed the playback transfer sequence for
143
+ the device, which is either a `transferSuccess` or `transferError` notification.
144
+ """
145
+
146
+ # wire up event handlers.
147
+ self.DeviceAdded += self.OnDeviceAdded
148
+ self.DeviceRemoved += self.OnDeviceRemoved
149
+ self.DeviceUpdated += self.OnDeviceUpdated
150
+
151
+ # clear event notifiers.
152
+ self.WaitForInitComplete.clear()
153
+ self.WaitForActivationComplete.set() # indicate activation is complete (initial state).
154
+ self.WaitForTransferComplete.set() # indicate transfer is complete (initial state).
155
+
156
+
157
+ @property
158
+ def InitialDiscoveryTimeout(self) -> float:
159
+ """
160
+ Time to wait (in seconds) after starting the Zeroconf discovery thread
161
+ tasks before marking the directory task initialization as complete.
162
+ """
163
+ return self._InitialDiscoveryTimeout
164
+
165
+
166
+ @property
167
+ def IsStopRequested(self) -> bool:
168
+ """
169
+ Indicator used to denote the task has been asked to stop by the main thread.
170
+ """
171
+ return self._IsStopRequested
172
+
173
+ @IsStopRequested.setter
174
+ def IsStopRequested(self, value:bool):
175
+ """
176
+ Sets the IsStopRequested property value.
177
+ """
178
+ if isinstance(value, bool):
179
+ self._IsStopRequested = value
180
+
181
+
182
+ @property
183
+ def CastAppTasks(self) -> Mapping[str, SpotifyConnectZeroconfCastAppTask]:
184
+ """
185
+ Collection of SpotifyConnectZeroconfCastAppTask objects that have been created
186
+ when transferring playback.
187
+ """
188
+ return self._CastAppTasks
189
+
190
+
191
+ @property
192
+ def CastBrowser(self) -> CastBrowser:
193
+ """
194
+ Returns the internal CastBrowser object.
195
+ """
196
+ return self._CastBrowser
197
+
198
+
199
+ @property
200
+ def IsZeroconDiscoveryEnabled(self) -> bool:
201
+ """
202
+ Returns True if Zeroconf discovery is enabled; otherwise, False.
203
+ """
204
+ # is discovery enabled?
205
+ # is zeroconf discovery enabled?
206
+ if (not self.is_alive()):
207
+ return False
208
+ if (self._InitialDiscoveryTimeout == 0):
209
+ return False
210
+ return True
211
+
212
+
213
+ @property
214
+ def SpotifyClientInstance(self) -> object: # SpotifyClient:
215
+ """
216
+ Returns the SpotifyClient instance.
217
+ """
218
+ return self._SpotifyClientInstance
219
+
220
+
221
+ @property
222
+ def ZeroconfInstance(self) -> Zeroconf:
223
+ """
224
+ Returns the zeroconf instance.
225
+ """
226
+ return self._ZeroconfInstance
227
+
228
+
229
+ def run(self):
230
+ """
231
+ The task to perform on a seperate thread.
232
+ """
233
+ try:
234
+
235
+ # trace.
236
+ _logsi.LogVerbose("%s - Starting thread RUN / TASK method" % (self.name))
237
+ _logsi.LogThread(SILevel.Debug, "%s - Thread information" % (self.name), self)
238
+
239
+ # refresh dynamic device list.
240
+ self.RefreshDynamicDevices()
241
+
242
+ # is zeroconf discovery enabled?
243
+ if (not self.IsZeroconDiscoveryEnabled):
244
+
245
+ _logsi.LogVerbose("%s - Spotify Connect Zeroconf discovery is disabled; only Spotify Web API player devices will be recognized" % (self.name), colorValue=SIColors.Coral)
246
+
247
+ else:
248
+
249
+ # create new spotify connect zeroconf discovery browser instance.
250
+ # this will discover spotify connect devices on the local network.
251
+ # when a device is found, cast_listener.add_cast is called.
252
+ # when a deviceost, the cast_listener.remove_cast is called
253
+ # when a devicepdated, cast_listener.update_cast is called.
254
+ _logsi.LogVerbose("%s - Starting Spotify Connect Zeroconf discovery browser" % (self.name))
255
+ handler:SpotifyConnectZeroconfListener = SpotifyConnectZeroconfListener(self, self._Zeroconf_Lock)
256
+ self._SpotifyConnectBrowser = ServiceBrowser(
257
+ self._ZeroconfInstance,
258
+ ZEROCONF_SERVICETYPE_SPOTIFYCONNECT,
259
+ handlers=[handler.OnServiceStateChange]
260
+ )
261
+
262
+ # create new chromecast zeroconf discovery browser instance.
263
+ # this will discover Chromecasts on the local network.
264
+ # when a Chromecast is found, cast_listener.add_cast is called.
265
+ # when a Chromecast is lost, the cast_listener.remove_cast is called
266
+ # when a Chromecast is updated, cast_listener.update_cast is called.
267
+ _logsi.LogVerbose("%s - Starting Chromecast Zeroconf discovery browser" % (self.name))
268
+ self._CastBrowser = CastBrowser(
269
+ zeroconf_instance=self._ZeroconfInstance,
270
+ cast_listener=SpotifyConnectZeroconfCastListener(self, self._Zeroconf_Lock),
271
+ known_hosts=None
272
+ )
273
+ self._CastBrowser.start_discovery()
274
+
275
+ # give child Zeroconf discovery threads time to process initial service information discovery.
276
+ time.sleep(self._InitialDiscoveryTimeout)
277
+
278
+ # trace - dump devices discovered initially.
279
+ if (_logsi.IsOn(SILevel.Verbose)):
280
+
281
+ # use lock, as zeroconf tasks could be updating the device list.
282
+ with self._Zeroconf_Lock:
283
+
284
+ # trace.
285
+ _logsi.LogVerbose("%s - Spotify Connect devices discovered at initialization (%s items)" % (self.name, len(self._SpotifyConnectDevices)), colorValue=SIColors.Coral)
286
+
287
+ # trace - log all devices that were discovered initially to SmartInspect console.
288
+ for scDevice in self._SpotifyConnectDevices.Items:
289
+ isActive:str = " (active)" if (scDevice.IsActiveDevice) else ""
290
+ _logsi.LogObject(SILevel.Verbose, "Spotify Connect device: %s [%s]%s - object" % (scDevice.Title, scDevice.DiscoveryResult.Description, isActive), scDevice, excludeNonPublic=True, colorValue=SIColors.Coral)
291
+ _logsi.LogObject(SILevel.Verbose, "Spotify Connect device: %s [%s]%s - DiscoveryResult" % (scDevice.Title, scDevice.DiscoveryResult.Description, isActive), scDevice.DiscoveryResult, excludeNonPublic=True, colorValue=SIColors.Coral)
292
+ _logsi.LogObject(SILevel.Verbose, "Spotify Connect device: %s [%s]%s - DeviceInfo / getInfo" % (scDevice.Title, scDevice.DiscoveryResult.Description, isActive), scDevice.DeviceInfo, excludeNonPublic=True, colorValue=SIColors.Coral)
293
+
294
+ # trace - log all devices that were discovered initially to system logger.
295
+ # (since LogObject does not log to system logger).
296
+ for scDevice in self._SpotifyConnectDevices.Items:
297
+ isActive:str = " (active)" if (scDevice.IsActiveDevice) else ""
298
+ _logsi.LogVerbose("Spotify Connect device: %s [%s]%s" % (scDevice.Title, scDevice.DiscoveryResult.Description, isActive))
299
+
300
+ # indicate we are ready for commands.
301
+ self.WaitForInitComplete.set()
302
+
303
+ # event loop.
304
+ # note that the directory task must be kept running in order to control Chromecast devices!
305
+ while True:
306
+
307
+ try:
308
+
309
+ # keep going until we are asked to stop.
310
+ if (self.IsStopRequested):
311
+ _logsi.LogVerbose("%s - Thread task stop requested" % (self.name))
312
+ break
313
+ time.sleep(0.50)
314
+
315
+ except Exception as ex:
316
+
317
+ # trace.
318
+ _logsi.LogException("%s - Exception: %s" % (str(ex)), ex, logToSystemLogger=False)
319
+ # ignore exceptions, as we can't do anything about them.
320
+
321
+ # at this point we have been requested to stop;
322
+ # loop through all active cast app tasks and request that they stop.
323
+ castAppTask:SpotifyConnectZeroconfCastAppTask
324
+ for castAppTask in self._CastAppTasks.values():
325
+ if (not castAppTask.is_alive()):
326
+ _logsi.LogVerbose("%s - Stopping %s" % (self.name, castAppTask.name))
327
+ castAppTask.IsStopRequested = True
328
+ castAppTask.join()
329
+
330
+ # trace.
331
+ _logsi.LogVerbose("%s - Thread task was stopped" % (self.name))
332
+
333
+ except Exception as ex:
334
+
335
+ # trace.
336
+ _logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
337
+ _logsi.LogVerbose("%s - Thread task is ending due to exception" % (self.name))
338
+ # ignore exceptions, since we are shutting down.
339
+
340
+ # indicate we are ready for commands.
341
+ self.WaitForInitComplete.set()
342
+
343
+ finally:
344
+
345
+ try:
346
+
347
+ # stop spotify connect zeroconf discovery browser.
348
+ if (self._SpotifyConnectBrowser is not None):
349
+ _logsi.LogVerbose("%s - Stopping Spotify Connect Zeroconf discovery" % (self.name))
350
+ self._SpotifyConnectBrowser.cancel()
351
+ _logsi.LogObject(SILevel.Verbose, "%s - ZeroconfInstance object after ServiceBrowser resources released" % (self.name), self._ZeroconfInstance)
352
+
353
+ except:
354
+
355
+ # trace.
356
+ _logsi.LogException("%s - An unhandled exception occured while stopping Spotify Connect Zeroconf discovery browser: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
357
+ # ignore exceptions, since we are shutting down.
358
+
359
+ try:
360
+
361
+ # stop chromecast zeroconf discovery browser.
362
+ if (self._CastBrowser is not None):
363
+ _logsi.LogVerbose("%s - Stopping Chromecast Zeroconf discovery" % (self.name))
364
+ self._CastBrowser.stop_discovery()
365
+ _logsi.LogObject(SILevel.Verbose, "%s - ZeroconfInstance object after CastBrowser resources released" % (self.name), self._ZeroconfInstance)
366
+
367
+ except:
368
+
369
+ # trace.
370
+ _logsi.LogException("%s - An unhandled exception occured while stopping Chromecast Zeroconf discovery browser: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
371
+ # ignore exceptions, since we are shutting down.
372
+
373
+
374
+ def ActivateCastAppSpotify(
375
+ self,
376
+ deviceName:str,
377
+ transferPlayback:bool=False,
378
+ timeoutActivation:float=15.0,
379
+ timeoutTransfer:float=10.0,
380
+ ) -> None:
381
+ """
382
+ Activates the Spotify Cast Application on the specified Chromecast device.
383
+
384
+ It will also call the `OnCastGetInfoResponseReceived` method when the Spotify Cast App
385
+ receives a `GetInfoResponse` block that contains Spotify Connect Zeroconf GetInformation
386
+ data about the device.
387
+
388
+ It will also call the `OnCastZeroconfResponseReceived` method when the Spotify Cast App
389
+ receives any of the following message that contain zeroconf response data: `addUserResponse`,
390
+ `addUserError`, `transferError`, `transferSuccess`.
391
+
392
+ Once activated, you have about 20 seconds to transfer playback to the device.
393
+ The Spotify Cast application will automatically end after 20 seconds if playback
394
+ is not transferred to it within the timeout period. Note that any application can
395
+ be used to transfer playback to the device (Spotify Mobile / Desktop / Web, spotipy,
396
+ spotifywebapiPython, etc).
397
+
398
+ Args:
399
+ deviceName (str):
400
+ Chromecast device friendly name to activate.
401
+ transferPlayback (bool):
402
+ True to transfer playback to the device; otherwise, False to just activate
403
+ the Spotify Cast App on the device.
404
+ timeoutActivation (float):
405
+ Amount of time to wait (in seconds) for the Spotify Cast App to be fully activated,
406
+ the user logged in, and ready for transfer of playback.
407
+ Default is 15 seconds.
408
+ timeoutTransfer (float):
409
+ If `transferPlayback` argument is True, the amount of time to wait (in seconds)
410
+ for the transfer to complete; otherwise, argument is ignored.
411
+ Default is 10 seconds.
412
+ """
413
+ apiMethodName:str = "ActivateCastAppSpotify"
414
+ apiMethodParms:SIMethodParmListContext = None
415
+
416
+ try:
417
+
418
+ # trace.
419
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
420
+ apiMethodParms.AppendKeyValue("deviceName", deviceName)
421
+ apiMethodParms.AppendKeyValue("transferPlayback", transferPlayback)
422
+ apiMethodParms.AppendKeyValue("timeoutActivation", timeoutActivation)
423
+ apiMethodParms.AppendKeyValue("timeoutTransfer", timeoutTransfer)
424
+ _logsi.LogMethodParmList(SILevel.Verbose, "Activating Spotify App on Chromecast device: \"%s\"" % (deviceName), apiMethodParms)
425
+
426
+ # validations.
427
+ timeoutActivation = validateDelay(timeoutActivation, 15.0, 30.0)
428
+ timeoutTransfer = validateDelay(timeoutTransfer, 10.0, 20.0)
429
+ if (deviceName is None) or (deviceName.strip() == ""):
430
+ raise SpotifyApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % (apiMethodName, 'deviceName'), logsi=_logsi)
431
+ if (transferPlayback is None):
432
+ transferPlayback = False
433
+
434
+ # validation - Spotify Connect device name / id.
435
+ # syncronize access via lock, as we are accessing the collection.
436
+ with self._SpotifyConnectDevices_RLock:
437
+
438
+ scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByName(deviceName)
439
+ if (scDevice is None):
440
+ scDevice = self._SpotifyConnectDevices.GetDeviceById(deviceName)
441
+ if (scDevice is None):
442
+ raise SpotifyApiError("Chromecast device \"%s\" could not be found in the SpotifyConnectDevices collection " % (deviceName), logsi=_logsi)
443
+
444
+ # is this a chromecast device?
445
+ if (not scDevice.IsChromeCast):
446
+ raise SpotifyApiError("Device %s is not a Chromecast device; use TransferPlayback instead " % (scDevice.Title), logsi=_logsi)
447
+
448
+ # is zeroconf discovery disabled?
449
+ if (not self.IsZeroconDiscoveryEnabled):
450
+ raise SpotifyApiError("Zeroconf discovery is disabled; cannot activate Chromecast devices", logsi=_logsi)
451
+
452
+ # has discovery browser been started yet?
453
+ if (self._CastBrowser is None):
454
+ raise SpotifyApiError("Chromecast discovery browser reference not set; the Browser must be started prior to calling this method", logsi=_logsi)
455
+
456
+ # any devices available?
457
+ if (len(self._CastBrowser.devices) == 0):
458
+ raise SpotifyApiError("No available Chromecast devices detected", logsi=_logsi)
459
+
460
+ # are we waiting on a previous request to finish?
461
+ if (not self.WaitForActivationComplete.is_set):
462
+ raise SpotifyApiError("Waiting for previous Chromecast device activation to complete; try again in a few seconds", logsi=_logsi)
463
+ if (not self.WaitForTransferComplete.is_set):
464
+ raise SpotifyApiError("Waiting for previous Chromecast device playback transfer to complete; try again in a few seconds", logsi=_logsi)
465
+
466
+ # clear event notifiers.
467
+ self.WaitForActivationComplete.clear()
468
+ self.WaitForTransferComplete.clear()
469
+
470
+ # if Spotify Cast App is active on a cast device then request that it stop.
471
+ castAppTask:SpotifyConnectZeroconfCastAppTask = self._CastAppTasks.get(scDevice.DiscoveryResult.Key, None)
472
+ if (castAppTask is not None):
473
+ if (not castAppTask.is_alive()):
474
+ _logsi.LogVerbose("%s - Stopping Spotify Cast App task" % (self.name))
475
+ castAppTask.IsStopRequested = True
476
+ castAppTask.join()
477
+ _logsi.LogVerbose("%s - Spotify Cast App task was stopped successfully" % (self.name))
478
+ self._CastAppTasks.pop(scDevice.DiscoveryResult.Key, None)
479
+
480
+ # get CastInfo details of the specified device.
481
+ _logsi.LogVerbose("%s - Getting Chromecast information for device: \"%s\"" % (self.name, deviceName))
482
+ castInfo:CastInfo = self._CastBrowser.devices[UUID(scDevice.DiscoveryResult.Key)]
483
+
484
+ # syncronize access via lock, as we are accessing the collection.
485
+ with self._SpotifyConnectDevices_RLock:
486
+
487
+ # reset device zeroconf response, as we are re-using an existing object.
488
+ scDevice.ZeroconfResponseInfo = ZeroconfResponse()
489
+
490
+ # connect to the device and build a Chromecast instance.
491
+ castDevice:Chromecast = get_chromecast_from_cast_info(
492
+ cast_info=castInfo,
493
+ zconf=self._ZeroconfInstance,
494
+ tries=2,
495
+ retry_wait=0.5,
496
+ timeout=3)
497
+
498
+ # wait for the device to become ready (5 seconds max).
499
+ castDevice.wait(5)
500
+
501
+ # start the Spotify Cast application on the Chromecast device.
502
+ # this will start the spotify app on the cast device.
503
+ # it will also call the `OnCastGetInfoResponseReceived` method when the SpotifyConnectZeroconfCastAppTask
504
+ # receives a `GetInfoResponse` block that contains Spotify Connect Zeroconf GetInformation data about the device.
505
+ # it will also call the `OnCastZeroconfResponseReceived` method when the SpotifyConnectZeroconfCastAppTask
506
+ # receives a `ZeroconfResponse` block that contains Spotify Connect Zeroconf Response data about the device.
507
+ _logsi.LogVerbose("%s - Starting Spotify Cast application on Chromecast device: \"%s\" (%s)" % (self.name, castDevice.name, str(castDevice.uuid)))
508
+ castAppTask:SpotifyConnectZeroconfCastAppTask = SpotifyConnectZeroconfCastAppTask(
509
+ castDevice,
510
+ self.SpotifyClientInstance,
511
+ self.OnCastGetInfoResponseReceived,
512
+ self.OnCastZeroconfResponseReceived,
513
+ transferPlayback
514
+ )
515
+ castAppTask.daemon = True
516
+ castAppTask.start()
517
+
518
+ # add cast app task to active tasks collection.
519
+ self._CastAppTasks[str(castInfo.uuid)] = castAppTask
520
+
521
+ # wait for the launched spotify app to activate the user (or fail).
522
+ # this occurs when we receive any of the following zeroconf response
523
+ # messages: `addUserResponse`, `addUserError`, `getInfoError`.
524
+
525
+ counter = 0
526
+ while counter < (timeoutActivation + 1):
527
+ if (self.WaitForActivationComplete.wait(1)):
528
+ # syncronize access via lock, as we are accessing the collection.
529
+ with self._SpotifyConnectDevices_RLock:
530
+ scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
531
+ if (scDevice is not None):
532
+ response = scDevice.ZeroconfResponseInfo
533
+ if (response.ResponseSource == TYPE_ADD_USER_RESPONSE):
534
+ # indicate device was reconnected.
535
+ scDevice.WasReConnected = True
536
+ break
537
+ raise SpotifyApiError("Spotify Cast App could not be activated on Chromecast device \"%s\": %s" % (deviceName, response.ToString(False)), logsi=_logsi)
538
+ raise SpotifyApiError("Spotify Cast App could not activated on Chromecast device: unknown error", logsi=_logsi)
539
+ if (counter >= timeoutActivation):
540
+ # syncronize access via lock, as we are accessing the collection.
541
+ with self._SpotifyConnectDevices_RLock:
542
+ scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
543
+ if (scDevice is not None):
544
+ raise SpotifyApiError("Spotify Cast App activation timeout (%s seconds) was exceeded while trying to activate Chromecast device \"%s\"" % (timeoutActivation, deviceName), logsi=_logsi)
545
+ counter += 1
546
+
547
+ # at this point we have received an `adduserResponse` from the Chromecast device, indicating
548
+ # that it has launched the Spotify Cast App and that the user is logged in.
549
+
550
+ # was transfer playback specified?
551
+ if (transferPlayback == True):
552
+
553
+ # note that we only wait if we are transferring playback.
554
+
555
+ # wait for the launched spotify app to transfer playback (or fail).
556
+ # this occurs when we receive any of the following zeroconf response
557
+ # messages: `transferSuccess`, `transferError`.
558
+
559
+ counter = 0
560
+ while counter < (timeoutTransfer + 1):
561
+ if (self.WaitForTransferComplete.wait(1)):
562
+ # syncronize access via lock, as we are accessing the collection.
563
+ with self._SpotifyConnectDevices_RLock:
564
+ scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
565
+ if (scDevice is not None):
566
+ response = scDevice.ZeroconfResponseInfo
567
+ if (response.ResponseSource == TYPE_TRANSFER_SUCCESS_RESPONSE):
568
+ break
569
+ raise SpotifyApiError("Spotify Cast App failed to receive playback transfer on Chromecast device \"%s\": %s" % (deviceName, response.ToString(False)), logsi=_logsi)
570
+ raise SpotifyApiError("Spotify Cast App failed to receive playback transfer on Chromecast device: unknown error", logsi=_logsi)
571
+ if (counter >= timeoutTransfer):
572
+ # syncronize access via lock, as we are accessing the collection.
573
+ with self._SpotifyConnectDevices_RLock:
574
+ scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
575
+ if (scDevice is not None):
576
+ 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)
577
+ counter += 1
578
+
579
+ # at this point we have received an `transferSuccess` from the Chromecast device, indicating
580
+ # that transfer of playback was a success; it should now be playing a track!
581
+
582
+ except SpotifyApiError: raise # pass handled exceptions on thru
583
+ except Exception as ex:
584
+
585
+ # format unhandled exception.
586
+ raise SpotifyApiError("Could not activate Spotify Cast application on Chromecast device \"%s\": %s" % (deviceName, str(ex)), ex, logsi=_logsi)
587
+
588
+ finally:
589
+
590
+ # reset activation and transfer wait events to initial state for next time;
591
+ # just in case they were not set in the wait logic above (e.g. error condition).
592
+ self.WaitForActivationComplete.set()
593
+ self.WaitForTransferComplete.set()
594
+
595
+ # trace.
596
+ _logsi.LeaveMethod(SILevel.Debug)
597
+
598
+
599
+ def AddDynamicDevice(
600
+ self,
601
+ device:Device,
602
+ ) -> None:
603
+ """
604
+ Adds a new dynamic SpotifyConnectDevice entry to the devices collection.
605
+
606
+ Args:
607
+ device (Device):
608
+ Device information to add.
609
+
610
+ Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
611
+ process, but still exist in the player device list. These are usually Spotify Player
612
+ client devices (e.g. mobile / web / desktop players) that utilize temporary device id's.
613
+
614
+ <details>
615
+ <summary>Sample Code</summary>
616
+ ```python
617
+ .. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/AddDynamicDevice.py
618
+ ```
619
+ </details>
620
+ """
621
+ # syncronize access via lock, as we are accessing the collection.
622
+ with self._SpotifyConnectDevices_RLock:
623
+
624
+ # validations.
625
+ if (device is None) or (not isinstance(device, Device)):
626
+ return
627
+
628
+ # build zeroconf discovery result with what we can.
629
+ discoverResult:ZeroconfDiscoveryResult = ZeroconfDiscoveryResult()
630
+ discoverResult.DeviceName = device.Name
631
+ discoverResult.HostIpAddresses.append('127.0.0.1')
632
+ discoverResult.Server = '127.0.0.1'
633
+ discoverResult.HostIpPort = 0
634
+ discoverResult.Name = device.Name
635
+ discoverResult.SpotifyConnectCPath = '/zc'
636
+ discoverResult.SpotifyConnectVersion = '1.0'
637
+
638
+ # build zeroconf getinfo result with what we can.
639
+ info:ZeroconfGetInfo = ZeroconfGetInfo()
640
+ info.ActiveUser = self.SpotifyClientInstance.SpotifyConnectLoginId
641
+ info.DeviceId = device.Id
642
+ info.DeviceType = device.Type
643
+ info.RemoteName = device.Name
644
+ info.SpotifyError = 0
645
+ info.Status = 101
646
+ info.StatusString = 'OK'
647
+
648
+ # populate brand info for popular devices.
649
+ if (device.Name == "Web Player (Chrome)"):
650
+ info.BrandDisplayName = 'Google'
651
+ info.ModelDisplayName = 'Chrome'
652
+ info.ProductId = 'Web Player'
653
+ elif (device.Name == "Web Player (Microsoft Edge)"):
654
+ info.BrandDisplayName = 'Microsoft'
655
+ info.ModelDisplayName = 'Edge'
656
+ info.ProductId = 'Web Player'
657
+ else:
658
+ info.BrandDisplayName = 'unknown'
659
+ info.ModelDisplayName = 'unknown'
660
+ info.ProductId = 'unknown'
661
+
662
+ # add the device.
663
+ scDevice:SpotifyConnectDevice = SpotifyConnectDevice()
664
+ scDevice.Id = info.DeviceId
665
+ scDevice.Name = info.RemoteName
666
+ scDevice.DeviceInfo = info
667
+ scDevice.DiscoveryResult = discoverResult
668
+ self._SpotifyConnectDevices.Items.append(scDevice)
669
+ self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
670
+
671
+ # sort devices collection by device name.
672
+ if (len(self._SpotifyConnectDevices.Items) > 0):
673
+ self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
674
+
675
+ # trace.
676
+ _logsi.LogVerbose("Added SpotifyConnectDevices collection entry: %s - %s" % (scDevice.Title, scDevice.DiscoveryResult.Description), colorValue=SIColors.ForestGreen)
677
+
678
+ # raise event.
679
+ self._RaiseDeviceAdded(scDevice)
680
+
681
+
682
+ def GetActiveDevice(
683
+ self,
684
+ refresh:bool=False,
685
+ ) -> SpotifyConnectDevice:
686
+ """
687
+ Returns the device currently marked as active if found; otherwise, null.
688
+
689
+ Args:
690
+ refresh (bool):
691
+ True to retrieve active device from Spotify Web API player state;
692
+ otherwise, False to retrieve active device from devices collection.
693
+
694
+ Returns:
695
+ The device currently marked as active if found; otherwise, null.
696
+
697
+ The object returned is a copy of the internal master device entry; any changes made
698
+ to the copy do not affect the internal master device entry.
699
+
700
+ <details>
701
+ <summary>Sample Code</summary>
702
+ ```python
703
+ .. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/GetActiveDevice.py
704
+ ```
705
+ </details>
706
+ """
707
+ # syncronize access via lock, as we are accessing the collection.
708
+ with self._SpotifyConnectDevices_RLock:
709
+
710
+ result:SpotifyConnectDevice = None
711
+
712
+ # if refresh selected then retrieve the active device from playerState.
713
+ if (refresh):
714
+ result = self.UpdateActiveDevice(None)
715
+ if (result is not None):
716
+ return result
717
+
718
+ # find active device in the devices collection.
719
+ for scDevice in self._SpotifyConnectDevices.Items:
720
+ if (scDevice.DeviceInfo is not None):
721
+ if (scDevice.IsActiveDevice):
722
+ _logsi.LogVerbose("Spotify Connect active device detected: %s" % (scDevice.Title))
723
+ result = copy.deepcopy(scDevice)
724
+ break
725
+
726
+ # returns null if no active device found.
727
+ return result
728
+
729
+
730
+ def GetDevice(
731
+ self,
732
+ value:str,
733
+ refreshDynamicDevices:bool=True,
734
+ ) -> SpotifyConnectDevice:
735
+ """
736
+ Returns a `SpotifyConnectDevice` object for the specified device name / id if found;
737
+ otherwise, an exception is returned indicating a device was not found.
738
+
739
+ Args:
740
+ value (str):
741
+ Spotify Connect device name or id value used to identify the device,
742
+ or null to retrieve the currently active Spotify Player device,
743
+ or an asterisk (*) to use the SpotifyPlus configured default device.
744
+ refreshDynamicDevices (bool):
745
+ True to refresh the list of dynamically added devices and the active player
746
+ state from real-time Spotify Web API data; otherwise, false to use the
747
+ current cached state and device list.
748
+ Default is True.
749
+
750
+ Returns:
751
+ A `SpotifyConnectDevice` object for the specified device name / id.
752
+
753
+ Raises:
754
+ SpotifyApiError:
755
+ If the specified device value was not found.
756
+ If there was no defaualt device found.
757
+ If an error occured while obtaining current Spotify Web API Player device data.
758
+
759
+ The object returned is a copy of the internal master device entry; any changes made
760
+ to the copy do not affect the internal master device entry.
761
+
762
+ <details>
763
+ <summary>Sample Code</summary>
764
+ ```python
765
+ .. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/GetDevice.py
766
+ ```
767
+ </details>
768
+ """
769
+ # syncronize access via lock, as we are accessing the collection.
770
+ with self._SpotifyConnectDevices_RLock:
771
+
772
+ apiMethodName:str = "GetDevice"
773
+ apiMethodParms:SIMethodParmListContext = None
774
+ scDevice:SpotifyConnectDevice = None
775
+ scActiveDevice:SpotifyConnectDevice = None
776
+
777
+ try:
778
+
779
+ # trace.
780
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
781
+ apiMethodParms.AppendKeyValue("value", value)
782
+ apiMethodParms.AppendKeyValue("refreshDynamicDevices", refreshDynamicDevices)
783
+ apiMethodParms.AppendKeyValue("SpotifyClient.DefaultDeviceId", self.SpotifyClientInstance.DefaultDeviceId)
784
+ _logsi.LogMethodParmList(SILevel.Verbose, "Getting Spotify Connect device instance for value \"%s\"" % (value), apiMethodParms)
785
+
786
+ # validations.
787
+ if (value is not None) and (not isinstance(value, str)):
788
+ value = None
789
+ if (not isinstance(refreshDynamicDevices, bool)):
790
+ refreshDynamicDevices = True
791
+ valueOriginal:str = value
792
+
793
+ # get currently active player device.
794
+ if (refreshDynamicDevices):
795
+ scActiveDevice = self.RefreshDynamicDevices()
796
+ else:
797
+ scActiveDevice = self.UpdateActiveDevice()
798
+
799
+ # was the active spotify player device value specified (e.g. null / empty string)?
800
+ if (value is None) or (value.strip() == ""):
801
+
802
+ # is there an active player device?
803
+ if (scActiveDevice is not None):
804
+ _logsi.LogObject(SILevel.Verbose, "Spotify Player device \"%s\" (%s) was found in the Spotify Connect Devices collection (by Active PlayerState)" % (scActiveDevice.Name, scActiveDevice.Id), scActiveDevice, excludeNonPublic=True)
805
+ return scActiveDevice
806
+
807
+ # was a default device value specified?
808
+ # if so, then use the `defaultDeviceId` value; if the `defaultDeviceId` value is not in the device list,
809
+ # then we will catch it with the `GetDeviceById` / `GetDeviceByName` logic.
810
+ # if `defaultDeviceId` was not configured, then use active player device (via playerstate).
811
+ # if `defaultDeviceId` value is not in the device list, then use active player device (via playerstate).
812
+ # if no active player device, then it's an exception!
813
+ if (value == "*"):
814
+
815
+ # get default device id value from the client.
816
+ defaultDeviceId:str = self.SpotifyClientInstance.DefaultDeviceId
817
+
818
+ # was a default device specified?
819
+ if (defaultDeviceId is None) or (defaultDeviceId.strip() == ""):
820
+ # is there an active player device?
821
+ if (scActiveDevice is not None):
822
+ _logsi.LogObject(SILevel.Verbose, "Spotify Player default device (*) was not configured; using active player device: %s" % (scActiveDevice.Title), scActiveDevice, excludeNonPublic=True)
823
+ return scActiveDevice
824
+ # if no default device and no active player, then it's an error!
825
+ raise SpotifyApiError("Spotify Player default device (*) was not configured, and there is no active Spotify Player device", logsi=_logsi)
826
+
827
+ # assign default device value.
828
+ _logsi.LogVerbose("Spotify Player device default \"%s\" will be used for selecting a device (by default *)" % (defaultDeviceId))
829
+ value = defaultDeviceId
830
+
831
+ # check for the device id in the devices collection.
832
+ scDevice = self._SpotifyConnectDevices.GetDeviceById(value)
833
+ if (scDevice is not None):
834
+ _logsi.LogObject(SILevel.Verbose, "Spotify Player device %s was found in the Spotify Connect Devices collection (by Id)" % (scDevice.Title), scDevice, excludeNonPublic=True)
835
+ return copy.deepcopy(scDevice)
836
+
837
+ # check for the device name in the devices collection.
838
+ scDevice = self._SpotifyConnectDevices.GetDeviceByName(value)
839
+ if (scDevice is not None):
840
+ _logsi.LogObject(SILevel.Verbose, "Spotify Player device %s was found in the Spotify Connect Devices collection (by Name)" % (scDevice.Title), scDevice, excludeNonPublic=True)
841
+ return copy.deepcopy(scDevice)
842
+
843
+ # is there an active player device?
844
+ if (scActiveDevice is not None):
845
+
846
+ # check if the active device is restricted; if it is, then it may not show up in the
847
+ # available device list of devices (common issue with Sonos devices). in this case,
848
+ # it WILL show up in the player state device property as the active device.
849
+ # note that the PlayerPlayState device is a name and not a device id.
850
+ if (scActiveDevice.IsRestricted):
851
+ value = ("" + value).lower()
852
+ if (scActiveDevice.Id.lower() == value) or (scActiveDevice.Name.lower() == value):
853
+ _logsi.LogObject(SILevel.Verbose, "Spotify Player device %s was found to be a restricted device (by Active PlayerState)" % (scDevice.Title), scDevice, excludeNonPublic=True)
854
+ return scActiveDevice
855
+
856
+ # if `defaultDeviceId` value is not in the device list, then use active player device.
857
+ if (valueOriginal == "*"):
858
+ _logsi.LogObject(SILevel.Verbose, "Spotify Player default device \"%s\" was not found in the Spotify Connect Devices collection; using active player device: %s" % (value, scActiveDevice.Title), scActiveDevice, excludeNonPublic=True)
859
+ return scActiveDevice
860
+
861
+ # at this point we could not resolve the device name / id!
862
+ raise SpotifyApiError("Spotify Player device \"%s\" was not found in the Spotify Connect Devices collection" % (value), logsi=_logsi)
863
+
864
+ except SpotifyApiError: raise # pass handled exceptions on thru
865
+ except Exception as ex:
866
+
867
+ # format unhandled exception.
868
+ raise SpotifyApiError("%s - Could not retrieve dynamic device list: %s" % (self.name, str(ex)), ex, logsi=_logsi)
869
+
870
+ finally:
871
+
872
+ # trace.
873
+ _logsi.LeaveMethod(SILevel.Debug)
874
+
875
+
876
+ def GetDevices(self,
877
+ ) -> SpotifyConnectDevices:
878
+ """
879
+ Returns a collection of available Spotify Connect devices.
880
+
881
+ The object returned is a copy of the internal master device list; any changes made
882
+ to the copy do not affect the internal master device list.
883
+
884
+ <details>
885
+ <summary>Sample Code</summary>
886
+ ```python
887
+ .. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/GetDevices.py
888
+ ```
889
+ </details>
890
+ """
891
+ result:SpotifyConnectDevices
892
+
893
+ # syncronize access via lock, as we are accessing the collection.
894
+ with self._SpotifyConnectDevices_RLock:
895
+
896
+ # creates a completely independent copy of the devices collection (no references),
897
+ # and return it to the caller.
898
+ result = copy.deepcopy(self._SpotifyConnectDevices)
899
+ return result
900
+
901
+
902
+ def GetPlayerDevice(
903
+ self,
904
+ value:str,
905
+ refresh:bool=False,
906
+ ) -> SpotifyConnectDevice:
907
+ """
908
+ Returns the device instance if it is currently in the Spotify Web API player device list;
909
+ otherwise, null.
910
+
911
+ Args:
912
+ value (str):
913
+ Spotify Connect device name or id value used to identify the device.
914
+ refresh (bool):
915
+ True to refresh player devices from Spotify Web API devices;
916
+ otherwise, False to retrieve player device from devices collection.
917
+
918
+ Returns:
919
+ The device instance if it is currently in the Spotify Web API player device list;
920
+ otherwise, null.
921
+
922
+ The object returned is a copy of the internal master device entry; any changes made
923
+ to the copy do not affect the internal master device entry.
924
+
925
+ <details>
926
+ <summary>Sample Code</summary>
927
+ ```python
928
+ .. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/GetPlayerDevice.py
929
+ ```
930
+ </details>
931
+ """
932
+ # syncronize access via lock, as we are accessing the collection.
933
+ with self._SpotifyConnectDevices_RLock:
934
+
935
+ result:SpotifyConnectDevice = None
936
+
937
+ # validations.
938
+ if (value is None):
939
+ return result
940
+
941
+ # if refresh selected then update player devices from Spotify Web API.
942
+ if (refresh):
943
+ self.UpdatePlayerDevices()
944
+
945
+ # prepare for compare.
946
+ value = "" + value.strip().lower()
947
+
948
+ # find specified player device in the devices collection.
949
+ for scDevice in self._SpotifyConnectDevices._Items:
950
+ if (scDevice.IsInDeviceList):
951
+ if (scDevice.Id.lower() == value) or (scDevice.Name.lower() == value):
952
+ _logsi.LogVerbose("Spotify Connect player device detected: %s" % (scDevice.Title))
953
+ result = copy.deepcopy(scDevice)
954
+ break
955
+
956
+ # returns null if no active device found.
957
+ return result
958
+
959
+
960
+ def GetSpotifyDeviceIDFromName(
961
+ self,
962
+ name:str
963
+ ) -> str:
964
+ """
965
+ Retrieve the Spotify deviceID from provided user-friendly name.
966
+ """
967
+ if (name is None):
968
+ name = ""
969
+
970
+ deviceId:str = hashlib.md5(name.encode()).hexdigest()
971
+ return deviceId
972
+
973
+
974
+ def RefreshDynamicDevices(
975
+ self,
976
+ ) -> SpotifyConnectDevice:
977
+ """
978
+ Refreshes dynamic Spotify Connect devices, and also determines the currently
979
+ active device (if any).
980
+
981
+ Returns:
982
+ A SpotifyConnectDevice object that contains the active device if one was found;
983
+ otherwise, null to indicate no active device.
984
+
985
+ Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
986
+ process, but exist in the player device list. These are usually Spotify Connect
987
+ web or mobile players with temporary device id's.
988
+
989
+ The object returned is a copy of the internal master device entry; any changes made
990
+ to the copy do not affect the internal master device entry.
991
+
992
+ <details>
993
+ <summary>Sample Code</summary>
994
+ ```python
995
+ .. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/RefreshDynamicDevices.py
996
+ ```
997
+ </details>
998
+ """
999
+ # syncronize access via lock, as we are accessing the collection.
1000
+ with self._SpotifyConnectDevices_RLock:
1001
+
1002
+ try:
1003
+
1004
+ # trace.
1005
+ apiMethodParms:SIMethodParmListContext = _logsi.EnterMethodParmList(SILevel.Debug)
1006
+ _logsi.LogMethodParmList(SILevel.Verbose, "Refreshing Spotify Connect dynamic device list", apiMethodParms)
1007
+
1008
+ # update player devices known to the Spotify Web API (e.g. dynamic devices).
1009
+ self.UpdatePlayerDevices()
1010
+
1011
+ # update the currently active device (if any).
1012
+ scDevice:SpotifyConnectDevice = self.UpdateActiveDevice()
1013
+
1014
+ # trace.
1015
+ if (scDevice is None):
1016
+ _logsi.LogVerbose("Spotify Player playstate active device is not present; no active device")
1017
+ else:
1018
+ _logsi.LogObject(SILevel.Verbose, "Spotify Player playstate active device instance %s" % (scDevice.Title), scDevice, excludeNonPublic=True)
1019
+
1020
+ # return real-time active Spotify Player device.
1021
+ return scDevice
1022
+
1023
+ except SpotifyApiError: raise # pass handled exceptions on thru
1024
+ except Exception as ex:
1025
+
1026
+ # format unhandled exception.
1027
+ raise SpotifyApiError("%s - Could not retrieve dynamic device list: %s" % (self.name, str(ex)), ex, logsi=_logsi)
1028
+
1029
+ finally:
1030
+
1031
+ # trace.
1032
+ _logsi.LeaveMethod(SILevel.Debug)
1033
+
1034
+
1035
+ def RemoveDevice(
1036
+ self,
1037
+ deviceId:str,
1038
+ dynamicDeviceOnly:bool=False
1039
+ ) -> SpotifyConnectDevice:
1040
+ """
1041
+ Removes existing device entry(s) from the devices collection by device id value.
1042
+
1043
+ Args:
1044
+ deviceId (str):
1045
+ Device id to remove.
1046
+ dynamicDeviceOnly (str):
1047
+ True to only remove the device if it's a dynamic device entry;
1048
+ otherwise, False to remove the device id regardless.
1049
+ Default is False.
1050
+
1051
+ Returns:
1052
+ A SpotifyConnectDevice object that contains the removed device if one was found;
1053
+ otherwise, null to indicate no device was removed.
1054
+
1055
+ It's possible to have multiple devices with the same id (e.g. both a dynamic and a
1056
+ zeroconf registered device).
1057
+
1058
+ Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
1059
+ process, but still exist in the player device list. These are usually Spotify Player
1060
+ client devices (e.g. mobile / web / desktop players) that utilize temporary device id's.
1061
+
1062
+ The object returned is a copy of the internal master device entry; any changes made
1063
+ to the copy do not affect the internal master device entry.
1064
+
1065
+ <details>
1066
+ <summary>Sample Code</summary>
1067
+ ```python
1068
+ .. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/AddDynamicDevice.py
1069
+ ```
1070
+ </details>
1071
+ """
1072
+ # syncronize access via lock, as we are accessing the collection.
1073
+ with self._SpotifyConnectDevices_RLock:
1074
+
1075
+ try:
1076
+
1077
+ # validations.
1078
+ if (not isinstance(dynamicDeviceOnly,bool)):
1079
+ dynamicDeviceOnly = False
1080
+
1081
+ # remove device from devices collection;
1082
+ # the reversed() function creates an iterator that traverses the list in reverse order.
1083
+ # this ensures that removing an element doesn't affect the indices of the subsequent
1084
+ # elements we're going to iterate over.
1085
+ deviceIdCompare:str = (deviceId or "").lower()
1086
+ scDevice:SpotifyConnectDevice
1087
+ for idx in reversed(range(len(self._SpotifyConnectDevices.Items))):
1088
+
1089
+ scDevice = self._SpotifyConnectDevices.Items[idx]
1090
+
1091
+ # did we find a match by device id?
1092
+ if (deviceIdCompare == (scDevice.Id or "").lower()):
1093
+
1094
+ isFound:bool = True
1095
+
1096
+ # get dynamic device status.
1097
+ isDynamic:bool = False
1098
+ if (scDevice.DiscoveryResult is not None):
1099
+ isDynamic = scDevice.DiscoveryResult.IsDynamicDevice
1100
+
1101
+ # are we checking for dynamic device? if so, and it's NOT dynamic,
1102
+ # then don't delete it!
1103
+ if (dynamicDeviceOnly) and (not isDynamic):
1104
+ isFound = False
1105
+
1106
+ # are we deleting this device?
1107
+ if (isFound):
1108
+
1109
+ # remove the device.
1110
+ scDevice:SpotifyConnectDevice = copy.deepcopy(self._SpotifyConnectDevices.Items.pop(idx))
1111
+ _logsi.LogVerbose("Removed SpotifyConnectDevices collection entry: %s - %s" % (scDevice.Title, scDevice.DiscoveryResult.Description), colorValue=SIColors.Orange)
1112
+
1113
+ # raise event.
1114
+ self._RaiseDeviceRemoved(scDevice)
1115
+
1116
+ except SpotifyApiError: raise # pass handled exceptions on thru
1117
+ except Exception as ex:
1118
+
1119
+ # format unhandled exception.
1120
+ raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format("RemoveDevice", str(ex)), ex, logsi=_logsi)
1121
+
1122
+
1123
+ def UpdatePlayerDevices(
1124
+ self,
1125
+ playerDevices:list[Device]=None,
1126
+ ) -> None:
1127
+ """
1128
+ Adds a list of dynamic player device entries to the devices collection, and removes any
1129
+ existing dynamic devices from the collection that are not in the `playerDevices` list.
1130
+
1131
+ Args:
1132
+ playerDevices (list[Device]):
1133
+ If specified, the list of current player devices obtained via a previous call to
1134
+ the Spotify Web API; otherwise, null to call the Spotify Web API to retrieve the list.
1135
+
1136
+ Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
1137
+ process, but still exist in the player device list. These are usually Spotify Player
1138
+ client devices (e.g. mobile / web / desktop players) that utilize temporary device id's.
1139
+
1140
+ <details>
1141
+ <summary>Sample Code</summary>
1142
+ ```python
1143
+ .. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/UpdatePlayerDevices.py
1144
+ ```
1145
+ </details>
1146
+ """
1147
+ # syncronize access via lock, as we are accessing the collection.
1148
+ with self._SpotifyConnectDevices_RLock:
1149
+
1150
+ # validations.
1151
+ if (playerDevices is not None) and (not isinstance(playerDevices, list)):
1152
+ return
1153
+
1154
+ # if playerDevices object not supplied, then go get them.
1155
+ if (playerDevices is None):
1156
+ playerDevices:list[Device] = self._SpotifyClientInstance.GetPlayerDevices(refresh=True)
1157
+
1158
+ # reset device list indicators, just in case any were disconnected and are not in the
1159
+ # current player device list.
1160
+ for scDevice in self._SpotifyConnectDevices.Items:
1161
+ scDevice.IsInDeviceList = False
1162
+
1163
+ # add dynamic devices.
1164
+ scDevice:SpotifyConnectDevice
1165
+ device:Device
1166
+ for device in playerDevices:
1167
+
1168
+ # is the player device present in the collection? if not, then add it.
1169
+ if (not self._SpotifyConnectDevices.ContainsDeviceId(device.Id)):
1170
+ self.AddDynamicDevice(device)
1171
+
1172
+ # is the device in the available device list for this user context?
1173
+ # if not, then set an indicator so we can re-activate it later if need be.
1174
+ for scDevice in self._SpotifyConnectDevices.Items:
1175
+ if (scDevice.DeviceInfo.DeviceId == device.Id):
1176
+ scDevice.IsInDeviceList = True
1177
+ break
1178
+
1179
+ # remove stale dynamic devices from results collection;
1180
+ # the reversed() function creates an iterator that traverses the list in reverse order.
1181
+ # this ensures that removing an element doesn't affect the indices of the subsequent
1182
+ # elements we're going to iterate over.
1183
+ for idx in reversed(range(len(self._SpotifyConnectDevices.Items))):
1184
+
1185
+ # only process dynamic devices.
1186
+ scDevice = self._SpotifyConnectDevices.Items[idx]
1187
+ if (scDevice.DiscoveryResult.IsDynamicDevice):
1188
+
1189
+ # process all player devices.
1190
+ wasFound:bool = False
1191
+ playerDevice:Device
1192
+ for playerDevice in playerDevices:
1193
+ if (scDevice.Id == playerDevice.Id):
1194
+ wasFound = True
1195
+ break
1196
+
1197
+ # if collection entry was not found in the player device list then remove it.
1198
+ if (not wasFound):
1199
+
1200
+ # remove the device.
1201
+ scDevice:SpotifyConnectDevice = copy.deepcopy(self._SpotifyConnectDevices.Items.pop(idx))
1202
+ _logsi.LogVerbose("Removed SpotifyConnectDevices collection entry: %s - %s" % (scDevice.Title, scDevice.DiscoveryResult.Description), colorValue=SIColors.Orange)
1203
+
1204
+ # raise event.
1205
+ self._RaiseDeviceRemoved(scDevice)
1206
+
1207
+
1208
+ def UpdateActiveDevice(
1209
+ self,
1210
+ playerState:PlayerPlayState=None
1211
+ ) -> SpotifyConnectDevice:
1212
+ """
1213
+ Updates the currently active device based on playerState.
1214
+
1215
+ Args:
1216
+ playerState (PlayerPlayState):
1217
+ Current player state obtained via a call to `GetPlayerPlaybackState` method
1218
+ which will be used to set the active device in the `Items` collection;
1219
+ otherwise, null to bypass active device set.
1220
+
1221
+ Returns:
1222
+ The currently active Spotify Player device instance if one was found; otherwise, null.
1223
+
1224
+ The Spotify Web API GetPlayerPlaybackState method is called to retrieve the active device
1225
+ information.
1226
+
1227
+ The object returned is a copy of the internal master device entry; any changes made
1228
+ to the copy do not affect the internal master device entry.
1229
+
1230
+ <details>
1231
+ <summary>Sample Code</summary>
1232
+ ```python
1233
+ .. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/UpdateActiveDevice.py
1234
+ ```
1235
+ </details>
1236
+ """
1237
+ # syncronize access via lock, as we are accessing the collection.
1238
+ with self._SpotifyConnectDevices_RLock:
1239
+
1240
+ result:SpotifyConnectDevice = None
1241
+
1242
+ # if playerState object not supplied, then go get it.
1243
+ if (playerState is None):
1244
+ playerState:PlayerPlayState = self._SpotifyClientInstance.GetPlayerPlaybackState()
1245
+
1246
+ # if valid object then process it.
1247
+ if (isinstance(playerState, PlayerPlayState)):
1248
+
1249
+ # we do this in case the active device is a "restricted" device; if it is, then it may
1250
+ # not show up as the active device (common issue with Sonos devices).
1251
+ # in this case, it WILL show up in the player state device property as the active device.
1252
+ # if it IS active, then we will set the active flag.
1253
+ # note that the PlayerPlayState device is a name and not a device id; it's device id
1254
+ # could also be set to null if it's a restricted device (e.g. Sonos).
1255
+ if playerState.Device is not None:
1256
+
1257
+ # reset active device status for all devices; set active device status if found.
1258
+ device = playerState.Device
1259
+ for scDevice in self._SpotifyConnectDevices.Items:
1260
+ scDevice.IsActiveDevice = False
1261
+ if (device.Name == scDevice.Name):
1262
+ _logsi.LogVerbose("Spotify Connect active device detected: %s" % (scDevice.Title))
1263
+ scDevice.IsActiveDevice = True
1264
+ if (playerState.Device is not None):
1265
+ scDevice.IsRestricted = playerState.Device.IsRestricted
1266
+ result = copy.deepcopy(scDevice)
1267
+
1268
+ # returns null if the `playerState` argument was not specified;
1269
+ # otherwise, the currently active Spotify Player device instance as determined by
1270
+ # the Spotify Player PlayState (could be null if no active device).
1271
+ return result
1272
+
1273
+
1274
+ def OnServiceInfoAddedUpdatedChromecast(
1275
+ self,
1276
+ zeroconfDiscoveryResult:ZeroconfDiscoveryResult,
1277
+ uuid:UUID,
1278
+ serviceName:str
1279
+ ) -> None:
1280
+ """
1281
+ Called when a Chromecast MDNS service record has been discovered
1282
+ or updated for a Chromecast device.
1283
+
1284
+ Args:
1285
+ zeroconfDiscoveryResult (ZeroconfDiscoveryResult):
1286
+ A `ZeroconfDiscoveryResult` object that contains discovery details.
1287
+ uuid (UUID):
1288
+ The cast's uuid, which is the dictionary key to find the chromecast
1289
+ metadata in CastBrowser.devices and CastBrowser.services collection.
1290
+ serviceName (str):
1291
+ First known MDNS service name or host:port.
1292
+
1293
+ This method will be called in a thread-safe manner, as the caller is using
1294
+ the `_Zeroconf_Lock` object to control access.
1295
+ """
1296
+ # syncronize access via lock, as we are accessing the collection.
1297
+ with self._SpotifyConnectDevices_RLock:
1298
+
1299
+ try:
1300
+
1301
+ # trace.
1302
+ _logsi.EnterMethod(SILevel.Debug)
1303
+
1304
+ # add / update discovered results by serviceinfo key (e.g. uuid) value.
1305
+ scDevice:SpotifyConnectDevice = None
1306
+ idx:int = self._SpotifyConnectDevices.GetDeviceIndexByDiscoveryName(zeroconfDiscoveryResult.Name)
1307
+ if (idx == -1):
1308
+
1309
+ # trace.
1310
+ _logsi.LogVerbose("Creating new SpotifyConnectDevice instance from CastInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
1311
+
1312
+ # connect to the device using CastInfo to get more details.
1313
+ castInfo:CastInfo = self._CastBrowser.services[uuid]
1314
+ _logsi.LogVerbose("Retrieving Chromecast device instance from CastInfo object \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
1315
+ castDevice:Chromecast = get_chromecast_from_cast_info(
1316
+ cast_info=castInfo,
1317
+ zconf=self._ZeroconfInstance,
1318
+ tries=2,
1319
+ retry_wait=0.5,
1320
+ timeout=5)
1321
+
1322
+ # wait for the device to become ready (5 seconds max).
1323
+ castDevice.wait(5)
1324
+ _logsi.LogObject(SILevel.Verbose, "Chromecast device instance \"%s\" (%s)" % (castInfo.friendly_name, str(uuid)), castDevice)
1325
+
1326
+ # create a new Spotify Connect Device instance.
1327
+ scDevice = SpotifyConnectDevice()
1328
+ scDevice.Name = zeroconfDiscoveryResult.DeviceName
1329
+ scDevice.Id = self.GetSpotifyDeviceIDFromName(castInfo.friendly_name)
1330
+
1331
+ # set zeroconf discovery result properties.
1332
+ scDevice.DiscoveryResult = zeroconfDiscoveryResult
1333
+
1334
+ # Spotify Connect GetInformation is called as part of the cast app launch process,
1335
+ # and cannot be performed yet.
1336
+
1337
+ # set device info properties that we know about; the rest will be filled in
1338
+ # when we connect to the device and the cast `getInfoResponse` is returned.
1339
+ scDevice.DeviceInfo = ZeroconfGetInfo()
1340
+ scDevice.DeviceInfo.DeviceId = scDevice.Id
1341
+ scDevice.DeviceInfo.DeviceType = "CastAudio"
1342
+ scDevice.DeviceInfo.BrandDisplayName = castInfo.manufacturer
1343
+ scDevice.DeviceInfo.ModelDisplayName = castInfo.model_name
1344
+ scDevice.DeviceInfo.RemoteName = castInfo.friendly_name
1345
+
1346
+ # did this device change from a dynamic device (e.g. non-zeroconf) to a zeroconf device?
1347
+ # this can happen if the device is initially added as a dynamic device, then the zeroconf
1348
+ # device is discovered. if so, we will remove the dynamic device entry.
1349
+ spDynamicDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceById(scDevice.DeviceInfo.DeviceId)
1350
+ if (spDynamicDevice is not None):
1351
+
1352
+ # trace.
1353
+ _logsi.LogVerbose("Chromecast Zeroconf is converting SpotifyConnectDevice entry %s from dynamic to zeroconf" % (spDynamicDevice.Title))
1354
+
1355
+ # copy real-time status dynamic properties to zerconf object.
1356
+ scDevice.IsActiveDevice = spDynamicDevice.IsActiveDevice
1357
+ scDevice.IsInDeviceList = spDynamicDevice.IsInDeviceList
1358
+
1359
+ # remove the dynamic device from the collection.
1360
+ self.RemoveDevice(spDynamicDevice.Id, dynamicDeviceOnly=True)
1361
+
1362
+ # add new Spotify Connect Device to devices collection.
1363
+ self._SpotifyConnectDevices.Items.append(scDevice)
1364
+ self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
1365
+
1366
+ # trace.
1367
+ _logsi.LogObject(SILevel.Verbose, "Chromecast Zeroconf added SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.ForestGreen)
1368
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
1369
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DiscoveryResult)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
1370
+
1371
+ # sort devices collection by device name.
1372
+ if (len(self._SpotifyConnectDevices.Items) > 0):
1373
+ self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
1374
+
1375
+ # raise event.
1376
+ self._RaiseDeviceAdded(scDevice)
1377
+
1378
+ else:
1379
+
1380
+ # trace.
1381
+ _logsi.LogDebug("Updating existing SpotifyConnectDevice instance from CastInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
1382
+
1383
+ # get existing Spotify Connect Device instance.
1384
+ scDevice = self._SpotifyConnectDevices.Items[idx]
1385
+
1386
+ # were any changes made to the zeroconf discovery results?
1387
+ if (not scDevice.DiscoveryResult.Equals(zeroconfDiscoveryResult)):
1388
+
1389
+ # set zeroconf discovery result properties.
1390
+ scDevice.DiscoveryResult = zeroconfDiscoveryResult
1391
+
1392
+ # update existing Spotify Connect Device in devices collection.
1393
+ self._SpotifyConnectDevices.Items[idx] = scDevice
1394
+ self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
1395
+
1396
+ # trace.
1397
+ _logsi.LogObject(SILevel.Verbose, "Chromecast Zeroconf updated SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.ForestGreen)
1398
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
1399
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DiscoveryResult)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
1400
+
1401
+ # sort devices collection by device name.
1402
+ if (len(self._SpotifyConnectDevices.Items) > 0):
1403
+ self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
1404
+
1405
+ # raise event.
1406
+ self._RaiseDeviceUpdated(scDevice)
1407
+
1408
+ else:
1409
+
1410
+ # trace.
1411
+ _logsi.LogObject(SILevel.Verbose, "SpotifyConnectDevice info: \"%s\" (ignored; DiscoveryResult did not change)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
1412
+
1413
+ except Exception as ex:
1414
+
1415
+ # trace.
1416
+ _logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
1417
+
1418
+ # ignore exceptions, as there is nothing we can do at this point.
1419
+
1420
+ finally:
1421
+
1422
+ # trace.
1423
+ _logsi.LeaveMethod(SILevel.Debug)
1424
+
1425
+
1426
+ def OnServiceInfoRemovedChromecast(
1427
+ self,
1428
+ zeroconfDiscoveryResult: ZeroconfDiscoveryResult,
1429
+ uuid:UUID,
1430
+ serviceName:str,
1431
+ castInfo:CastInfo
1432
+ ) -> None:
1433
+ """
1434
+ Called when a cast has been lost (MDNS info expired or host down).
1435
+
1436
+ Args:
1437
+ zeroconfDiscoveryResult (ZeroconfDiscoveryResult):
1438
+ A `ZeroconfDiscoveryResult` object that contains discovery details.
1439
+ uuid (UUID):
1440
+ The cast's uuid, which is the dictionary key to find the chromecast
1441
+ metadata in CastBrowser.devices collection.
1442
+ serviceName (str):
1443
+ Last valid MDNS service name or host:port.
1444
+ castInfo (CastInfo):
1445
+ CastInfo for the service to aid cleanup.
1446
+
1447
+ This could be called under the following conditions:
1448
+ - when a Chromecast device has logged out of zeroconf and left the network.
1449
+ - transfer playback failed (e.g. device restriction, etc).
1450
+
1451
+ This method will be called in a thread-safe manner, as the caller is using
1452
+ the `_Zeroconf_Lock` object to control access.
1453
+ """
1454
+ # syncronize access via lock, as we are accessing the collection.
1455
+ with self._SpotifyConnectDevices_RLock:
1456
+
1457
+ try:
1458
+
1459
+ # trace.
1460
+ _logsi.EnterMethod(SILevel.Debug)
1461
+
1462
+ # remove discovered results by serviceinfo key value.
1463
+ idx:int = self._SpotifyConnectDevices.GetDeviceIndexByDiscoveryName(zeroconfDiscoveryResult.Name)
1464
+ if (idx == -1):
1465
+
1466
+ # trace.
1467
+ _logsi.LogVerbose("SpotifyConnectDevice instance could not be found to remove: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
1468
+
1469
+ else:
1470
+
1471
+ # trace.
1472
+ _logsi.LogVerbose("Removing existing SpotifyConnectDevice instance from CastInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
1473
+
1474
+ # remove existing Spotify Connect Device instance from devices collection.
1475
+ scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.Items.pop(idx)
1476
+ self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
1477
+
1478
+ # trace.
1479
+ _logsi.LogObject(SILevel.Verbose, "Chromecast Zeroconf removed SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.DarkOrange)
1480
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
1481
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DiscoveryResult)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
1482
+
1483
+ # raise event.
1484
+ self._RaiseDeviceRemoved(scDevice)
1485
+
1486
+ except Exception as ex:
1487
+
1488
+ # trace.
1489
+ _logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
1490
+
1491
+ # ignore exceptions, as there is nothing we can do at this point.
1492
+
1493
+ finally:
1494
+
1495
+ # trace.
1496
+ _logsi.LeaveMethod(SILevel.Debug)
1497
+
1498
+
1499
+ def OnCastGetInfoResponseReceived(
1500
+ self,
1501
+ uuid:UUID,
1502
+ info:ZeroconfGetInfo
1503
+ ) -> None:
1504
+ """
1505
+ Called when the cast app task has received a Spotify Connect Zeroconf GetInfoResponse
1506
+ data structure.
1507
+
1508
+ Args:
1509
+ uuid (UUID):
1510
+ Spotify Connect device that was added.
1511
+ info (ZeroconfGetInfo):
1512
+ A `ZeroconfGetInfo` instance that contains the GetInfoResponse data.
1513
+
1514
+ This method allows us to update the SpotifyConnectDevice entry with the Spotify Connect
1515
+ Zeroconf device information.
1516
+ """
1517
+ # syncronize access via lock, as we are accessing the collection.
1518
+ with self._SpotifyConnectDevices_RLock:
1519
+
1520
+ # get device instance.
1521
+ scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(uuid))
1522
+ if (scDevice is not None):
1523
+
1524
+ # ZeroconfGetInfo class properties.
1525
+ # we use underscore property reference for those that don't have a property setter method.
1526
+ scDevice.DeviceInfo._AccountReq = info.AccountReq
1527
+ scDevice.DeviceInfo.ActiveUser = info.ActiveUser
1528
+ scDevice.DeviceInfo._Aliases = info.Aliases
1529
+ scDevice.DeviceInfo._Availability = info.Availability
1530
+ scDevice.DeviceInfo._ClientId = info.ClientId
1531
+ scDevice.DeviceInfo.DeviceId = info.DeviceId # same as zeroconf deviceId
1532
+ scDevice.DeviceInfo._GroupStatus = info.GroupStatus
1533
+ scDevice.DeviceInfo._LibraryVersion = info.LibraryVersion
1534
+ scDevice.DeviceInfo.ProductId = info.ProductId
1535
+ scDevice.DeviceInfo.PublicKey = info.PublicKey
1536
+ scDevice.DeviceInfo.RemoteName = info.RemoteName # same as zeroconf friendly_name
1537
+ scDevice.DeviceInfo._ResolverVersion = info.ResolverVersion
1538
+ scDevice.DeviceInfo._Scope = info.Scope
1539
+ scDevice.DeviceInfo._SupportedCapabilities = info.SupportedCapabilities
1540
+ scDevice.DeviceInfo._SupportedDrmMediaFormats = info.SupportedDrmMediaFormats
1541
+ scDevice.DeviceInfo._TokenType = info.TokenType
1542
+ scDevice.DeviceInfo._Version = info.Version
1543
+ scDevice.DeviceInfo._VoiceSupport = info.VoiceSupport
1544
+
1545
+ # we will leave the following properties with their current values as
1546
+ # set from the CastInfo instance, as they contain more consistent values
1547
+ # that properly describe the device.
1548
+ #scDevice.DeviceInfo.BrandDisplayName = info.BrandDisplayName
1549
+ #scDevice.DeviceInfo.DeviceType = info.DeviceType
1550
+ #scDevice.DeviceInfo.ModelDisplayName = info.ModelDisplayName
1551
+
1552
+ # ZeroconfResponse base class properties.
1553
+ scDevice.DeviceInfo._ResponseSource = info.ResponseSource
1554
+ scDevice.DeviceInfo.SpotifyError = info.SpotifyError
1555
+ scDevice.DeviceInfo.Status = info.Status
1556
+ scDevice.DeviceInfo.StatusString = info.StatusString
1557
+
1558
+ # trace.
1559
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
1560
+
1561
+
1562
+ def OnCastZeroconfResponseReceived(
1563
+ self,
1564
+ uuid:UUID,
1565
+ response:ZeroconfResponse,
1566
+ ) -> None:
1567
+ """
1568
+ Called when the cast app task has received a Spotify Connect ZeroconfResponse
1569
+ data structure.
1570
+
1571
+ Args:
1572
+ uuid (UUID):
1573
+ Spotify Connect device that was added.
1574
+ response (ZeroconfResponse):
1575
+ A `ZeroconfResponse` instance that contains the zeroconf response data.
1576
+
1577
+ This method allows us to update the SpotifyConnectDevice entry with the Spotify Connect
1578
+ Zeroconf response information.
1579
+ """
1580
+ # syncronize access via lock, as we are accessing the collection.
1581
+ with self._SpotifyConnectDevices_RLock:
1582
+
1583
+ # get device instance.
1584
+ scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(uuid))
1585
+ if (scDevice is not None):
1586
+
1587
+ # ZeroconfResponse class properties.
1588
+ scDevice.ZeroconfResponseInfo.ResponseSource = response.ResponseSource
1589
+ scDevice.ZeroconfResponseInfo.SpotifyError = response.SpotifyError
1590
+ scDevice.ZeroconfResponseInfo.Status = response.Status
1591
+ scDevice.ZeroconfResponseInfo.StatusString = response.StatusString
1592
+
1593
+ # trace.
1594
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (ZeroconfResponseInfo)" % (scDevice.Name), scDevice.ZeroconfResponseInfo, excludeNonPublic=True)
1595
+
1596
+ # indicate zeroconf response data was received.
1597
+ if (response.ResponseSource == TYPE_ADD_USER_RESPONSE) or (response.ResponseSource == TYPE_ADD_USER_ERROR) or (response.ResponseSource == TYPE_GET_INFO_ERROR):
1598
+ self.WaitForActivationComplete.set()
1599
+ elif (response.ResponseSource == TYPE_TRANSFER_SUCCESS_RESPONSE) or (response.ResponseSource == TYPE_TRANSFER_ERROR_RESPONSE):
1600
+ self.WaitForTransferComplete.set()
1601
+
1602
+
1603
+ def OnServiceInfoAddedUpdatedSpotifyConnect(
1604
+ self,
1605
+ zeroconfDiscoveryResult:ZeroconfDiscoveryResult
1606
+ ) -> None:
1607
+ """
1608
+ Called when a new Spotify Connect ServiceInfo record has been discovered
1609
+ or updated for a Spotify Connect device.
1610
+
1611
+ Args:
1612
+ zeroconfDiscoveryResult (ZeroconfDiscoveryResult):
1613
+ A `ZeroconfDiscoveryResult` object that contains discovery details.
1614
+
1615
+ This method will be called in a thread-safe manner, as the caller is using
1616
+ the `_Zeroconf_Lock` object to control access.
1617
+ """
1618
+ # syncronize access via lock, as we are accessing the collection.
1619
+ with self._SpotifyConnectDevices_RLock:
1620
+
1621
+ try:
1622
+
1623
+ # trace.
1624
+ _logsi.EnterMethod(SILevel.Debug)
1625
+
1626
+ # add / update discovered results by serviceinfo key value.
1627
+ # we use the serviceinfo NAME value (not KEY) since some manufacturers use random ip port
1628
+ # numbers, and the port number could have been updated. in this case, the name SHOULD remain the same.
1629
+ # note that a different serviceinfo name SHOULD be created for speaker groups; at least they are for BOSE devices.
1630
+ scDevice:SpotifyConnectDevice = None
1631
+ idx:int = self._SpotifyConnectDevices.GetDeviceIndexByDiscoveryName(zeroconfDiscoveryResult.Name)
1632
+ if (idx == -1):
1633
+
1634
+ # trace.
1635
+ _logsi.LogVerbose("Creating new SpotifyConnectDevice instance from ServiceInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
1636
+
1637
+ # create a new Spotify Connect Device instance.
1638
+ scDevice = SpotifyConnectDevice()
1639
+ scDevice.Name = zeroconfDiscoveryResult.DeviceName
1640
+
1641
+ # set zeroconf discovery result properties.
1642
+ scDevice.DiscoveryResult = zeroconfDiscoveryResult
1643
+
1644
+ # verify CPath property was present; if not, log a warning as the endpoint url will be invalid!
1645
+ if (zeroconfDiscoveryResult.SpotifyConnectCPath is None):
1646
+ _logsi.LogWarning("Spotify Connect device Zeroconf ServiceInfo data did not contain a CPath value: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
1647
+
1648
+ try:
1649
+
1650
+ # trace.
1651
+ _logsi.LogVerbose("Retrieving Spotify Connect device information: %s" % (zeroconfDiscoveryResult.Id))
1652
+
1653
+ # create connection object to retrieve spotify connect device information (via direct ip address).
1654
+ zconn:ZeroconfConnect = ZeroconfConnect(
1655
+ zeroconfDiscoveryResult.HostIpAddress,
1656
+ zeroconfDiscoveryResult.HostIpPort,
1657
+ zeroconfDiscoveryResult.SpotifyConnectCPath,
1658
+ zeroconfDiscoveryResult.SpotifyConnectVersion,
1659
+ useSSL=False,
1660
+ tokenStorageDir=self.SpotifyClientInstance.TokenStorageDir,
1661
+ tokenStorageFile=self.SpotifyClientInstance.TokenStorageFile
1662
+ )
1663
+
1664
+ # retrieve initial spotify connect device information (by ip address).
1665
+ scDevice.DeviceInfo = zconn.GetInformation()
1666
+ scDevice.Id = scDevice.DeviceInfo.DeviceId
1667
+ scDevice.Name = scDevice.DeviceInfo.RemoteName
1668
+
1669
+ except Exception as ex:
1670
+
1671
+ # trace.
1672
+ _logsi.LogVerbose("Could not retrieve Spotify Connect device information by ip address; retrying with DNS server alias \"%s\"" % (zeroconfDiscoveryResult.Server))
1673
+
1674
+ try:
1675
+
1676
+ # create connection object to retrieve spotify connect device information (via dns alias).
1677
+ zconn:ZeroconfConnect = ZeroconfConnect(
1678
+ zeroconfDiscoveryResult.Server,
1679
+ zeroconfDiscoveryResult.HostIpPort,
1680
+ zeroconfDiscoveryResult.SpotifyConnectCPath,
1681
+ zeroconfDiscoveryResult.SpotifyConnectVersion,
1682
+ useSSL=False,
1683
+ tokenStorageDir=self.SpotifyClientInstance.TokenStorageDir,
1684
+ tokenStorageFile=self.SpotifyClientInstance.TokenStorageFile
1685
+ )
1686
+
1687
+ # retrieve initial spotify connect device information (by dns alias).
1688
+ scDevice.DeviceInfo = zconn.GetInformation()
1689
+ scDevice.Id = scDevice.DeviceInfo.DeviceId
1690
+ scDevice.Name = scDevice.DeviceInfo.RemoteName
1691
+
1692
+ # update HostIpAddress in discovery result so it knows to use the alias
1693
+ # instead of the ip address.
1694
+ _logsi.LogVerbose("Spotify Connect Zeroconf GetInformation call for Instance Name \"%s\" (%s) was resolved using DNS Server alias; HostIpAddress will be updated with the resolved ip address" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Server))
1695
+ resolvedIpAddress = socket.gethostbyname(zeroconfDiscoveryResult.Server)
1696
+ zeroconfDiscoveryResult.HostIpAddress = resolvedIpAddress
1697
+
1698
+ except Exception as ex:
1699
+
1700
+ # trace.
1701
+ _logsi.LogException("Spotify Connect Zeroconf GetInformation call failed for device instance \"%s\" (ip=%s:%s): %s" % (scDevice.Name, zeroconfDiscoveryResult.HostIpAddress, zeroconfDiscoveryResult.HostIpPort, str(ex)), ex, logToSystemLogger=False)
1702
+
1703
+ # if Getinformation call failed, then default device information.
1704
+ scDevice.DeviceInfo = ZeroconfGetInfo()
1705
+ scDevice.DeviceInfo.DeviceId = "getInfoError"
1706
+ scDevice.DeviceInfo.RemoteName = zeroconfDiscoveryResult.DeviceName
1707
+ scDevice.DeviceInfo.ResponseSource = "spotifywebapiPython"
1708
+ scDevice.DeviceInfo.Status = 9999
1709
+ scDevice.DeviceInfo.StatusString = str(ex)
1710
+ scDevice.Id = scDevice.DeviceInfo.DeviceId
1711
+
1712
+ # did this device change from a dynamic device (e.g. non-zeroconf) to a zeroconf device?
1713
+ # this can happen if the device is initially added as a dynamic device, then the zeroconf
1714
+ # device is discovered. if so, we will remove the dynamic device entry.
1715
+ spDynamicDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceById(scDevice.DeviceInfo.DeviceId)
1716
+ if (spDynamicDevice is not None):
1717
+
1718
+ # trace.
1719
+ _logsi.LogVerbose("Spotify Connect Zeroconf is converting SpotifyConnectDevice entry %s from dynamic to zeroconf" % (spDynamicDevice.Title))
1720
+
1721
+ # copy real-time status dynamic properties to zerconf object.
1722
+ scDevice.IsActiveDevice = spDynamicDevice.IsActiveDevice
1723
+ scDevice.IsInDeviceList = spDynamicDevice.IsInDeviceList
1724
+
1725
+ # remove the dynamic device from the collection.
1726
+ self.RemoveDevice(spDynamicDevice.Id, dynamicDeviceOnly=True)
1727
+
1728
+ # add new Spotify Connect Device to devices collection.
1729
+ self._SpotifyConnectDevices.Items.append(scDevice)
1730
+ self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
1731
+
1732
+ # trace.
1733
+ _logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf added SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.ForestGreen)
1734
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
1735
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DiscoveryResult)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
1736
+
1737
+ # sort devices collection by device name.
1738
+ if (len(self._SpotifyConnectDevices.Items) > 0):
1739
+ self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
1740
+
1741
+ # raise event.
1742
+ self._RaiseDeviceAdded(scDevice)
1743
+
1744
+ else:
1745
+
1746
+ # trace.
1747
+ _logsi.LogDebug("Updating existing SpotifyConnectDevice instance from ServiceInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
1748
+
1749
+ # get existing Spotify Connect Device instance.
1750
+ scDevice = self._SpotifyConnectDevices.Items[idx]
1751
+
1752
+ # were any changes made to the zeroconf discovery results?
1753
+ if (not scDevice.DiscoveryResult.Equals(zeroconfDiscoveryResult)):
1754
+
1755
+ # set zeroconf discovery result properties.
1756
+ scDevice.DiscoveryResult = zeroconfDiscoveryResult
1757
+
1758
+ # update existing Spotify Connect Device in devices collection.
1759
+ self._SpotifyConnectDevices.Items[idx] = scDevice
1760
+ self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
1761
+
1762
+ # trace.
1763
+ _logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf updated SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.ForestGreen)
1764
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
1765
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DiscoveryResult)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
1766
+
1767
+ # sort devices collection by device name.
1768
+ if (len(self._SpotifyConnectDevices.Items) > 0):
1769
+ self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
1770
+
1771
+ # raise event.
1772
+ self._RaiseDeviceUpdated(scDevice)
1773
+
1774
+ else:
1775
+
1776
+ # trace.
1777
+ _logsi.LogObject(SILevel.Verbose, "SpotifyConnectDevice info: \"%s\" (ignored; DiscoveryResult did not change)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
1778
+
1779
+ except Exception as ex:
1780
+
1781
+ # trace.
1782
+ _logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
1783
+
1784
+ # ignore exceptions, as there is nothing we can do at this point.
1785
+
1786
+ finally:
1787
+
1788
+ # trace.
1789
+ _logsi.LeaveMethod(SILevel.Debug)
1790
+
1791
+
1792
+ def OnServiceInfoRemovedSpotifyConnect(
1793
+ self,
1794
+ zeroconfDiscoveryResult:ZeroconfDiscoveryResult
1795
+ ) -> None:
1796
+ """
1797
+ Called when an mDNS service is lost.
1798
+
1799
+ Args:
1800
+ zeroconfDiscoveryResult (ZeroconfDiscoveryResult):
1801
+ A `ZeroconfDiscoveryResult` object that contains discovery details.
1802
+
1803
+ This method will be called in a thread-safe manner, as the caller is using
1804
+ the `_Zeroconf_Lock` object to control access.
1805
+ """
1806
+ # syncronize access via lock, as we are accessing the collection.
1807
+ with self._SpotifyConnectDevices_RLock:
1808
+
1809
+ try:
1810
+
1811
+ # trace.
1812
+ _logsi.EnterMethod(SILevel.Debug)
1813
+
1814
+ # remove discovered results by serviceinfo key value.
1815
+ # we use the serviceinfo NAME value (not KEY) since that is the only information available on
1816
+ # a remove serviceinfo request.
1817
+ idx:int = self._SpotifyConnectDevices.GetDeviceIndexByDiscoveryName(zeroconfDiscoveryResult.Name)
1818
+ if (idx == -1):
1819
+
1820
+ # trace.
1821
+ _logsi.LogVerbose("SpotifyConnectDevice instance could not be found to remove: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
1822
+
1823
+ else:
1824
+
1825
+ # trace.
1826
+ _logsi.LogVerbose("Removing existing SpotifyConnectDevice instance from ServiceInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
1827
+
1828
+ # remove existing Spotify Connect Device instance from devices collection.
1829
+ scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.Items.pop(idx)
1830
+ self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
1831
+
1832
+ # trace.
1833
+ _logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf removed SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.DarkOrange)
1834
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
1835
+ _logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DiscoveryResult)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
1836
+
1837
+ # raise event.
1838
+ self._RaiseDeviceRemoved(scDevice)
1839
+
1840
+ except Exception as ex:
1841
+
1842
+ # trace.
1843
+ _logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
1844
+
1845
+ # ignore exceptions, as there is nothing we can do at this point.
1846
+
1847
+ finally:
1848
+
1849
+ # trace.
1850
+ _logsi.LeaveMethod(SILevel.Debug)
1851
+
1852
+
1853
+ def _RaiseDeviceAdded(
1854
+ self,
1855
+ device:SpotifyConnectDevice
1856
+ ) -> None:
1857
+ """
1858
+ Raises the `DeviceAdded` event.
1859
+
1860
+ Args:
1861
+ device (SpotifyConnectDevice):
1862
+ Spotify Connect device that was added.
1863
+ """
1864
+ try:
1865
+
1866
+ # raise event.
1867
+ args:SpotifyConnectDeviceEventArgs = SpotifyConnectDeviceEventArgs(device)
1868
+ self.DeviceAdded(self, args)
1869
+
1870
+ except Exception as ex:
1871
+
1872
+ # ignore exceptions.
1873
+ pass
1874
+
1875
+
1876
+ @abstractmethod
1877
+ def OnDeviceAdded(
1878
+ self,
1879
+ sender:object,
1880
+ e:SpotifyConnectDeviceEventArgs
1881
+ ) -> None:
1882
+ """
1883
+ Method that will handle the `DeviceAdded` event.
1884
+ Inheriting classes can override this method to handle the event.
1885
+
1886
+ Args:
1887
+ sender (object):
1888
+ The object which fired the event.
1889
+ e (SpotifyConnectDeviceEventArgs):
1890
+ Arguments that contain detailed information related to the event.
1891
+
1892
+ This event is fired when a Spotify Connect device has been added to the
1893
+ SpotifyConnectDevices collection.
1894
+
1895
+ This method will be called in a thread-safe manner, as the caller is using
1896
+ the `SpotifyConnectDevices_RLock` object to control access.
1897
+ """
1898
+ pass
1899
+
1900
+
1901
+ def _RaiseDeviceRemoved(
1902
+ self,
1903
+ device:SpotifyConnectDevice
1904
+ ) -> None:
1905
+ """
1906
+ Raises the `DeviceRemoved` event.
1907
+
1908
+ Args:
1909
+ device (SpotifyConnectDevice):
1910
+ Spotify Connect device that was removed.
1911
+ """
1912
+ try:
1913
+
1914
+ # raise event.
1915
+ args:SpotifyConnectDeviceEventArgs = SpotifyConnectDeviceEventArgs(device)
1916
+ self.DeviceRemoved(self, args)
1917
+
1918
+ except Exception as ex:
1919
+
1920
+ # ignore exceptions.
1921
+ pass
1922
+
1923
+
1924
+ @abstractmethod
1925
+ def OnDeviceRemoved(
1926
+ self,
1927
+ sender:object,
1928
+ e:SpotifyConnectDeviceEventArgs
1929
+ ) -> None:
1930
+ """
1931
+ Method that will handle the `DeviceRemoved` event.
1932
+ Inheriting classes can override this method to handle the event.
1933
+
1934
+ Args:
1935
+ sender (object):
1936
+ The object which fired the event.
1937
+ e (SpotifyConnectDeviceEventArgs):
1938
+ Arguments that contain detailed information related to the event.
1939
+
1940
+ This event is fired when a Spotify Connect device has been removed from the
1941
+ SpotifyConnectDevices collection.
1942
+
1943
+ This method will be called in a thread-safe manner, as the caller is using
1944
+ the `SpotifyConnectDevices_RLock` object to control access.
1945
+ """
1946
+ pass
1947
+
1948
+
1949
+ def _RaiseDeviceUpdated(
1950
+ self,
1951
+ device:SpotifyConnectDevice
1952
+ ) -> None:
1953
+ """
1954
+ Raises the `DeviceUpdated` event.
1955
+
1956
+ Args:
1957
+ device (SpotifyConnectDevice):
1958
+ Spotify Connect device that was updated.
1959
+ """
1960
+ try:
1961
+
1962
+ # raise event.
1963
+ args:SpotifyConnectDeviceEventArgs = SpotifyConnectDeviceEventArgs(device)
1964
+ self.DeviceUpdated(self, args)
1965
+
1966
+ except Exception as ex:
1967
+
1968
+ # ignore exceptions.
1969
+ pass
1970
+
1971
+
1972
+ @abstractmethod
1973
+ def OnDeviceUpdated(
1974
+ self,
1975
+ sender:object,
1976
+ e:SpotifyConnectDeviceEventArgs
1977
+ ) -> None:
1978
+ """
1979
+ Method that will handle the `DeviceUpdated` event.
1980
+ Inheriting classes can override this method to handle the event.
1981
+
1982
+ Args:
1983
+ sender (object):
1984
+ The object which fired the event.
1985
+ e (SpotifyConnectDeviceEventArgs):
1986
+ Arguments that contain detailed information related to the event.
1987
+
1988
+ This event is fired when a Spotify Connect device has been updated in the
1989
+ SpotifyConnectDevices collection.
1990
+
1991
+ This method will be called in a thread-safe manner, as the caller is using
1992
+ the `SpotifyConnectDevices_RLock` object to control access.
1993
+ """
1994
+ pass