spotifywebapiPython 1.0.148__py3-none-any.whl → 1.0.149__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.148.dist-info → spotifywebapiPython-1.0.149.dist-info}/METADATA +5 -9
- {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.149.dist-info}/RECORD +38 -29
- spotifywebapipython/__init__.py +0 -2
- spotifywebapipython/const.py +1 -1
- spotifywebapipython/docs/__init__.html +19 -4
- spotifywebapipython/docs/index.html +26 -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 +8 -8
- 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 +514 -741
- spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +3 -3
- spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +466 -0
- spotifywebapipython/models/spotifyconnectdevice.py +84 -4
- spotifywebapipython/models/spotifyconnectdevices.py +16 -307
- spotifywebapipython/models/zeroconfdiscoveryresult.py +0 -41
- spotifywebapipython/sautils.py +0 -1
- spotifywebapipython/spotifyclient.py +1384 -1835
- spotifywebapipython/spotifyconnect/__init__.py +19 -0
- spotifywebapipython/{spotifyconnectdeviceeventargs.py → spotifyconnect/spotifyconnectdeviceeventargs.py} +1 -1
- spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +1967 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +336 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +609 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +316 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +10 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconflistener.py +335 -0
- spotifywebapipython/spotifydiscovery.py +0 -1
- spotifywebapipython/spotifywebplayertoken.py +344 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +4 -52
- spotifywebapipython/zeroconfapi/zeroconfresponse.py +8 -0
- {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.149.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.149.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.149.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1967 @@
|
|
|
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.clear()
|
|
154
|
+
self.WaitForTransferComplete.clear()
|
|
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 still 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
|
+
# if Spotify Cast App is active on a cast device then request that it stop.
|
|
461
|
+
castAppTask:SpotifyConnectZeroconfCastAppTask = self._CastAppTasks.get(scDevice.DiscoveryResult.Key, None)
|
|
462
|
+
if (castAppTask is not None):
|
|
463
|
+
if (not castAppTask.is_alive()):
|
|
464
|
+
_logsi.LogVerbose("%s - Stopping Spotify Cast App task" % (self.name))
|
|
465
|
+
castAppTask.IsStopRequested = True
|
|
466
|
+
castAppTask.join()
|
|
467
|
+
_logsi.LogVerbose("%s - Spotify Cast App task was stopped successfully" % (self.name))
|
|
468
|
+
self._CastAppTasks.pop(scDevice.DiscoveryResult.Key, None)
|
|
469
|
+
|
|
470
|
+
# get CastInfo details of the specified device.
|
|
471
|
+
_logsi.LogVerbose("%s - Getting Chromecast information for device: \"%s\"" % (self.name, deviceName))
|
|
472
|
+
castInfo:CastInfo = self._CastBrowser.devices[UUID(scDevice.DiscoveryResult.Key)]
|
|
473
|
+
|
|
474
|
+
# connect to the device and build a Chromecast instance.
|
|
475
|
+
castDevice:Chromecast = get_chromecast_from_cast_info(
|
|
476
|
+
cast_info=castInfo,
|
|
477
|
+
zconf=self._ZeroconfInstance,
|
|
478
|
+
tries=2,
|
|
479
|
+
retry_wait=0.5,
|
|
480
|
+
timeout=3)
|
|
481
|
+
|
|
482
|
+
# wait for the device to become ready (5 seconds max).
|
|
483
|
+
castDevice.wait(5)
|
|
484
|
+
|
|
485
|
+
# start the Spotify Cast application on the Chromecast device.
|
|
486
|
+
# this will start the spotify app on the cast device.
|
|
487
|
+
# it will also call the `OnCastGetInfoResponseReceived` method when the SpotifyConnectZeroconfCastAppTask
|
|
488
|
+
# receives a `GetInfoResponse` block that contains Spotify Connect Zeroconf GetInformation data about the device.
|
|
489
|
+
# it will also call the `OnCastZeroconfResponseReceived` method when the SpotifyConnectZeroconfCastAppTask
|
|
490
|
+
# receives a `ZeroconfResponse` block that contains Spotify Connect Zeroconf Response data about the device.
|
|
491
|
+
_logsi.LogVerbose("%s - Starting Spotify Cast application on Chromecast device: \"%s\" (%s)" % (self.name, castDevice.name, str(castDevice.uuid)))
|
|
492
|
+
castAppTask:SpotifyConnectZeroconfCastAppTask = SpotifyConnectZeroconfCastAppTask(
|
|
493
|
+
castDevice,
|
|
494
|
+
self.SpotifyClientInstance,
|
|
495
|
+
self.OnCastGetInfoResponseReceived,
|
|
496
|
+
self.OnCastZeroconfResponseReceived,
|
|
497
|
+
transferPlayback
|
|
498
|
+
)
|
|
499
|
+
castAppTask.daemon = True
|
|
500
|
+
castAppTask.start()
|
|
501
|
+
|
|
502
|
+
# add cast app task to active tasks collection.
|
|
503
|
+
self._CastAppTasks[str(castInfo.uuid)] = castAppTask
|
|
504
|
+
|
|
505
|
+
# wait for the launched spotify app to activate the user (or fail).
|
|
506
|
+
# this occurs when we receive an `addUserResponse` or `addUserError` zeroconf response message.
|
|
507
|
+
|
|
508
|
+
counter = 0
|
|
509
|
+
while counter < (timeoutActivation + 1):
|
|
510
|
+
if (self.WaitForActivationComplete.wait(1)):
|
|
511
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
|
|
512
|
+
if (scDevice is not None):
|
|
513
|
+
response = scDevice.ZeroconfResponseInfo
|
|
514
|
+
if (response.ResponseSource == TYPE_ADD_USER_RESPONSE):
|
|
515
|
+
break
|
|
516
|
+
raise SpotifyApiError("Spotify Cast App could not be activated on Chromecast device \"%s\": %s" % (deviceName, response.ToString(False)), logsi=_logsi)
|
|
517
|
+
raise SpotifyApiError("Spotify Cast App could not activated on Chromecast device: unknown error", logsi=_logsi)
|
|
518
|
+
if (counter >= timeoutActivation):
|
|
519
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
|
|
520
|
+
if (scDevice is not None):
|
|
521
|
+
raise SpotifyApiError("Spotify Cast App activation timeout (%s seconds) was exceeded while trying to activate Chromecast device \"%s\"" % (timeoutActivation, deviceName), logsi=_logsi)
|
|
522
|
+
counter += 1
|
|
523
|
+
|
|
524
|
+
# at this point we have received an `adduserResponse` from the Chromecast device, indicating
|
|
525
|
+
# that it has launched the Spotify Cast App and that the user is logged in.
|
|
526
|
+
|
|
527
|
+
# was transfer playback specified?
|
|
528
|
+
if (transferPlayback == True):
|
|
529
|
+
|
|
530
|
+
# wait for the launched spotify app to activate the user (or fail).
|
|
531
|
+
# this occurs when we receive an `addUserResponse` or `addUserError` zeroconf response message.
|
|
532
|
+
|
|
533
|
+
counter = 0
|
|
534
|
+
while counter < (timeoutTransfer + 1):
|
|
535
|
+
if (self.WaitForTransferComplete.wait(1)):
|
|
536
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
|
|
537
|
+
if (scDevice is not None):
|
|
538
|
+
response = scDevice.ZeroconfResponseInfo
|
|
539
|
+
if (response.ResponseSource == TYPE_TRANSFER_SUCCESS_RESPONSE):
|
|
540
|
+
break
|
|
541
|
+
raise SpotifyApiError("Spotify Cast App failed to receive playback transfer on Chromecast device \"%s\": %s" % (deviceName, response.ToString(False)), logsi=_logsi)
|
|
542
|
+
raise SpotifyApiError("Spotify Cast App failed to receive playback transfer on Chromecast device: unknown error", logsi=_logsi)
|
|
543
|
+
if (counter >= timeoutTransfer):
|
|
544
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
|
|
545
|
+
if (scDevice is not None):
|
|
546
|
+
raise SpotifyApiError("Spotify Cast App transfer playback timeout (%s seconds) was exceeded while waiting for transfer of playback on Chromecast device \"%s\"" % (timeoutTransfer, deviceName), logsi=_logsi)
|
|
547
|
+
counter += 1
|
|
548
|
+
|
|
549
|
+
# at this point we have received an `transferSuccess` from the Chromecast device, indicating
|
|
550
|
+
# that transfer of playback was a success; it should now be playing a track!
|
|
551
|
+
|
|
552
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
553
|
+
except Exception as ex:
|
|
554
|
+
|
|
555
|
+
# format unhandled exception.
|
|
556
|
+
raise SpotifyApiError("Could not activate Spotify Cast application on Chromecast device \"%s\": %s" % (deviceName, str(ex)), ex, logsi=_logsi)
|
|
557
|
+
|
|
558
|
+
finally:
|
|
559
|
+
|
|
560
|
+
# trace.
|
|
561
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def AddDynamicDevice(
|
|
565
|
+
self,
|
|
566
|
+
device:Device,
|
|
567
|
+
) -> None:
|
|
568
|
+
"""
|
|
569
|
+
Adds a new dynamic SpotifyConnectDevice entry to the devices collection.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
device (Device):
|
|
573
|
+
Device information to add.
|
|
574
|
+
|
|
575
|
+
Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
|
|
576
|
+
process, but still exist in the player device list. These are usually Spotify Player
|
|
577
|
+
client devices (e.g. mobile / web / desktop players) that utilize temporary device id's.
|
|
578
|
+
|
|
579
|
+
<details>
|
|
580
|
+
<summary>Sample Code</summary>
|
|
581
|
+
```python
|
|
582
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/AddDynamicDevice.py
|
|
583
|
+
```
|
|
584
|
+
</details>
|
|
585
|
+
"""
|
|
586
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
587
|
+
with self._SpotifyConnectDevices_RLock:
|
|
588
|
+
|
|
589
|
+
# validations.
|
|
590
|
+
if (device is None) or (not isinstance(device, Device)):
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
# build zeroconf discovery result with what we can.
|
|
594
|
+
discoverResult:ZeroconfDiscoveryResult = ZeroconfDiscoveryResult()
|
|
595
|
+
discoverResult.DeviceName = device.Name
|
|
596
|
+
discoverResult.HostIpAddresses.append('127.0.0.1')
|
|
597
|
+
discoverResult.Server = '127.0.0.1'
|
|
598
|
+
discoverResult.HostIpPort = 0
|
|
599
|
+
discoverResult.Name = device.Name
|
|
600
|
+
discoverResult.SpotifyConnectCPath = '/zc'
|
|
601
|
+
discoverResult.SpotifyConnectVersion = '1.0'
|
|
602
|
+
|
|
603
|
+
# build zeroconf getinfo result with what we can.
|
|
604
|
+
info:ZeroconfGetInfo = ZeroconfGetInfo()
|
|
605
|
+
info.ActiveUser = self.SpotifyClientInstance.SpotifyConnectLoginId
|
|
606
|
+
info.DeviceId = device.Id
|
|
607
|
+
info.DeviceType = device.Type
|
|
608
|
+
info.RemoteName = device.Name
|
|
609
|
+
info.SpotifyError = 0
|
|
610
|
+
info.Status = 101
|
|
611
|
+
info.StatusString = 'OK'
|
|
612
|
+
|
|
613
|
+
# populate brand info for popular devices.
|
|
614
|
+
if (device.Name == "Web Player (Chrome)"):
|
|
615
|
+
info.BrandDisplayName = 'Google'
|
|
616
|
+
info.ModelDisplayName = 'Chrome'
|
|
617
|
+
info.ProductId = 'Web Player'
|
|
618
|
+
elif (device.Name == "Web Player (Microsoft Edge)"):
|
|
619
|
+
info.BrandDisplayName = 'Microsoft'
|
|
620
|
+
info.ModelDisplayName = 'Edge'
|
|
621
|
+
info.ProductId = 'Web Player'
|
|
622
|
+
else:
|
|
623
|
+
info.BrandDisplayName = 'unknown'
|
|
624
|
+
info.ModelDisplayName = 'unknown'
|
|
625
|
+
info.ProductId = 'unknown'
|
|
626
|
+
|
|
627
|
+
# add the device.
|
|
628
|
+
scDevice:SpotifyConnectDevice = SpotifyConnectDevice()
|
|
629
|
+
scDevice.Id = info.DeviceId
|
|
630
|
+
scDevice.Name = info.RemoteName
|
|
631
|
+
scDevice.DeviceInfo = info
|
|
632
|
+
scDevice.DiscoveryResult = discoverResult
|
|
633
|
+
self._SpotifyConnectDevices.Items.append(scDevice)
|
|
634
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
635
|
+
|
|
636
|
+
# sort devices collection by device name.
|
|
637
|
+
if (len(self._SpotifyConnectDevices.Items) > 0):
|
|
638
|
+
self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
|
|
639
|
+
|
|
640
|
+
# trace.
|
|
641
|
+
_logsi.LogVerbose("Added SpotifyConnectDevices collection entry: %s - %s" % (scDevice.Title, scDevice.DiscoveryResult.Description), colorValue=SIColors.ForestGreen)
|
|
642
|
+
|
|
643
|
+
# raise event.
|
|
644
|
+
self._RaiseDeviceAdded(scDevice)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def GetActiveDevice(
|
|
648
|
+
self,
|
|
649
|
+
refresh:bool=False,
|
|
650
|
+
) -> SpotifyConnectDevice:
|
|
651
|
+
"""
|
|
652
|
+
Returns the device currently marked as active if found; otherwise, null.
|
|
653
|
+
|
|
654
|
+
Args:
|
|
655
|
+
refresh (bool):
|
|
656
|
+
True to retrieve active device from Spotify Web API player state;
|
|
657
|
+
otherwise, False to retrieve active device from devices collection.
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
The device currently marked as active if found; otherwise, null.
|
|
661
|
+
|
|
662
|
+
The object returned is a copy of the internal master device entry; any changes made
|
|
663
|
+
to the copy do not affect the internal master device entry.
|
|
664
|
+
|
|
665
|
+
<details>
|
|
666
|
+
<summary>Sample Code</summary>
|
|
667
|
+
```python
|
|
668
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/GetActiveDevice.py
|
|
669
|
+
```
|
|
670
|
+
</details>
|
|
671
|
+
"""
|
|
672
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
673
|
+
with self._SpotifyConnectDevices_RLock:
|
|
674
|
+
|
|
675
|
+
result:SpotifyConnectDevice = None
|
|
676
|
+
|
|
677
|
+
# if refresh selected then retrieve the active device from playerState.
|
|
678
|
+
if (refresh):
|
|
679
|
+
result = self.UpdateActiveDevice(None)
|
|
680
|
+
if (result is not None):
|
|
681
|
+
return result
|
|
682
|
+
|
|
683
|
+
# find active device in the devices collection.
|
|
684
|
+
for scDevice in self._SpotifyConnectDevices.Items:
|
|
685
|
+
if (scDevice.DeviceInfo is not None):
|
|
686
|
+
if (scDevice.IsActiveDevice):
|
|
687
|
+
_logsi.LogVerbose("Spotify Connect active device detected: %s" % (scDevice.Title))
|
|
688
|
+
result = copy.deepcopy(scDevice)
|
|
689
|
+
break
|
|
690
|
+
|
|
691
|
+
# returns null if no active device found.
|
|
692
|
+
return result
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def GetDevice(
|
|
696
|
+
self,
|
|
697
|
+
value:str,
|
|
698
|
+
refreshDynamicDevices:bool=True,
|
|
699
|
+
) -> SpotifyConnectDevice:
|
|
700
|
+
"""
|
|
701
|
+
Returns a `SpotifyConnectDevice` object for the specified device name / id if found;
|
|
702
|
+
otherwise, an exception is returned indicating a device was not found.
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
value (str):
|
|
706
|
+
Spotify Connect device name or id value used to identify the device,
|
|
707
|
+
or null to retrieve the currently active Spotify Player device,
|
|
708
|
+
or an asterisk (*) to use the SpotifyPlus configured default device.
|
|
709
|
+
refreshDynamicDevices (bool):
|
|
710
|
+
True to refresh the list of dynamically added devices and the active player
|
|
711
|
+
state from real-time Spotify Web API data; otherwise, false to use the
|
|
712
|
+
current cached state and device list.
|
|
713
|
+
Default is True.
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
A `SpotifyConnectDevice` object for the specified device name / id.
|
|
717
|
+
|
|
718
|
+
Raises:
|
|
719
|
+
SpotifyApiError:
|
|
720
|
+
If the specified device value was not found.
|
|
721
|
+
If there was no defaualt device found.
|
|
722
|
+
If an error occured while obtaining current Spotify Web API Player device data.
|
|
723
|
+
|
|
724
|
+
The object returned is a copy of the internal master device entry; any changes made
|
|
725
|
+
to the copy do not affect the internal master device entry.
|
|
726
|
+
|
|
727
|
+
<details>
|
|
728
|
+
<summary>Sample Code</summary>
|
|
729
|
+
```python
|
|
730
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/GetDevice.py
|
|
731
|
+
```
|
|
732
|
+
</details>
|
|
733
|
+
"""
|
|
734
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
735
|
+
with self._SpotifyConnectDevices_RLock:
|
|
736
|
+
|
|
737
|
+
apiMethodName:str = "GetDevice"
|
|
738
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
739
|
+
scDevice:SpotifyConnectDevice = None
|
|
740
|
+
scActiveDevice:SpotifyConnectDevice = None
|
|
741
|
+
|
|
742
|
+
try:
|
|
743
|
+
|
|
744
|
+
# trace.
|
|
745
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
746
|
+
apiMethodParms.AppendKeyValue("value", value)
|
|
747
|
+
apiMethodParms.AppendKeyValue("refreshDynamicDevices", refreshDynamicDevices)
|
|
748
|
+
apiMethodParms.AppendKeyValue("SpotifyClient.DefaultDeviceId", self.SpotifyClientInstance.DefaultDeviceId)
|
|
749
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Getting Spotify Connect device instance for value \"%s\"" % (value), apiMethodParms)
|
|
750
|
+
|
|
751
|
+
# validations.
|
|
752
|
+
if (value is not None) and (not isinstance(value, str)):
|
|
753
|
+
value = None
|
|
754
|
+
if (not isinstance(refreshDynamicDevices, bool)):
|
|
755
|
+
refreshDynamicDevices = True
|
|
756
|
+
valueOriginal:str = value
|
|
757
|
+
|
|
758
|
+
# get currently active player device.
|
|
759
|
+
if (refreshDynamicDevices):
|
|
760
|
+
scActiveDevice = self.RefreshDynamicDevices()
|
|
761
|
+
else:
|
|
762
|
+
scActiveDevice = self.UpdateActiveDevice()
|
|
763
|
+
|
|
764
|
+
# was the active spotify player device value specified (e.g. null / empty string)?
|
|
765
|
+
if (value is None) or (value.strip() == ""):
|
|
766
|
+
|
|
767
|
+
# is there an active player device?
|
|
768
|
+
if (scActiveDevice is not None):
|
|
769
|
+
_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)
|
|
770
|
+
return scActiveDevice
|
|
771
|
+
|
|
772
|
+
# was a default device value specified?
|
|
773
|
+
# if so, then use the `defaultDeviceId` value; if the `defaultDeviceId` value is not in the device list,
|
|
774
|
+
# then we will catch it with the `GetDeviceById` / `GetDeviceByName` logic.
|
|
775
|
+
# if `defaultDeviceId` was not configured, then use active player device (via playerstate).
|
|
776
|
+
# if `defaultDeviceId` value is not in the device list, then use active player device (via playerstate).
|
|
777
|
+
# if no active player device, then it's an exception!
|
|
778
|
+
if (value == "*"):
|
|
779
|
+
|
|
780
|
+
# get default device id value from the client.
|
|
781
|
+
defaultDeviceId:str = self.SpotifyClientInstance.DefaultDeviceId
|
|
782
|
+
|
|
783
|
+
# was a default device specified?
|
|
784
|
+
if (defaultDeviceId is None) or (defaultDeviceId.strip() == ""):
|
|
785
|
+
# is there an active player device?
|
|
786
|
+
if (scActiveDevice is not None):
|
|
787
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Player default device (*) was not configured; using active player device: %s" % (scActiveDevice.Title), scActiveDevice, excludeNonPublic=True)
|
|
788
|
+
return scActiveDevice
|
|
789
|
+
# if no default device and no active player, then it's an error!
|
|
790
|
+
raise SpotifyApiError("Spotify Player default device (*) was not configured, and there is no active Spotify Player device", logsi=_logsi)
|
|
791
|
+
|
|
792
|
+
# assign default device value.
|
|
793
|
+
_logsi.LogVerbose("Spotify Player device default \"%s\" will be used for selecting a device (by default *)" % (defaultDeviceId))
|
|
794
|
+
value = defaultDeviceId
|
|
795
|
+
|
|
796
|
+
# check for the device id in the devices collection.
|
|
797
|
+
scDevice = self._SpotifyConnectDevices.GetDeviceById(value)
|
|
798
|
+
if (scDevice is not None):
|
|
799
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Player device %s was found in the Spotify Connect Devices collection (by Id)" % (scDevice.Title), scDevice, excludeNonPublic=True)
|
|
800
|
+
return copy.deepcopy(scDevice)
|
|
801
|
+
|
|
802
|
+
# check for the device name in the devices collection.
|
|
803
|
+
scDevice = self._SpotifyConnectDevices.GetDeviceByName(value)
|
|
804
|
+
if (scDevice is not None):
|
|
805
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Player device %s was found in the Spotify Connect Devices collection (by Name)" % (scDevice.Title), scDevice, excludeNonPublic=True)
|
|
806
|
+
return copy.deepcopy(scDevice)
|
|
807
|
+
|
|
808
|
+
# is there an active player device?
|
|
809
|
+
if (scActiveDevice is not None):
|
|
810
|
+
|
|
811
|
+
# check if the active device is restricted; if it is, then it may not show up in the
|
|
812
|
+
# available device list of devices (common issue with Sonos devices). in this case,
|
|
813
|
+
# it WILL show up in the player state device property as the active device.
|
|
814
|
+
# note that the PlayerPlayState device is a name and not a device id.
|
|
815
|
+
if (scActiveDevice.IsRestricted):
|
|
816
|
+
value = ("" + value).lower()
|
|
817
|
+
if (scActiveDevice.Id.lower() == value) or (scActiveDevice.Name.lower() == value):
|
|
818
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Player device %s was found to be a restricted device (by Active PlayerState)" % (scDevice.Title), scDevice, excludeNonPublic=True)
|
|
819
|
+
return scActiveDevice
|
|
820
|
+
|
|
821
|
+
# if `defaultDeviceId` value is not in the device list, then use active player device.
|
|
822
|
+
if (valueOriginal == "*"):
|
|
823
|
+
_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)
|
|
824
|
+
return scActiveDevice
|
|
825
|
+
|
|
826
|
+
# at this point we could not resolve the device name / id!
|
|
827
|
+
raise SpotifyApiError("Spotify Player device \"%s\" was not found in the Spotify Connect Devices collection" % (value), logsi=_logsi)
|
|
828
|
+
|
|
829
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
830
|
+
except Exception as ex:
|
|
831
|
+
|
|
832
|
+
# format unhandled exception.
|
|
833
|
+
raise SpotifyApiError("%s - Could not retrieve dynamic device list: %s" % (self.name, str(ex)), ex, logsi=_logsi)
|
|
834
|
+
|
|
835
|
+
finally:
|
|
836
|
+
|
|
837
|
+
# trace.
|
|
838
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def GetDevices(self,
|
|
842
|
+
) -> SpotifyConnectDevices:
|
|
843
|
+
"""
|
|
844
|
+
Returns a collection of available Spotify Connect devices.
|
|
845
|
+
|
|
846
|
+
The object returned is a copy of the internal master device list; any changes made
|
|
847
|
+
to the copy do not affect the internal master device list.
|
|
848
|
+
|
|
849
|
+
<details>
|
|
850
|
+
<summary>Sample Code</summary>
|
|
851
|
+
```python
|
|
852
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/GetDevices.py
|
|
853
|
+
```
|
|
854
|
+
</details>
|
|
855
|
+
"""
|
|
856
|
+
result:SpotifyConnectDevices
|
|
857
|
+
|
|
858
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
859
|
+
with self._SpotifyConnectDevices_RLock:
|
|
860
|
+
|
|
861
|
+
# creates a completely independent copy of the devices collection (no references),
|
|
862
|
+
# and return it to the caller.
|
|
863
|
+
result = copy.deepcopy(self._SpotifyConnectDevices)
|
|
864
|
+
return result
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def GetPlayerDevice(
|
|
868
|
+
self,
|
|
869
|
+
value:str,
|
|
870
|
+
refresh:bool=False,
|
|
871
|
+
) -> SpotifyConnectDevice:
|
|
872
|
+
"""
|
|
873
|
+
Returns the device instance if it is currently in the Spotify Web API player device list;
|
|
874
|
+
otherwise, null.
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
value (str):
|
|
878
|
+
Spotify Connect device name or id value used to identify the device.
|
|
879
|
+
refresh (bool):
|
|
880
|
+
True to refresh player devices from Spotify Web API devices;
|
|
881
|
+
otherwise, False to retrieve player device from devices collection.
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
The device instance if it is currently in the Spotify Web API player device list;
|
|
885
|
+
otherwise, null.
|
|
886
|
+
|
|
887
|
+
The object returned is a copy of the internal master device entry; any changes made
|
|
888
|
+
to the copy do not affect the internal master device entry.
|
|
889
|
+
|
|
890
|
+
<details>
|
|
891
|
+
<summary>Sample Code</summary>
|
|
892
|
+
```python
|
|
893
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/GetPlayerDevice.py
|
|
894
|
+
```
|
|
895
|
+
</details>
|
|
896
|
+
"""
|
|
897
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
898
|
+
with self._SpotifyConnectDevices_RLock:
|
|
899
|
+
|
|
900
|
+
result:SpotifyConnectDevice = None
|
|
901
|
+
|
|
902
|
+
# validations.
|
|
903
|
+
if (value is None):
|
|
904
|
+
return result
|
|
905
|
+
|
|
906
|
+
# if refresh selected then update player devices from Spotify Web API.
|
|
907
|
+
if (refresh):
|
|
908
|
+
self.UpdatePlayerDevices()
|
|
909
|
+
|
|
910
|
+
# prepare for compare.
|
|
911
|
+
value = "" + value.strip().lower()
|
|
912
|
+
|
|
913
|
+
# find specified player device in the devices collection.
|
|
914
|
+
for scDevice in self._SpotifyConnectDevices._Items:
|
|
915
|
+
if (scDevice.IsInDeviceList):
|
|
916
|
+
if (scDevice.Id.lower() == value) or (scDevice.Name.lower() == value):
|
|
917
|
+
_logsi.LogVerbose("Spotify Connect player device detected: %s" % (scDevice.Title))
|
|
918
|
+
result = copy.deepcopy(scDevice)
|
|
919
|
+
break
|
|
920
|
+
|
|
921
|
+
# returns null if no active device found.
|
|
922
|
+
return result
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def GetSpotifyDeviceIDFromName(
|
|
926
|
+
self,
|
|
927
|
+
name:str
|
|
928
|
+
) -> str:
|
|
929
|
+
"""
|
|
930
|
+
Retrieve the Spotify deviceID from provided user-friendly name.
|
|
931
|
+
"""
|
|
932
|
+
if (name is None):
|
|
933
|
+
name = ""
|
|
934
|
+
|
|
935
|
+
deviceId:str = hashlib.md5(name.encode()).hexdigest()
|
|
936
|
+
return deviceId
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def RefreshDynamicDevices(
|
|
940
|
+
self,
|
|
941
|
+
) -> SpotifyConnectDevice:
|
|
942
|
+
"""
|
|
943
|
+
Refreshes dynamic Spotify Connect devices, and also determines the currently
|
|
944
|
+
active device (if any).
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
A SpotifyConnectDevice object that contains the active device if one was found;
|
|
948
|
+
otherwise, null to indicate no active device.
|
|
949
|
+
|
|
950
|
+
Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
|
|
951
|
+
process, but exist in the player device list. These are usually Spotify Connect
|
|
952
|
+
web or mobile players with temporary device id's.
|
|
953
|
+
|
|
954
|
+
The object returned is a copy of the internal master device entry; any changes made
|
|
955
|
+
to the copy do not affect the internal master device entry.
|
|
956
|
+
|
|
957
|
+
<details>
|
|
958
|
+
<summary>Sample Code</summary>
|
|
959
|
+
```python
|
|
960
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/RefreshDynamicDevices.py
|
|
961
|
+
```
|
|
962
|
+
</details>
|
|
963
|
+
"""
|
|
964
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
965
|
+
with self._SpotifyConnectDevices_RLock:
|
|
966
|
+
|
|
967
|
+
try:
|
|
968
|
+
|
|
969
|
+
# trace.
|
|
970
|
+
apiMethodParms:SIMethodParmListContext = _logsi.EnterMethodParmList(SILevel.Debug)
|
|
971
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Refreshing Spotify Connect dynamic device list", apiMethodParms)
|
|
972
|
+
|
|
973
|
+
# update player devices known to the Spotify Web API (e.g. dynamic devices).
|
|
974
|
+
self.UpdatePlayerDevices()
|
|
975
|
+
|
|
976
|
+
# update the currently active device (if any).
|
|
977
|
+
scDevice:SpotifyConnectDevice = self.UpdateActiveDevice()
|
|
978
|
+
|
|
979
|
+
# trace.
|
|
980
|
+
if (scDevice is None):
|
|
981
|
+
_logsi.LogVerbose("Spotify Player playstate active device is not present; no active device")
|
|
982
|
+
else:
|
|
983
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Player playstate active device instance %s" % (scDevice.Title), scDevice, excludeNonPublic=True)
|
|
984
|
+
|
|
985
|
+
# return real-time active Spotify Player device.
|
|
986
|
+
return scDevice
|
|
987
|
+
|
|
988
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
989
|
+
except Exception as ex:
|
|
990
|
+
|
|
991
|
+
# format unhandled exception.
|
|
992
|
+
raise SpotifyApiError("%s - Could not retrieve dynamic device list: %s" % (self.name, str(ex)), ex, logsi=_logsi)
|
|
993
|
+
|
|
994
|
+
finally:
|
|
995
|
+
|
|
996
|
+
# trace.
|
|
997
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def RemoveDevice(
|
|
1001
|
+
self,
|
|
1002
|
+
deviceId:str,
|
|
1003
|
+
dynamicDeviceOnly:bool=False
|
|
1004
|
+
) -> SpotifyConnectDevice:
|
|
1005
|
+
"""
|
|
1006
|
+
Removes existing device entry(s) from the devices collection by device id value.
|
|
1007
|
+
|
|
1008
|
+
Args:
|
|
1009
|
+
deviceId (str):
|
|
1010
|
+
Device id to remove.
|
|
1011
|
+
dynamicDeviceOnly (str):
|
|
1012
|
+
True to only remove the device if it's a dynamic device entry;
|
|
1013
|
+
otherwise, False to remove the device id regardless.
|
|
1014
|
+
Default is False.
|
|
1015
|
+
|
|
1016
|
+
Returns:
|
|
1017
|
+
A SpotifyConnectDevice object that contains the removed device if one was found;
|
|
1018
|
+
otherwise, null to indicate no device was removed.
|
|
1019
|
+
|
|
1020
|
+
It's possible to have multiple devices with the same id (e.g. both a dynamic and a
|
|
1021
|
+
zeroconf registered device).
|
|
1022
|
+
|
|
1023
|
+
Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
|
|
1024
|
+
process, but still exist in the player device list. These are usually Spotify Player
|
|
1025
|
+
client devices (e.g. mobile / web / desktop players) that utilize temporary device id's.
|
|
1026
|
+
|
|
1027
|
+
The object returned is a copy of the internal master device entry; any changes made
|
|
1028
|
+
to the copy do not affect the internal master device entry.
|
|
1029
|
+
|
|
1030
|
+
<details>
|
|
1031
|
+
<summary>Sample Code</summary>
|
|
1032
|
+
```python
|
|
1033
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/AddDynamicDevice.py
|
|
1034
|
+
```
|
|
1035
|
+
</details>
|
|
1036
|
+
"""
|
|
1037
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1038
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1039
|
+
|
|
1040
|
+
try:
|
|
1041
|
+
|
|
1042
|
+
# validations.
|
|
1043
|
+
if (not isinstance(dynamicDeviceOnly,bool)):
|
|
1044
|
+
dynamicDeviceOnly = False
|
|
1045
|
+
|
|
1046
|
+
# remove device from devices collection;
|
|
1047
|
+
# the reversed() function creates an iterator that traverses the list in reverse order.
|
|
1048
|
+
# this ensures that removing an element doesn't affect the indices of the subsequent
|
|
1049
|
+
# elements we're going to iterate over.
|
|
1050
|
+
deviceIdCompare:str = (deviceId or "").lower()
|
|
1051
|
+
scDevice:SpotifyConnectDevice
|
|
1052
|
+
for idx in reversed(range(len(self._SpotifyConnectDevices.Items))):
|
|
1053
|
+
|
|
1054
|
+
scDevice = self._SpotifyConnectDevices.Items[idx]
|
|
1055
|
+
|
|
1056
|
+
# did we find a match by device id?
|
|
1057
|
+
if (deviceIdCompare == (scDevice.Id or "").lower()):
|
|
1058
|
+
|
|
1059
|
+
isFound:bool = True
|
|
1060
|
+
|
|
1061
|
+
# get dynamic device status.
|
|
1062
|
+
isDynamic:bool = False
|
|
1063
|
+
if (scDevice.DiscoveryResult is not None):
|
|
1064
|
+
isDynamic = scDevice.DiscoveryResult.IsDynamicDevice
|
|
1065
|
+
|
|
1066
|
+
# are we checking for dynamic device? if so, and it's NOT dynamic,
|
|
1067
|
+
# then don't delete it!
|
|
1068
|
+
if (dynamicDeviceOnly) and (not isDynamic):
|
|
1069
|
+
isFound = False
|
|
1070
|
+
|
|
1071
|
+
# are we deleting this device?
|
|
1072
|
+
if (isFound):
|
|
1073
|
+
|
|
1074
|
+
# remove the device.
|
|
1075
|
+
scDevice:SpotifyConnectDevice = copy.deepcopy(self._SpotifyConnectDevices.Items.pop(idx))
|
|
1076
|
+
_logsi.LogVerbose("Removed SpotifyConnectDevices collection entry: %s - %s" % (scDevice.Title, scDevice.DiscoveryResult.Description), colorValue=SIColors.Orange)
|
|
1077
|
+
|
|
1078
|
+
# raise event.
|
|
1079
|
+
self._RaiseDeviceRemoved(scDevice)
|
|
1080
|
+
|
|
1081
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
1082
|
+
except Exception as ex:
|
|
1083
|
+
|
|
1084
|
+
# format unhandled exception.
|
|
1085
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format("RemoveDevice", str(ex)), ex, logsi=_logsi)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def UpdatePlayerDevices(
|
|
1089
|
+
self,
|
|
1090
|
+
playerDevices:list[Device]=None,
|
|
1091
|
+
) -> None:
|
|
1092
|
+
"""
|
|
1093
|
+
Adds a list of dynamic player device entries to the devices collection, and removes any
|
|
1094
|
+
existing dynamic devices from the collection that are not in the `playerDevices` list.
|
|
1095
|
+
|
|
1096
|
+
Args:
|
|
1097
|
+
playerDevices (list[Device]):
|
|
1098
|
+
If specified, the list of current player devices obtained via a previous call to
|
|
1099
|
+
the Spotify Web API; otherwise, null to call the Spotify Web API to retrieve the list.
|
|
1100
|
+
|
|
1101
|
+
Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
|
|
1102
|
+
process, but still exist in the player device list. These are usually Spotify Player
|
|
1103
|
+
client devices (e.g. mobile / web / desktop players) that utilize temporary device id's.
|
|
1104
|
+
|
|
1105
|
+
<details>
|
|
1106
|
+
<summary>Sample Code</summary>
|
|
1107
|
+
```python
|
|
1108
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/UpdatePlayerDevices.py
|
|
1109
|
+
```
|
|
1110
|
+
</details>
|
|
1111
|
+
"""
|
|
1112
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1113
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1114
|
+
|
|
1115
|
+
# validations.
|
|
1116
|
+
if (playerDevices is not None) and (not isinstance(playerDevices, list)):
|
|
1117
|
+
return
|
|
1118
|
+
|
|
1119
|
+
# if playerDevices object not supplied, then go get them.
|
|
1120
|
+
if (playerDevices is None):
|
|
1121
|
+
playerDevices:list[Device] = self._SpotifyClientInstance.GetPlayerDevices(refresh=True)
|
|
1122
|
+
|
|
1123
|
+
# reset device list indicators, just in case any were disconnected and are not in the
|
|
1124
|
+
# current player device list.
|
|
1125
|
+
for scDevice in self._SpotifyConnectDevices.Items:
|
|
1126
|
+
scDevice.IsInDeviceList = False
|
|
1127
|
+
|
|
1128
|
+
# add dynamic devices.
|
|
1129
|
+
scDevice:SpotifyConnectDevice
|
|
1130
|
+
device:Device
|
|
1131
|
+
for device in playerDevices:
|
|
1132
|
+
|
|
1133
|
+
# is the player device present in the collection? if not, then add it.
|
|
1134
|
+
if (not self._SpotifyConnectDevices.ContainsDeviceId(device.Id)):
|
|
1135
|
+
self.AddDynamicDevice(device)
|
|
1136
|
+
|
|
1137
|
+
# is the device in the available device list for this user context?
|
|
1138
|
+
# if not, then set an indicator so we can re-activate it later if need be.
|
|
1139
|
+
for scDevice in self._SpotifyConnectDevices.Items:
|
|
1140
|
+
if (scDevice.DeviceInfo.DeviceId == device.Id):
|
|
1141
|
+
scDevice.IsInDeviceList = True
|
|
1142
|
+
break
|
|
1143
|
+
|
|
1144
|
+
# remove stale dynamic devices from results collection;
|
|
1145
|
+
# the reversed() function creates an iterator that traverses the list in reverse order.
|
|
1146
|
+
# this ensures that removing an element doesn't affect the indices of the subsequent
|
|
1147
|
+
# elements we're going to iterate over.
|
|
1148
|
+
for idx in reversed(range(len(self._SpotifyConnectDevices.Items))):
|
|
1149
|
+
|
|
1150
|
+
# only process dynamic devices.
|
|
1151
|
+
scDevice = self._SpotifyConnectDevices.Items[idx]
|
|
1152
|
+
if (scDevice.DiscoveryResult.IsDynamicDevice):
|
|
1153
|
+
|
|
1154
|
+
# process all player devices.
|
|
1155
|
+
wasFound:bool = False
|
|
1156
|
+
playerDevice:Device
|
|
1157
|
+
for playerDevice in playerDevices:
|
|
1158
|
+
if (scDevice.Id == playerDevice.Id):
|
|
1159
|
+
wasFound = True
|
|
1160
|
+
break
|
|
1161
|
+
|
|
1162
|
+
# if collection entry was not found in the player device list then remove it.
|
|
1163
|
+
if (not wasFound):
|
|
1164
|
+
|
|
1165
|
+
# remove the device.
|
|
1166
|
+
scDevice:SpotifyConnectDevice = copy.deepcopy(self._SpotifyConnectDevices.Items.pop(idx))
|
|
1167
|
+
_logsi.LogVerbose("Removed SpotifyConnectDevices collection entry: %s - %s" % (scDevice.Title, scDevice.DiscoveryResult.Description), colorValue=SIColors.Orange)
|
|
1168
|
+
|
|
1169
|
+
# raise event.
|
|
1170
|
+
self._RaiseDeviceRemoved(scDevice)
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def UpdateActiveDevice(
|
|
1174
|
+
self,
|
|
1175
|
+
playerState:PlayerPlayState=None
|
|
1176
|
+
) -> SpotifyConnectDevice:
|
|
1177
|
+
"""
|
|
1178
|
+
Updates the currently active device based on playerState.
|
|
1179
|
+
|
|
1180
|
+
Args:
|
|
1181
|
+
playerState (PlayerPlayState):
|
|
1182
|
+
Current player state obtained via a call to `GetPlayerPlaybackState` method
|
|
1183
|
+
which will be used to set the active device in the `Items` collection;
|
|
1184
|
+
otherwise, null to bypass active device set.
|
|
1185
|
+
|
|
1186
|
+
Returns:
|
|
1187
|
+
The currently active Spotify Player device instance if one was found; otherwise, null.
|
|
1188
|
+
|
|
1189
|
+
The Spotify Web API GetPlayerPlaybackState method is called to retrieve the active device
|
|
1190
|
+
information.
|
|
1191
|
+
|
|
1192
|
+
The object returned is a copy of the internal master device entry; any changes made
|
|
1193
|
+
to the copy do not affect the internal master device entry.
|
|
1194
|
+
|
|
1195
|
+
<details>
|
|
1196
|
+
<summary>Sample Code</summary>
|
|
1197
|
+
```python
|
|
1198
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/UpdateActiveDevice.py
|
|
1199
|
+
```
|
|
1200
|
+
</details>
|
|
1201
|
+
"""
|
|
1202
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1203
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1204
|
+
|
|
1205
|
+
result:SpotifyConnectDevice = None
|
|
1206
|
+
|
|
1207
|
+
# if playerState object not supplied, then go get it.
|
|
1208
|
+
if (playerState is None):
|
|
1209
|
+
playerState:PlayerPlayState = self._SpotifyClientInstance.GetPlayerPlaybackState()
|
|
1210
|
+
|
|
1211
|
+
# if valid object then process it.
|
|
1212
|
+
if (isinstance(playerState, PlayerPlayState)):
|
|
1213
|
+
|
|
1214
|
+
# we do this in case the active device is a "restricted" device; if it is, then it may
|
|
1215
|
+
# not show up as the active device (common issue with Sonos devices).
|
|
1216
|
+
# in this case, it WILL show up in the player state device property as the active device.
|
|
1217
|
+
# if it IS active, then we will set the active flag.
|
|
1218
|
+
# note that the PlayerPlayState device is a name and not a device id; it's device id
|
|
1219
|
+
# could also be set to null if it's a restricted device (e.g. Sonos).
|
|
1220
|
+
if playerState.Device is not None:
|
|
1221
|
+
|
|
1222
|
+
# reset active device status for all devices; set active device status if found.
|
|
1223
|
+
device = playerState.Device
|
|
1224
|
+
for scDevice in self._SpotifyConnectDevices.Items:
|
|
1225
|
+
scDevice.IsActiveDevice = False
|
|
1226
|
+
if (device.Name == scDevice.Name):
|
|
1227
|
+
_logsi.LogVerbose("Spotify Connect active device detected: %s" % (scDevice.Title))
|
|
1228
|
+
scDevice.IsActiveDevice = True
|
|
1229
|
+
if (playerState.Device is not None):
|
|
1230
|
+
scDevice.IsRestricted = playerState.Device.IsRestricted
|
|
1231
|
+
result = copy.deepcopy(scDevice)
|
|
1232
|
+
|
|
1233
|
+
# returns null if the `playerState` argument was not specified;
|
|
1234
|
+
# otherwise, the currently active Spotify Player device instance as determined by
|
|
1235
|
+
# the Spotify Player PlayState (could be null if no active device).
|
|
1236
|
+
return result
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
def OnServiceInfoAddedUpdatedChromecast(
|
|
1240
|
+
self,
|
|
1241
|
+
zeroconfDiscoveryResult:ZeroconfDiscoveryResult,
|
|
1242
|
+
uuid:UUID,
|
|
1243
|
+
serviceName:str
|
|
1244
|
+
) -> None:
|
|
1245
|
+
"""
|
|
1246
|
+
Called when a Chromecast MDNS service record has been discovered
|
|
1247
|
+
or updated for a Chromecast device.
|
|
1248
|
+
|
|
1249
|
+
Args:
|
|
1250
|
+
zeroconfDiscoveryResult (ZeroconfDiscoveryResult):
|
|
1251
|
+
A `ZeroconfDiscoveryResult` object that contains discovery details.
|
|
1252
|
+
uuid (UUID):
|
|
1253
|
+
The cast's uuid, which is the dictionary key to find the chromecast
|
|
1254
|
+
metadata in CastBrowser.devices and CastBrowser.services collection.
|
|
1255
|
+
serviceName (str):
|
|
1256
|
+
First known MDNS service name or host:port.
|
|
1257
|
+
|
|
1258
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
1259
|
+
the `_Zeroconf_Lock` object to control access.
|
|
1260
|
+
"""
|
|
1261
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1262
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1263
|
+
|
|
1264
|
+
try:
|
|
1265
|
+
|
|
1266
|
+
# trace.
|
|
1267
|
+
_logsi.EnterMethod(SILevel.Debug)
|
|
1268
|
+
|
|
1269
|
+
# add / update discovered results by serviceinfo key (e.g. uuid) value.
|
|
1270
|
+
scDevice:SpotifyConnectDevice = None
|
|
1271
|
+
idx:int = self._SpotifyConnectDevices.GetDeviceIndexByDiscoveryName(zeroconfDiscoveryResult.Name)
|
|
1272
|
+
if (idx == -1):
|
|
1273
|
+
|
|
1274
|
+
# trace.
|
|
1275
|
+
_logsi.LogVerbose("Creating new SpotifyConnectDevice instance from CastInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
1276
|
+
|
|
1277
|
+
# connect to the device using CastInfo to get more details.
|
|
1278
|
+
castInfo:CastInfo = self._CastBrowser.services[uuid]
|
|
1279
|
+
_logsi.LogVerbose("Retrieving Chromecast device instance from CastInfo object \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
1280
|
+
castDevice:Chromecast = get_chromecast_from_cast_info(
|
|
1281
|
+
cast_info=castInfo,
|
|
1282
|
+
zconf=self._ZeroconfInstance,
|
|
1283
|
+
tries=2,
|
|
1284
|
+
retry_wait=0.5,
|
|
1285
|
+
timeout=5)
|
|
1286
|
+
|
|
1287
|
+
# wait for the device to become ready (5 seconds max).
|
|
1288
|
+
castDevice.wait(5)
|
|
1289
|
+
_logsi.LogObject(SILevel.Verbose, "Chromecast device instance \"%s\" (%s)" % (castInfo.friendly_name, str(uuid)), castDevice)
|
|
1290
|
+
|
|
1291
|
+
# create a new Spotify Connect Device instance.
|
|
1292
|
+
scDevice = SpotifyConnectDevice()
|
|
1293
|
+
scDevice.Name = zeroconfDiscoveryResult.DeviceName
|
|
1294
|
+
scDevice.Id = self.GetSpotifyDeviceIDFromName(castInfo.friendly_name)
|
|
1295
|
+
|
|
1296
|
+
# set zeroconf discovery result properties.
|
|
1297
|
+
scDevice.DiscoveryResult = zeroconfDiscoveryResult
|
|
1298
|
+
|
|
1299
|
+
# Spotify Connect GetInformation is called as part of the cast app launch process,
|
|
1300
|
+
# and cannot be performed yet.
|
|
1301
|
+
|
|
1302
|
+
# set device info properties that we know about; the rest will be filled in
|
|
1303
|
+
# when we connect to the device and the cast `getInfoResponse` is returned.
|
|
1304
|
+
scDevice.DeviceInfo = ZeroconfGetInfo()
|
|
1305
|
+
scDevice.DeviceInfo.DeviceId = scDevice.Id
|
|
1306
|
+
scDevice.DeviceInfo.DeviceType = "CastAudio"
|
|
1307
|
+
scDevice.DeviceInfo.BrandDisplayName = castInfo.manufacturer
|
|
1308
|
+
scDevice.DeviceInfo.ModelDisplayName = castInfo.model_name
|
|
1309
|
+
scDevice.DeviceInfo.RemoteName = castInfo.friendly_name
|
|
1310
|
+
|
|
1311
|
+
# did this device change from a dynamic device (e.g. non-zeroconf) to a zeroconf device?
|
|
1312
|
+
# this can happen if the device is initially added as a dynamic device, then the zeroconf
|
|
1313
|
+
# device is discovered. if so, we will remove the dynamic device entry.
|
|
1314
|
+
spDynamicDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceById(scDevice.DeviceInfo.DeviceId)
|
|
1315
|
+
if (spDynamicDevice is not None):
|
|
1316
|
+
|
|
1317
|
+
# trace.
|
|
1318
|
+
_logsi.LogVerbose("Chromecast Zeroconf is converting SpotifyConnectDevice entry %s from dynamic to zeroconf" % (spDynamicDevice.Title))
|
|
1319
|
+
|
|
1320
|
+
# copy real-time status dynamic properties to zerconf object.
|
|
1321
|
+
scDevice.IsActiveDevice = spDynamicDevice.IsActiveDevice
|
|
1322
|
+
scDevice.IsInDeviceList = spDynamicDevice.IsInDeviceList
|
|
1323
|
+
|
|
1324
|
+
# remove the dynamic device from the collection.
|
|
1325
|
+
self.RemoveDevice(spDynamicDevice.Id, dynamicDeviceOnly=True)
|
|
1326
|
+
|
|
1327
|
+
# add new Spotify Connect Device to devices collection.
|
|
1328
|
+
self._SpotifyConnectDevices.Items.append(scDevice)
|
|
1329
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
1330
|
+
|
|
1331
|
+
# trace.
|
|
1332
|
+
_logsi.LogObject(SILevel.Verbose, "Chromecast Zeroconf added SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.ForestGreen)
|
|
1333
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
1334
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DiscoveryResult)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1335
|
+
|
|
1336
|
+
# sort devices collection by device name.
|
|
1337
|
+
if (len(self._SpotifyConnectDevices.Items) > 0):
|
|
1338
|
+
self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
|
|
1339
|
+
|
|
1340
|
+
# raise event.
|
|
1341
|
+
self._RaiseDeviceAdded(scDevice)
|
|
1342
|
+
|
|
1343
|
+
else:
|
|
1344
|
+
|
|
1345
|
+
# trace.
|
|
1346
|
+
_logsi.LogDebug("Updating existing SpotifyConnectDevice instance from CastInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
1347
|
+
|
|
1348
|
+
# get existing Spotify Connect Device instance.
|
|
1349
|
+
scDevice = self._SpotifyConnectDevices.Items[idx]
|
|
1350
|
+
|
|
1351
|
+
# were any changes made to the zeroconf discovery results?
|
|
1352
|
+
if (not scDevice.DiscoveryResult.Equals(zeroconfDiscoveryResult)):
|
|
1353
|
+
|
|
1354
|
+
# set zeroconf discovery result properties.
|
|
1355
|
+
scDevice.DiscoveryResult = zeroconfDiscoveryResult
|
|
1356
|
+
|
|
1357
|
+
# update existing Spotify Connect Device in devices collection.
|
|
1358
|
+
self._SpotifyConnectDevices.Items[idx] = scDevice
|
|
1359
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
1360
|
+
|
|
1361
|
+
# trace.
|
|
1362
|
+
_logsi.LogObject(SILevel.Verbose, "Chromecast Zeroconf updated SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.ForestGreen)
|
|
1363
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
1364
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DiscoveryResult)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1365
|
+
|
|
1366
|
+
# sort devices collection by device name.
|
|
1367
|
+
if (len(self._SpotifyConnectDevices.Items) > 0):
|
|
1368
|
+
self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
|
|
1369
|
+
|
|
1370
|
+
# raise event.
|
|
1371
|
+
self._RaiseDeviceUpdated(scDevice)
|
|
1372
|
+
|
|
1373
|
+
else:
|
|
1374
|
+
|
|
1375
|
+
# trace.
|
|
1376
|
+
_logsi.LogObject(SILevel.Verbose, "SpotifyConnectDevice info: \"%s\" (ignored; DiscoveryResult did not change)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1377
|
+
|
|
1378
|
+
except Exception as ex:
|
|
1379
|
+
|
|
1380
|
+
# trace.
|
|
1381
|
+
_logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
1382
|
+
|
|
1383
|
+
# ignore exceptions, as there is nothing we can do at this point.
|
|
1384
|
+
|
|
1385
|
+
finally:
|
|
1386
|
+
|
|
1387
|
+
# trace.
|
|
1388
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
def OnServiceInfoRemovedChromecast(
|
|
1392
|
+
self,
|
|
1393
|
+
zeroconfDiscoveryResult: ZeroconfDiscoveryResult,
|
|
1394
|
+
uuid:UUID,
|
|
1395
|
+
serviceName:str,
|
|
1396
|
+
castInfo:CastInfo
|
|
1397
|
+
) -> None:
|
|
1398
|
+
"""
|
|
1399
|
+
Called when a cast has been lost (MDNS info expired or host down).
|
|
1400
|
+
|
|
1401
|
+
Args:
|
|
1402
|
+
zeroconfDiscoveryResult (ZeroconfDiscoveryResult):
|
|
1403
|
+
A `ZeroconfDiscoveryResult` object that contains discovery details.
|
|
1404
|
+
uuid (UUID):
|
|
1405
|
+
The cast's uuid, which is the dictionary key to find the chromecast
|
|
1406
|
+
metadata in CastBrowser.devices collection.
|
|
1407
|
+
serviceName (str):
|
|
1408
|
+
Last valid MDNS service name or host:port.
|
|
1409
|
+
castInfo (CastInfo):
|
|
1410
|
+
CastInfo for the service to aid cleanup.
|
|
1411
|
+
|
|
1412
|
+
This could be called under the following conditions:
|
|
1413
|
+
- when a Chromecast device has logged out of zeroconf and left the network.
|
|
1414
|
+
- transfer playback failed (e.g. device restriction, etc).
|
|
1415
|
+
|
|
1416
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
1417
|
+
the `_Zeroconf_Lock` object to control access.
|
|
1418
|
+
"""
|
|
1419
|
+
|
|
1420
|
+
# TODO - research this some more!
|
|
1421
|
+
# saw the following logged to the console about 4 minutes after I unplugged the speaker!
|
|
1422
|
+
# it also DID NOT show up in the SI trace!
|
|
1423
|
+
# "[Nest Audio 01(192.168.1.95):8009] Failed to connect. No retries"
|
|
1424
|
+
# ??? we don't want to remove the device, since it's just disconnecting and not necessarily
|
|
1425
|
+
# removing itself from the network.
|
|
1426
|
+
|
|
1427
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1428
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1429
|
+
|
|
1430
|
+
try:
|
|
1431
|
+
|
|
1432
|
+
# trace.
|
|
1433
|
+
_logsi.EnterMethod(SILevel.Debug)
|
|
1434
|
+
|
|
1435
|
+
# remove discovered results by serviceinfo key value.
|
|
1436
|
+
idx:int = self._SpotifyConnectDevices.GetDeviceIndexByDiscoveryName(zeroconfDiscoveryResult.Name)
|
|
1437
|
+
if (idx == -1):
|
|
1438
|
+
|
|
1439
|
+
# trace.
|
|
1440
|
+
_logsi.LogVerbose("SpotifyConnectDevice instance could not be found to remove: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
1441
|
+
|
|
1442
|
+
else:
|
|
1443
|
+
|
|
1444
|
+
# trace.
|
|
1445
|
+
_logsi.LogVerbose("Removing existing SpotifyConnectDevice instance from CastInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
1446
|
+
|
|
1447
|
+
# remove existing Spotify Connect Device instance from devices collection.
|
|
1448
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.Items.pop(idx)
|
|
1449
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
1450
|
+
|
|
1451
|
+
# trace.
|
|
1452
|
+
_logsi.LogObject(SILevel.Verbose, "Chromecast Zeroconf removed SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.DarkOrange)
|
|
1453
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
1454
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DiscoveryResult)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1455
|
+
|
|
1456
|
+
# raise event.
|
|
1457
|
+
self._RaiseDeviceRemoved(scDevice)
|
|
1458
|
+
|
|
1459
|
+
except Exception as ex:
|
|
1460
|
+
|
|
1461
|
+
# trace.
|
|
1462
|
+
_logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
1463
|
+
|
|
1464
|
+
# ignore exceptions, as there is nothing we can do at this point.
|
|
1465
|
+
|
|
1466
|
+
finally:
|
|
1467
|
+
|
|
1468
|
+
# trace.
|
|
1469
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
def OnCastGetInfoResponseReceived(
|
|
1473
|
+
self,
|
|
1474
|
+
uuid:UUID,
|
|
1475
|
+
info:ZeroconfGetInfo
|
|
1476
|
+
) -> None:
|
|
1477
|
+
"""
|
|
1478
|
+
Called when the cast app task has received a Spotify Connect Zeroconf GetInfoResponse
|
|
1479
|
+
data structure.
|
|
1480
|
+
|
|
1481
|
+
Args:
|
|
1482
|
+
uuid (UUID):
|
|
1483
|
+
Spotify Connect device that was added.
|
|
1484
|
+
info (ZeroconfGetInfo):
|
|
1485
|
+
A `ZeroconfGetInfo` instance that contains the GetInfoResponse data.
|
|
1486
|
+
|
|
1487
|
+
This method allows us to update the SpotifyConnectDevice entry with the Spotify Connect
|
|
1488
|
+
Zeroconf device information.
|
|
1489
|
+
"""
|
|
1490
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1491
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1492
|
+
|
|
1493
|
+
# get device instance.
|
|
1494
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(uuid))
|
|
1495
|
+
if (scDevice is not None):
|
|
1496
|
+
|
|
1497
|
+
# ZeroconfGetInfo class properties.
|
|
1498
|
+
# we use underscore property reference for those that don't have a property setter method.
|
|
1499
|
+
scDevice.DeviceInfo._AccountReq = info.AccountReq
|
|
1500
|
+
scDevice.DeviceInfo.ActiveUser = info.ActiveUser
|
|
1501
|
+
scDevice.DeviceInfo._Aliases = info.Aliases
|
|
1502
|
+
scDevice.DeviceInfo._Availability = info.Availability
|
|
1503
|
+
scDevice.DeviceInfo._ClientId = info.ClientId
|
|
1504
|
+
scDevice.DeviceInfo.DeviceId = info.DeviceId # same as zeroconf deviceId
|
|
1505
|
+
scDevice.DeviceInfo._GroupStatus = info.GroupStatus
|
|
1506
|
+
scDevice.DeviceInfo._LibraryVersion = info.LibraryVersion
|
|
1507
|
+
scDevice.DeviceInfo.ProductId = info.ProductId
|
|
1508
|
+
scDevice.DeviceInfo.PublicKey = info.PublicKey
|
|
1509
|
+
scDevice.DeviceInfo.RemoteName = info.RemoteName # same as zeroconf friendly_name
|
|
1510
|
+
scDevice.DeviceInfo._ResolverVersion = info.ResolverVersion
|
|
1511
|
+
scDevice.DeviceInfo._Scope = info.Scope
|
|
1512
|
+
scDevice.DeviceInfo._SupportedCapabilities = info.SupportedCapabilities
|
|
1513
|
+
scDevice.DeviceInfo._SupportedDrmMediaFormats = info.SupportedDrmMediaFormats
|
|
1514
|
+
scDevice.DeviceInfo._TokenType = info.TokenType
|
|
1515
|
+
scDevice.DeviceInfo._Version = info.Version
|
|
1516
|
+
scDevice.DeviceInfo._VoiceSupport = info.VoiceSupport
|
|
1517
|
+
|
|
1518
|
+
# we will leave the following properties with their current values as
|
|
1519
|
+
# set from the CastInfo instance, as they contain more consistent values
|
|
1520
|
+
# that properly describe the device.
|
|
1521
|
+
#scDevice.DeviceInfo.BrandDisplayName = info.BrandDisplayName
|
|
1522
|
+
#scDevice.DeviceInfo.DeviceType = info.DeviceType
|
|
1523
|
+
#scDevice.DeviceInfo.ModelDisplayName = info.ModelDisplayName
|
|
1524
|
+
|
|
1525
|
+
# ZeroconfResponse base class properties.
|
|
1526
|
+
scDevice.DeviceInfo._ResponseSource = info.ResponseSource
|
|
1527
|
+
scDevice.DeviceInfo.SpotifyError = info.SpotifyError
|
|
1528
|
+
scDevice.DeviceInfo.Status = info.Status
|
|
1529
|
+
scDevice.DeviceInfo.StatusString = info.StatusString
|
|
1530
|
+
|
|
1531
|
+
# trace.
|
|
1532
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
def OnCastZeroconfResponseReceived(
|
|
1536
|
+
self,
|
|
1537
|
+
uuid:UUID,
|
|
1538
|
+
response:ZeroconfResponse,
|
|
1539
|
+
) -> None:
|
|
1540
|
+
"""
|
|
1541
|
+
Called when the cast app task has received a Spotify Connect ZeroconfResponse
|
|
1542
|
+
data structure.
|
|
1543
|
+
|
|
1544
|
+
Args:
|
|
1545
|
+
uuid (UUID):
|
|
1546
|
+
Spotify Connect device that was added.
|
|
1547
|
+
response (ZeroconfResponse):
|
|
1548
|
+
A `ZeroconfResponse` instance that contains the zeroconf response data.
|
|
1549
|
+
|
|
1550
|
+
This method allows us to update the SpotifyConnectDevice entry with the Spotify Connect
|
|
1551
|
+
Zeroconf response information.
|
|
1552
|
+
"""
|
|
1553
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1554
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1555
|
+
|
|
1556
|
+
# get device instance.
|
|
1557
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(uuid))
|
|
1558
|
+
if (scDevice is not None):
|
|
1559
|
+
|
|
1560
|
+
# ZeroconfResponse class properties.
|
|
1561
|
+
scDevice.ZeroconfResponseInfo.ResponseSource = response.ResponseSource
|
|
1562
|
+
scDevice.ZeroconfResponseInfo.SpotifyError = response.SpotifyError
|
|
1563
|
+
scDevice.ZeroconfResponseInfo.Status = response.Status
|
|
1564
|
+
scDevice.ZeroconfResponseInfo.StatusString = response.StatusString
|
|
1565
|
+
|
|
1566
|
+
# trace.
|
|
1567
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (ZeroconfResponseInfo)" % (scDevice.Name), scDevice.ZeroconfResponseInfo, excludeNonPublic=True)
|
|
1568
|
+
|
|
1569
|
+
# indicate zeroconf response data was received.
|
|
1570
|
+
if (response.ResponseSource == TYPE_ADD_USER_RESPONSE) or (response.ResponseSource == TYPE_ADD_USER_ERROR) or (response.ResponseSource == TYPE_GET_INFO_ERROR):
|
|
1571
|
+
self.WaitForActivationComplete.set()
|
|
1572
|
+
elif (response.ResponseSource == TYPE_TRANSFER_SUCCESS_RESPONSE) or (response.ResponseSource == TYPE_TRANSFER_ERROR_RESPONSE):
|
|
1573
|
+
self.WaitForTransferComplete.set()
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
def OnServiceInfoAddedUpdatedSpotifyConnect(
|
|
1577
|
+
self,
|
|
1578
|
+
zeroconfDiscoveryResult:ZeroconfDiscoveryResult
|
|
1579
|
+
) -> None:
|
|
1580
|
+
"""
|
|
1581
|
+
Called when a new Spotify Connect ServiceInfo record has been discovered
|
|
1582
|
+
or updated for a Spotify Connect device.
|
|
1583
|
+
|
|
1584
|
+
Args:
|
|
1585
|
+
zeroconfDiscoveryResult (ZeroconfDiscoveryResult):
|
|
1586
|
+
A `ZeroconfDiscoveryResult` object that contains discovery details.
|
|
1587
|
+
|
|
1588
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
1589
|
+
the `_Zeroconf_Lock` object to control access.
|
|
1590
|
+
"""
|
|
1591
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1592
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1593
|
+
|
|
1594
|
+
try:
|
|
1595
|
+
|
|
1596
|
+
# trace.
|
|
1597
|
+
_logsi.EnterMethod(SILevel.Debug)
|
|
1598
|
+
|
|
1599
|
+
# add / update discovered results by serviceinfo key value.
|
|
1600
|
+
# we use the serviceinfo NAME value (not KEY) since some manufacturers use random ip port
|
|
1601
|
+
# numbers, and the port number could have been updated. in this case, the name SHOULD remain the same.
|
|
1602
|
+
# note that a different serviceinfo name SHOULD be created for speaker groups; at least they are for BOSE devices.
|
|
1603
|
+
scDevice:SpotifyConnectDevice = None
|
|
1604
|
+
idx:int = self._SpotifyConnectDevices.GetDeviceIndexByDiscoveryName(zeroconfDiscoveryResult.Name)
|
|
1605
|
+
if (idx == -1):
|
|
1606
|
+
|
|
1607
|
+
# trace.
|
|
1608
|
+
_logsi.LogVerbose("Creating new SpotifyConnectDevice instance from ServiceInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
1609
|
+
|
|
1610
|
+
# create a new Spotify Connect Device instance.
|
|
1611
|
+
scDevice = SpotifyConnectDevice()
|
|
1612
|
+
scDevice.Name = zeroconfDiscoveryResult.DeviceName
|
|
1613
|
+
|
|
1614
|
+
# set zeroconf discovery result properties.
|
|
1615
|
+
scDevice.DiscoveryResult = zeroconfDiscoveryResult
|
|
1616
|
+
|
|
1617
|
+
# verify CPath property was present; if not, log a warning as the endpoint url will be invalid!
|
|
1618
|
+
if (zeroconfDiscoveryResult.SpotifyConnectCPath is None):
|
|
1619
|
+
_logsi.LogWarning("Spotify Connect device Zeroconf ServiceInfo data did not contain a CPath value: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
1620
|
+
|
|
1621
|
+
try:
|
|
1622
|
+
|
|
1623
|
+
# trace.
|
|
1624
|
+
_logsi.LogVerbose("Retrieving Spotify Connect device information: %s" % (zeroconfDiscoveryResult.Id))
|
|
1625
|
+
|
|
1626
|
+
# create connection object to retrieve spotify connect device information (via direct ip address).
|
|
1627
|
+
zconn:ZeroconfConnect = ZeroconfConnect(
|
|
1628
|
+
zeroconfDiscoveryResult.HostIpAddress,
|
|
1629
|
+
zeroconfDiscoveryResult.HostIpPort,
|
|
1630
|
+
zeroconfDiscoveryResult.SpotifyConnectCPath,
|
|
1631
|
+
zeroconfDiscoveryResult.SpotifyConnectVersion,
|
|
1632
|
+
useSSL=False,
|
|
1633
|
+
tokenStorageDir=self.SpotifyClientInstance.TokenStorageDir,
|
|
1634
|
+
tokenStorageFile=self.SpotifyClientInstance.TokenStorageFile
|
|
1635
|
+
)
|
|
1636
|
+
|
|
1637
|
+
# retrieve initial spotify connect device information (by ip address).
|
|
1638
|
+
scDevice.DeviceInfo = zconn.GetInformation()
|
|
1639
|
+
scDevice.Id = scDevice.DeviceInfo.DeviceId
|
|
1640
|
+
scDevice.Name = scDevice.DeviceInfo.RemoteName
|
|
1641
|
+
|
|
1642
|
+
except Exception as ex:
|
|
1643
|
+
|
|
1644
|
+
# trace.
|
|
1645
|
+
_logsi.LogVerbose("Could not retrieve Spotify Connect device information by ip address; retrying with DNS server alias \"%s\"" % (zeroconfDiscoveryResult.Server))
|
|
1646
|
+
|
|
1647
|
+
try:
|
|
1648
|
+
|
|
1649
|
+
# create connection object to retrieve spotify connect device information (via dns alias).
|
|
1650
|
+
zconn:ZeroconfConnect = ZeroconfConnect(
|
|
1651
|
+
zeroconfDiscoveryResult.Server,
|
|
1652
|
+
zeroconfDiscoveryResult.HostIpPort,
|
|
1653
|
+
zeroconfDiscoveryResult.SpotifyConnectCPath,
|
|
1654
|
+
zeroconfDiscoveryResult.SpotifyConnectVersion,
|
|
1655
|
+
useSSL=False,
|
|
1656
|
+
tokenStorageDir=self.SpotifyClientInstance.TokenStorageDir,
|
|
1657
|
+
tokenStorageFile=self.SpotifyClientInstance.TokenStorageFile
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
# retrieve initial spotify connect device information (by dns alias).
|
|
1661
|
+
scDevice.DeviceInfo = zconn.GetInformation()
|
|
1662
|
+
scDevice.Id = scDevice.DeviceInfo.DeviceId
|
|
1663
|
+
scDevice.Name = scDevice.DeviceInfo.RemoteName
|
|
1664
|
+
|
|
1665
|
+
# update HostIpAddress in discovery result so it knows to use the alias
|
|
1666
|
+
# instead of the ip address.
|
|
1667
|
+
_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))
|
|
1668
|
+
resolvedIpAddress = socket.gethostbyname(zeroconfDiscoveryResult.Server)
|
|
1669
|
+
zeroconfDiscoveryResult.HostIpAddress = resolvedIpAddress
|
|
1670
|
+
|
|
1671
|
+
except Exception as ex:
|
|
1672
|
+
|
|
1673
|
+
# trace.
|
|
1674
|
+
_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)
|
|
1675
|
+
|
|
1676
|
+
# if Getinformation call failed, then default device information.
|
|
1677
|
+
scDevice.DeviceInfo = ZeroconfGetInfo()
|
|
1678
|
+
scDevice.DeviceInfo.DeviceId = "getInfoError"
|
|
1679
|
+
scDevice.DeviceInfo.RemoteName = zeroconfDiscoveryResult.DeviceName
|
|
1680
|
+
scDevice.DeviceInfo.ResponseSource = "spotifywebapiPython"
|
|
1681
|
+
scDevice.DeviceInfo.Status = 9999
|
|
1682
|
+
scDevice.DeviceInfo.StatusString = str(ex)
|
|
1683
|
+
scDevice.Id = scDevice.DeviceInfo.DeviceId
|
|
1684
|
+
|
|
1685
|
+
# did this device change from a dynamic device (e.g. non-zeroconf) to a zeroconf device?
|
|
1686
|
+
# this can happen if the device is initially added as a dynamic device, then the zeroconf
|
|
1687
|
+
# device is discovered. if so, we will remove the dynamic device entry.
|
|
1688
|
+
spDynamicDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceById(scDevice.DeviceInfo.DeviceId)
|
|
1689
|
+
if (spDynamicDevice is not None):
|
|
1690
|
+
|
|
1691
|
+
# trace.
|
|
1692
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf is converting SpotifyConnectDevice entry %s from dynamic to zeroconf" % (spDynamicDevice.Title))
|
|
1693
|
+
|
|
1694
|
+
# copy real-time status dynamic properties to zerconf object.
|
|
1695
|
+
scDevice.IsActiveDevice = spDynamicDevice.IsActiveDevice
|
|
1696
|
+
scDevice.IsInDeviceList = spDynamicDevice.IsInDeviceList
|
|
1697
|
+
|
|
1698
|
+
# remove the dynamic device from the collection.
|
|
1699
|
+
self.RemoveDevice(spDynamicDevice.Id, dynamicDeviceOnly=True)
|
|
1700
|
+
|
|
1701
|
+
# add new Spotify Connect Device to devices collection.
|
|
1702
|
+
self._SpotifyConnectDevices.Items.append(scDevice)
|
|
1703
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
1704
|
+
|
|
1705
|
+
# trace.
|
|
1706
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf added SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.ForestGreen)
|
|
1707
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
1708
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DiscoveryResult)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1709
|
+
|
|
1710
|
+
# sort devices collection by device name.
|
|
1711
|
+
if (len(self._SpotifyConnectDevices.Items) > 0):
|
|
1712
|
+
self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
|
|
1713
|
+
|
|
1714
|
+
# raise event.
|
|
1715
|
+
self._RaiseDeviceAdded(scDevice)
|
|
1716
|
+
|
|
1717
|
+
else:
|
|
1718
|
+
|
|
1719
|
+
# trace.
|
|
1720
|
+
_logsi.LogDebug("Updating existing SpotifyConnectDevice instance from ServiceInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
1721
|
+
|
|
1722
|
+
# get existing Spotify Connect Device instance.
|
|
1723
|
+
scDevice = self._SpotifyConnectDevices.Items[idx]
|
|
1724
|
+
|
|
1725
|
+
# were any changes made to the zeroconf discovery results?
|
|
1726
|
+
if (not scDevice.DiscoveryResult.Equals(zeroconfDiscoveryResult)):
|
|
1727
|
+
|
|
1728
|
+
# set zeroconf discovery result properties.
|
|
1729
|
+
scDevice.DiscoveryResult = zeroconfDiscoveryResult
|
|
1730
|
+
|
|
1731
|
+
# update existing Spotify Connect Device in devices collection.
|
|
1732
|
+
self._SpotifyConnectDevices.Items[idx] = scDevice
|
|
1733
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
1734
|
+
|
|
1735
|
+
# trace.
|
|
1736
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf updated SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.ForestGreen)
|
|
1737
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
1738
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DiscoveryResult)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1739
|
+
|
|
1740
|
+
# sort devices collection by device name.
|
|
1741
|
+
if (len(self._SpotifyConnectDevices.Items) > 0):
|
|
1742
|
+
self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
|
|
1743
|
+
|
|
1744
|
+
# raise event.
|
|
1745
|
+
self._RaiseDeviceUpdated(scDevice)
|
|
1746
|
+
|
|
1747
|
+
else:
|
|
1748
|
+
|
|
1749
|
+
# trace.
|
|
1750
|
+
_logsi.LogObject(SILevel.Verbose, "SpotifyConnectDevice info: \"%s\" (ignored; DiscoveryResult did not change)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1751
|
+
|
|
1752
|
+
except Exception as ex:
|
|
1753
|
+
|
|
1754
|
+
# trace.
|
|
1755
|
+
_logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
1756
|
+
|
|
1757
|
+
# ignore exceptions, as there is nothing we can do at this point.
|
|
1758
|
+
|
|
1759
|
+
finally:
|
|
1760
|
+
|
|
1761
|
+
# trace.
|
|
1762
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
1763
|
+
|
|
1764
|
+
|
|
1765
|
+
def OnServiceInfoRemovedSpotifyConnect(
|
|
1766
|
+
self,
|
|
1767
|
+
zeroconfDiscoveryResult:ZeroconfDiscoveryResult
|
|
1768
|
+
) -> None:
|
|
1769
|
+
"""
|
|
1770
|
+
Called when an mDNS service is lost.
|
|
1771
|
+
|
|
1772
|
+
Args:
|
|
1773
|
+
zeroconfDiscoveryResult (ZeroconfDiscoveryResult):
|
|
1774
|
+
A `ZeroconfDiscoveryResult` object that contains discovery details.
|
|
1775
|
+
|
|
1776
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
1777
|
+
the `_Zeroconf_Lock` object to control access.
|
|
1778
|
+
"""
|
|
1779
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1780
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1781
|
+
|
|
1782
|
+
try:
|
|
1783
|
+
|
|
1784
|
+
# trace.
|
|
1785
|
+
_logsi.EnterMethod(SILevel.Debug)
|
|
1786
|
+
|
|
1787
|
+
# remove discovered results by serviceinfo key value.
|
|
1788
|
+
# we use the serviceinfo NAME value (not KEY) since that is the only information available on
|
|
1789
|
+
# a remove serviceinfo request.
|
|
1790
|
+
idx:int = self._SpotifyConnectDevices.GetDeviceIndexByDiscoveryName(zeroconfDiscoveryResult.Name)
|
|
1791
|
+
if (idx == -1):
|
|
1792
|
+
|
|
1793
|
+
# trace.
|
|
1794
|
+
_logsi.LogVerbose("SpotifyConnectDevice instance could not be found to remove: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
1795
|
+
|
|
1796
|
+
else:
|
|
1797
|
+
|
|
1798
|
+
# trace.
|
|
1799
|
+
_logsi.LogVerbose("Removing existing SpotifyConnectDevice instance from ServiceInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
1800
|
+
|
|
1801
|
+
# remove existing Spotify Connect Device instance from devices collection.
|
|
1802
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.Items.pop(idx)
|
|
1803
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
1804
|
+
|
|
1805
|
+
# trace.
|
|
1806
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf removed SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.DarkOrange)
|
|
1807
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DeviceInfo / getInfo)" % (scDevice.Name), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
1808
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (DiscoveryResult)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1809
|
+
|
|
1810
|
+
# raise event.
|
|
1811
|
+
self._RaiseDeviceRemoved(scDevice)
|
|
1812
|
+
|
|
1813
|
+
except Exception as ex:
|
|
1814
|
+
|
|
1815
|
+
# trace.
|
|
1816
|
+
_logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
1817
|
+
|
|
1818
|
+
# ignore exceptions, as there is nothing we can do at this point.
|
|
1819
|
+
|
|
1820
|
+
finally:
|
|
1821
|
+
|
|
1822
|
+
# trace.
|
|
1823
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
def _RaiseDeviceAdded(
|
|
1827
|
+
self,
|
|
1828
|
+
device:SpotifyConnectDevice
|
|
1829
|
+
) -> None:
|
|
1830
|
+
"""
|
|
1831
|
+
Raises the `DeviceAdded` event.
|
|
1832
|
+
|
|
1833
|
+
Args:
|
|
1834
|
+
device (SpotifyConnectDevice):
|
|
1835
|
+
Spotify Connect device that was added.
|
|
1836
|
+
"""
|
|
1837
|
+
try:
|
|
1838
|
+
|
|
1839
|
+
# raise event.
|
|
1840
|
+
args:SpotifyConnectDeviceEventArgs = SpotifyConnectDeviceEventArgs(device)
|
|
1841
|
+
self.DeviceAdded(self, args)
|
|
1842
|
+
|
|
1843
|
+
except Exception as ex:
|
|
1844
|
+
|
|
1845
|
+
# ignore exceptions.
|
|
1846
|
+
pass
|
|
1847
|
+
|
|
1848
|
+
|
|
1849
|
+
@abstractmethod
|
|
1850
|
+
def OnDeviceAdded(
|
|
1851
|
+
self,
|
|
1852
|
+
sender:object,
|
|
1853
|
+
e:SpotifyConnectDeviceEventArgs
|
|
1854
|
+
) -> None:
|
|
1855
|
+
"""
|
|
1856
|
+
Method that will handle the `DeviceAdded` event.
|
|
1857
|
+
Inheriting classes can override this method to handle the event.
|
|
1858
|
+
|
|
1859
|
+
Args:
|
|
1860
|
+
sender (object):
|
|
1861
|
+
The object which fired the event.
|
|
1862
|
+
e (SpotifyConnectDeviceEventArgs):
|
|
1863
|
+
Arguments that contain detailed information related to the event.
|
|
1864
|
+
|
|
1865
|
+
This event is fired when a Spotify Connect device has been added to the
|
|
1866
|
+
SpotifyConnectDevices collection.
|
|
1867
|
+
|
|
1868
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
1869
|
+
the `SpotifyConnectDevices_RLock` object to control access.
|
|
1870
|
+
"""
|
|
1871
|
+
pass
|
|
1872
|
+
|
|
1873
|
+
|
|
1874
|
+
def _RaiseDeviceRemoved(
|
|
1875
|
+
self,
|
|
1876
|
+
device:SpotifyConnectDevice
|
|
1877
|
+
) -> None:
|
|
1878
|
+
"""
|
|
1879
|
+
Raises the `DeviceRemoved` event.
|
|
1880
|
+
|
|
1881
|
+
Args:
|
|
1882
|
+
device (SpotifyConnectDevice):
|
|
1883
|
+
Spotify Connect device that was removed.
|
|
1884
|
+
"""
|
|
1885
|
+
try:
|
|
1886
|
+
|
|
1887
|
+
# raise event.
|
|
1888
|
+
args:SpotifyConnectDeviceEventArgs = SpotifyConnectDeviceEventArgs(device)
|
|
1889
|
+
self.DeviceRemoved(self, args)
|
|
1890
|
+
|
|
1891
|
+
except Exception as ex:
|
|
1892
|
+
|
|
1893
|
+
# ignore exceptions.
|
|
1894
|
+
pass
|
|
1895
|
+
|
|
1896
|
+
|
|
1897
|
+
@abstractmethod
|
|
1898
|
+
def OnDeviceRemoved(
|
|
1899
|
+
self,
|
|
1900
|
+
sender:object,
|
|
1901
|
+
e:SpotifyConnectDeviceEventArgs
|
|
1902
|
+
) -> None:
|
|
1903
|
+
"""
|
|
1904
|
+
Method that will handle the `DeviceRemoved` event.
|
|
1905
|
+
Inheriting classes can override this method to handle the event.
|
|
1906
|
+
|
|
1907
|
+
Args:
|
|
1908
|
+
sender (object):
|
|
1909
|
+
The object which fired the event.
|
|
1910
|
+
e (SpotifyConnectDeviceEventArgs):
|
|
1911
|
+
Arguments that contain detailed information related to the event.
|
|
1912
|
+
|
|
1913
|
+
This event is fired when a Spotify Connect device has been removed from the
|
|
1914
|
+
SpotifyConnectDevices collection.
|
|
1915
|
+
|
|
1916
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
1917
|
+
the `SpotifyConnectDevices_RLock` object to control access.
|
|
1918
|
+
"""
|
|
1919
|
+
pass
|
|
1920
|
+
|
|
1921
|
+
|
|
1922
|
+
def _RaiseDeviceUpdated(
|
|
1923
|
+
self,
|
|
1924
|
+
device:SpotifyConnectDevice
|
|
1925
|
+
) -> None:
|
|
1926
|
+
"""
|
|
1927
|
+
Raises the `DeviceUpdated` event.
|
|
1928
|
+
|
|
1929
|
+
Args:
|
|
1930
|
+
device (SpotifyConnectDevice):
|
|
1931
|
+
Spotify Connect device that was updated.
|
|
1932
|
+
"""
|
|
1933
|
+
try:
|
|
1934
|
+
|
|
1935
|
+
# raise event.
|
|
1936
|
+
args:SpotifyConnectDeviceEventArgs = SpotifyConnectDeviceEventArgs(device)
|
|
1937
|
+
self.DeviceUpdated(self, args)
|
|
1938
|
+
|
|
1939
|
+
except Exception as ex:
|
|
1940
|
+
|
|
1941
|
+
# ignore exceptions.
|
|
1942
|
+
pass
|
|
1943
|
+
|
|
1944
|
+
|
|
1945
|
+
@abstractmethod
|
|
1946
|
+
def OnDeviceUpdated(
|
|
1947
|
+
self,
|
|
1948
|
+
sender:object,
|
|
1949
|
+
e:SpotifyConnectDeviceEventArgs
|
|
1950
|
+
) -> None:
|
|
1951
|
+
"""
|
|
1952
|
+
Method that will handle the `DeviceUpdated` event.
|
|
1953
|
+
Inheriting classes can override this method to handle the event.
|
|
1954
|
+
|
|
1955
|
+
Args:
|
|
1956
|
+
sender (object):
|
|
1957
|
+
The object which fired the event.
|
|
1958
|
+
e (SpotifyConnectDeviceEventArgs):
|
|
1959
|
+
Arguments that contain detailed information related to the event.
|
|
1960
|
+
|
|
1961
|
+
This event is fired when a Spotify Connect device has been updated in the
|
|
1962
|
+
SpotifyConnectDevices collection.
|
|
1963
|
+
|
|
1964
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
1965
|
+
the `SpotifyConnectDevices_RLock` object to control access.
|
|
1966
|
+
"""
|
|
1967
|
+
pass
|