spotifywebapiPython 1.0.147__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.

Files changed (38) hide show
  1. {spotifywebapiPython-1.0.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/METADATA +5 -9
  2. {spotifywebapiPython-1.0.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/RECORD +38 -29
  3. spotifywebapipython/__init__.py +0 -2
  4. spotifywebapipython/const.py +1 -1
  5. spotifywebapipython/docs/__init__.html +25 -4
  6. spotifywebapipython/docs/index.html +26 -2
  7. spotifywebapipython/docs/search.js +1 -1
  8. spotifywebapipython/docs/spotifywebapipython/const.html +3 -3
  9. spotifywebapipython/docs/spotifywebapipython/saappmessages.html +2 -2
  10. spotifywebapipython/docs/spotifywebapipython/sautils.html +8 -8
  11. spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +2 -2
  12. spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +2 -2
  13. spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +2 -2
  14. spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +514 -741
  15. spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +3 -3
  16. spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +2 -2
  17. spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +2 -2
  18. spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +466 -0
  19. spotifywebapipython/models/spotifyconnectdevice.py +84 -4
  20. spotifywebapipython/models/spotifyconnectdevices.py +16 -307
  21. spotifywebapipython/models/zeroconfdiscoveryresult.py +0 -41
  22. spotifywebapipython/sautils.py +0 -1
  23. spotifywebapipython/spotifyclient.py +1384 -1835
  24. spotifywebapipython/spotifyconnect/__init__.py +19 -0
  25. spotifywebapipython/{spotifyconnectdeviceeventargs.py → spotifyconnect/spotifyconnectdeviceeventargs.py} +1 -1
  26. spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +1967 -0
  27. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +336 -0
  28. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +609 -0
  29. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +316 -0
  30. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +10 -0
  31. spotifywebapipython/spotifyconnect/spotifyconnectzeroconflistener.py +335 -0
  32. spotifywebapipython/spotifydiscovery.py +0 -1
  33. spotifywebapipython/spotifywebplayertoken.py +344 -0
  34. spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +4 -52
  35. spotifywebapipython/zeroconfapi/zeroconfresponse.py +8 -0
  36. {spotifywebapiPython-1.0.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/LICENSE +0 -0
  37. {spotifywebapiPython-1.0.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/WHEEL +0 -0
  38. {spotifywebapiPython-1.0.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,316 @@
1
+ # external package imports.
2
+ from pychromecast import CastInfo
3
+ from pychromecast.discovery import AbstractCastListener
4
+ import threading
5
+ from uuid import UUID
6
+
7
+ # our package imports.
8
+ from spotifywebapipython.models import ZeroconfProperty, ZeroconfDiscoveryResult
9
+ # note - cannot reference `SpotifyConnectDirectoryTask` due to circular import!
10
+ # from .spotifyconnectdirectorytask import SpotifyConnectDirectoryTask
11
+
12
+ # get smartinspect logger reference; create a new session for this module name.
13
+ from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext
14
+ import logging
15
+
16
+ _logsi:SISession = SIAuto.Si.GetSession(__name__)
17
+ if (_logsi == None):
18
+ _logsi = SIAuto.Si.AddSession(__name__, True)
19
+ _logsi.SystemLogger = logging.getLogger(__name__)
20
+
21
+ ZEROCONF_SERVICETYPE_GOOGLECAST:str = "_googlecast._tcp.local."
22
+ """
23
+ Googlecast Zeroconf service type identifier.
24
+ """
25
+
26
+
27
+ class SpotifyConnectZeroconfCastListener(AbstractCastListener):
28
+ """
29
+ Google Chromecast Zeroconf Listener class.
30
+
31
+ Listens for Chromecast device connection updates for devices that
32
+ support Spotify Connect.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ parentDirectory,
38
+ zeroconf_Lock:threading.Lock,
39
+ ) -> None:
40
+ """
41
+ Initializes a new instance of the class.
42
+
43
+ Args:
44
+ parentDirectory (SpotifyConnectDirectoryTask):
45
+ Parent SpotifyConnectDirectoryTask instance.
46
+ zeroconf_Lock (threading.Lock):
47
+ Lock object used to enforce thread-safe updates.
48
+ """
49
+ # invoke base class method.
50
+ super().__init__()
51
+
52
+ # initialize storage.
53
+ self._ParentDirectory = parentDirectory
54
+ self._Zeroconf_Lock:threading.Lock = zeroconf_Lock
55
+
56
+
57
+ def _GetZeroconfDiscoveryResult(
58
+ self,
59
+ uuid:UUID,
60
+ serviceName:str,
61
+ castInfo_removed:CastInfo = None
62
+ ) -> ZeroconfDiscoveryResult:
63
+ """
64
+ Builds a `ZeroconfDiscoveryResult` instance from Zeroconf CastInfo data
65
+ when an add or update service state change takes place.
66
+
67
+ Args:
68
+ uuid (UUID):
69
+ The cast's uuid, which is the dictionary key to find the chromecast
70
+ metadata in CastBrowser.devices collection.
71
+ serviceName (str):
72
+ First known MDNS service name or host:port.
73
+ castInfo_removed (CastInfo):
74
+ CastInfo for the service to aid cleanup; only supplied for removal
75
+ requests since the CastBrowser.services collection no longer contains
76
+ a CastInfo object for the specified uuid.
77
+ """
78
+ result:ZeroconfDiscoveryResult = None
79
+
80
+ try:
81
+
82
+ # is this an MDNSServiceInfo service record? we only want discovery results
83
+ # that contain MDNSServiceInfo service information.
84
+ if (serviceName.find(ZEROCONF_SERVICETYPE_GOOGLECAST) == -1):
85
+ _logsi.LogDebug("Chromecast Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, "ignored; not MDNSInfo"))
86
+ return None
87
+
88
+ # get chromecast info.
89
+ castInfo:CastInfo = castInfo_removed
90
+ if (castInfo is None):
91
+ castInfo:CastInfo = self._ParentDirectory._CastBrowser.services[uuid]
92
+
93
+ # trace.
94
+ _logsi.LogObject(SILevel.Debug, "Chromecast Zeroconf service details: \"%s\" (%s) (CastInfo object)" % (castInfo.friendly_name, serviceName), castInfo)
95
+
96
+ # TODO we may need to expand this check based on cast_type value ...
97
+ # Regular chromecast, supports video/audio
98
+ #CAST_TYPE_CHROMECAST = "cast"
99
+ # Cast Audio device, supports only audio
100
+ #CAST_TYPE_AUDIO = "audio"
101
+ # Cast Audio group device, supports only audio
102
+ #CAST_TYPE_GROUP = "group"
103
+ if (castInfo.cast_type != "audio"):
104
+ _logsi.LogDebug("Chromecast device does not support 'audio'; ignoring: \"%s\" (%s)" % (castInfo.friendly_name, serviceName))
105
+ return None
106
+
107
+ # create new discovery result instance.
108
+ result:ZeroconfDiscoveryResult = ZeroconfDiscoveryResult()
109
+ result.DeviceName = castInfo.friendly_name
110
+ result.Domain = ".local"
111
+ result.HostIpAddresses = [castInfo.host]
112
+ result.HostIpPort = castInfo.port
113
+ result.HostTTL = 120
114
+ result.IsChromeCast = True
115
+ result.Key = str(uuid)
116
+ result.Name = serviceName
117
+ result.Priority = 0
118
+ result.OtherTTL = 4500
119
+ result.Server = serviceName
120
+ result.ServerKey = serviceName
121
+ result.ServiceType = ZEROCONF_SERVICETYPE_GOOGLECAST
122
+ result.Weight = 0
123
+ result.Properties.append(ZeroconfProperty("CPath","/na"))
124
+ result.Properties.append(ZeroconfProperty("VERSION","1.0"))
125
+ result.SpotifyConnectCPath = "/na"
126
+ result.SpotifyConnectVersion = "1.0"
127
+ result.Id = "\"%s\" (%s:%s)" % (result.DeviceName, result.HostIpAddress, result.HostIpPort)
128
+
129
+ # trace.
130
+ _logsi.LogObject(SILevel.Debug, "Chromecast Zeroconf Discovery Result: %s (object)" % (result.Id), result, excludeNonPublic=True)
131
+
132
+ # return discovery result.
133
+ return result
134
+
135
+ except Exception as ex:
136
+
137
+ # trace.
138
+ _logsi.LogException("Could not load ZeroconfDiscoveryResult instance from Chromecast Zeroconf service details", ex, logToSystemLogger=False)
139
+
140
+ # ignore exception
141
+ return None
142
+
143
+
144
+ def add_cast(
145
+ self,
146
+ uuid:UUID,
147
+ serviceName:str
148
+ ) -> None:
149
+ """
150
+ Called when a new cast has been discovered.
151
+
152
+ Args:
153
+ uuid (UUID):
154
+ The cast's uuid, which is the dictionary key to find the chromecast
155
+ metadata in CastBrowser.devices collection.
156
+ serviceName (str):
157
+ First known MDNS service name or host:port.
158
+ """
159
+ # use lock, as multiple threads could be calling this method simultaneously.
160
+ with self._Zeroconf_Lock:
161
+
162
+ # copy passed parameters to thread-safe storage.
163
+ argsUuid:UUID = uuid
164
+ argsServiceName:str = serviceName
165
+
166
+ apiMethodName:str = "add_cast"
167
+ apiMethodParms:SIMethodParmListContext = None
168
+
169
+ try:
170
+
171
+ # trace.
172
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
173
+ apiMethodParms.AppendKeyValue("uuid", argsUuid)
174
+ apiMethodParms.AppendKeyValue("serviceName", argsServiceName)
175
+ _logsi.LogMethodParmList(SILevel.Debug, "Chromecast Zeroconf discovery service notification: \"%s\" (%s)" % (argsServiceName, apiMethodName), apiMethodParms)
176
+
177
+ # build discovery result instance from service state information.
178
+ # if nothing returned, then don't bother!
179
+ result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(argsUuid, argsServiceName, None)
180
+ if (result is None):
181
+ return
182
+
183
+ # let the parent handle the event.
184
+ self._ParentDirectory.OnServiceInfoAddedUpdatedChromecast(result, argsUuid, argsServiceName)
185
+
186
+ except Exception as ex:
187
+
188
+ # trace.
189
+ _logsi.LogException("Unhandled exception occured while processing Chromecast Zeroconf service browser ServiceStateChange notification (%s)" % (apiMethodName), ex, logToSystemLogger=False)
190
+
191
+ # ignore exception, as nothing can be done about it.
192
+
193
+ finally:
194
+
195
+ # trace.
196
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
197
+
198
+
199
+ def remove_cast(
200
+ self,
201
+ uuid:UUID,
202
+ serviceName:str,
203
+ castInfo:CastInfo
204
+ ) -> None:
205
+ """
206
+ Called when a cast has been lost (MDNS info expired or host down).
207
+
208
+ Args:
209
+ uuid (UUID):
210
+ The cast's uuid, which is the dictionary key to find the chromecast
211
+ metadata in CastBrowser.devices collection.
212
+ serviceName (str):
213
+ Last valid MDNS service name or host:port.
214
+ castInfo (CastInfo):
215
+ CastInfo for the service to aid cleanup.
216
+ """
217
+ # use lock, as multiple threads could be calling this method simultaneously.
218
+ with self._Zeroconf_Lock:
219
+
220
+ # copy passed parameters to thread-safe storage.
221
+ argsUuid:UUID = uuid
222
+ argsServiceName:str = serviceName
223
+ argsCastInfo:CastInfo = castInfo
224
+
225
+ apiMethodName:str = "remove_cast"
226
+ apiMethodParms:SIMethodParmListContext = None
227
+
228
+ try:
229
+
230
+ # trace.
231
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
232
+ apiMethodParms.AppendKeyValue("uuid", argsUuid)
233
+ apiMethodParms.AppendKeyValue("serviceName", argsServiceName)
234
+ _logsi.LogMethodParmList(SILevel.Debug, "Chromecast Zeroconf discovery service notification: \"%s\" (%s)" % (argsServiceName, apiMethodName), apiMethodParms)
235
+
236
+ # for service removal notifications, there will be no zeroconf service info.
237
+ # build discovery result instance from service state information.
238
+ # if nothing returned, then it's ok - build a minimal discovery instance that
239
+ # will contain the necessary details to remove the device entry.
240
+ result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(argsUuid, argsServiceName, argsCastInfo)
241
+ if (result is None):
242
+ result = ZeroconfDiscoveryResult()
243
+ result.DeviceName = castInfo.friendly_name
244
+ result.Key = str(uuid)
245
+ result.Name = serviceName
246
+ result.ServiceType = ZEROCONF_SERVICETYPE_GOOGLECAST
247
+
248
+ # let the parent handle the event.
249
+ self._ParentDirectory.OnServiceInfoRemovedChromecast(result, argsUuid, argsServiceName, argsCastInfo)
250
+
251
+ except Exception as ex:
252
+
253
+ # trace.
254
+ _logsi.LogException("Unhandled exception occured while processing Chromecast Zeroconf service browser ServiceStateChange notification (%s)" % (apiMethodName), ex, logToSystemLogger=False)
255
+
256
+ # ignore exception, as nothing can be done about it.
257
+
258
+ finally:
259
+
260
+ # trace.
261
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
262
+
263
+
264
+ def update_cast(
265
+ self,
266
+ uuid:UUID,
267
+ serviceName:str
268
+ ) -> None:
269
+ """
270
+ Called when a cast has been updated (MDNS info renewed or changed).
271
+
272
+ Args:
273
+ uuid (UUID):
274
+ The cast's uuid, which is the dictionary key to find the chromecast
275
+ metadata in CastBrowser.devices collection.
276
+ serviceName (str):
277
+ MDNS service name or host:port.
278
+ """
279
+ # use lock, as multiple threads could be calling this method simultaneously.
280
+ with self._Zeroconf_Lock:
281
+
282
+ # copy passed parameters to thread-safe storage.
283
+ argsUuid:UUID = uuid
284
+ argsServiceName:str = serviceName
285
+
286
+ apiMethodName:str = "update_cast"
287
+ apiMethodParms:SIMethodParmListContext = None
288
+
289
+ try:
290
+
291
+ # trace.
292
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
293
+ apiMethodParms.AppendKeyValue("uuid", argsUuid)
294
+ apiMethodParms.AppendKeyValue("serviceName", argsServiceName)
295
+ _logsi.LogMethodParmList(SILevel.Debug, "Chromecast Zeroconf discovery service notification: \"%s\" (%s)" % (str(argsUuid), apiMethodName), apiMethodParms)
296
+
297
+ # build discovery result instance from service state information.
298
+ # if nothing returned, then don't bother!
299
+ result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(argsUuid, argsServiceName, None)
300
+ if (result is None):
301
+ return
302
+
303
+ # let the parent handle the event.
304
+ self._ParentDirectory.OnServiceInfoAddedUpdatedChromecast(result, argsUuid, argsServiceName)
305
+
306
+ except Exception as ex:
307
+
308
+ # trace.
309
+ _logsi.LogException("Unhandled exception occured while processing Chromecast Zeroconf service browser ServiceStateChange notification (%s)" % (apiMethodName), ex, logToSystemLogger=False)
310
+
311
+ # ignore exception, as nothing can be done about it.
312
+
313
+ finally:
314
+
315
+ # trace.
316
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
@@ -0,0 +1,10 @@
1
+ class SpotifyConnectZeroconfLaunchError(Exception):
2
+ """
3
+ Exception raised when a cast app fails to launch.
4
+ """
5
+
6
+
7
+ class SpotifyConnectZeroconfPlaybackTransferError(Exception):
8
+ """
9
+ Exception raised when a cast app fails to receive transfer of playback control.
10
+ """
@@ -0,0 +1,335 @@
1
+ # external package imports.
2
+ import threading
3
+ from zeroconf import Zeroconf, ServiceInfo, ServiceStateChange, IPVersion
4
+
5
+ # our package imports.
6
+ from spotifywebapipython.models import ZeroconfProperty, ZeroconfDiscoveryResult
7
+ # note - cannot reference `SpotifyConnectDirectoryTask` due to circular import!
8
+ # from .SpotifyConnectDirectoryTask import SpotifyConnectDirectoryTask
9
+
10
+ # get smartinspect logger reference; create a new session for this module name.
11
+ from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext
12
+ import logging
13
+
14
+ _logsi:SISession = SIAuto.Si.GetSession(__name__)
15
+ if (_logsi == None):
16
+ _logsi = SIAuto.Si.AddSession(__name__, True)
17
+ _logsi.SystemLogger = logging.getLogger(__name__)
18
+
19
+ ZEROCONF_SERVICETYPE_SPOTIFYCONNECT:str = "_spotify-connect._tcp.local."
20
+ """
21
+ Spotify Connect Zeroconf service type identifier.
22
+ """
23
+
24
+
25
+ class SpotifyConnectZeroconfListener:
26
+ """
27
+ Spotify Connect Zeroconf Listener class.
28
+
29
+ Listens for Chromecast device connection updates for devices that
30
+ support Spotify Connect.
31
+ """
32
+
33
+ # this class can be tested by issuing the following commands from an MS-DOS
34
+ # command prompt to simulate Zeroconf registration updates.
35
+
36
+ # - add a new Spotify Connect instance registration:
37
+ # > dns-sd -R "MyTestSC1" _spotify-connect._tcp. local 8080 cpath=/zcXXX version=1.0
38
+
39
+ # - update a new Spotify Connect instance registration;
40
+ # - if you change the name, service type, or port number, a NEW registration is created!
41
+ # - for updates, the only thing you can really update is the TXT record properties.
42
+ # > dns-sd -R "MyTestSC1" _spotify-connect._tcp. local 8080 cpath=/zcYYY version=1.0
43
+
44
+ # - remove an existing Spotify Connect instance registration;
45
+ # > the only way to remove an entry is to kill the process the entry was created under!
46
+
47
+
48
+ def __init__(
49
+ self,
50
+ parentDirectory,
51
+ zeroconf_Lock:threading.Lock,
52
+ ) -> None:
53
+ """
54
+ Initializes a new instance of the class.
55
+
56
+ Args:
57
+ parentDirectory (SpotifyConnectDirectoryTask):
58
+ Parent SpotifyConnectDirectoryTask instance.
59
+ zeroconf_Lock (threading.Lock):
60
+ Lock object used to enforce thread-safe updates.
61
+ """
62
+ # invoke base class method.
63
+ super().__init__()
64
+
65
+ # initialize storage.
66
+ self._ParentDirectory = parentDirectory
67
+ self._Zeroconf_Lock:threading.Lock = zeroconf_Lock
68
+
69
+
70
+ def _GetZeroconfDiscoveryResult(
71
+ self,
72
+ zeroconf:Zeroconf,
73
+ serviceType:str,
74
+ serviceName:str,
75
+ serviceStateChange:ServiceStateChange
76
+ ) -> ZeroconfDiscoveryResult:
77
+ """
78
+ Builds a `ZeroconfDiscoveryResult` instance from Zeroconf Service Information data
79
+ when an add or update service state change takes place.
80
+
81
+ Args:
82
+ zeroconf (Zeroconf):
83
+ Zeroconf instance that raised the notification.
84
+ serviceType (str):
85
+ Service type that was affected.
86
+ serviceName (str):
87
+ Service name that was affected.
88
+ serviceStateChange (ServiceStateChange):
89
+ A description of the type of service state change (add, remove, update).
90
+ """
91
+ result:ZeroconfDiscoveryResult = None
92
+
93
+ try:
94
+
95
+ # get service information instance; if none, then we are done.
96
+ deviceName:str = (serviceName.split(".")[0])
97
+ serviceInfo:ServiceInfo = zeroconf.get_service_info(serviceType, serviceName)
98
+ if serviceInfo is None:
99
+ # serviceinfo will be missing if it's a removal, so don't log the "ignored" message in this case.
100
+ if (serviceStateChange != ServiceStateChange.Removed):
101
+ _logsi.LogDebug("Spotify Connect Zeroconf discovery service notification ignored: \"%s\" (%s:%s) - no serviceinfo data" % (serviceName, serviceType, serviceStateChange))
102
+ return result
103
+
104
+ # get list of displayable (parsed) ipv4 addresses; if none, then we are done.
105
+ ipAddressList = serviceInfo.parsed_addresses(IPVersion.V4Only)
106
+ if (ipAddressList is None):
107
+ _logsi.LogDebug("Spotify Connect Zeroconf discovery service notification ignored: \"%s\" (%s:%s) - no IP address list" % (serviceName, serviceInfo.name, serviceStateChange))
108
+ return result
109
+
110
+ # trace.
111
+ _logsi.LogObject(SILevel.Debug, "Spotify Connect Zeroconf service details: \"%s\" (%s) (serviceinfo object)" % (deviceName, serviceInfo.name), serviceInfo)
112
+
113
+ # create new discovery result instance.
114
+ result:ZeroconfDiscoveryResult = ZeroconfDiscoveryResult()
115
+ result.DeviceName = deviceName
116
+ result.Domain = '.local'
117
+ for item in ipAddressList:
118
+ result.HostIpAddresses.append(item)
119
+ result.HostIpPort = serviceInfo.port
120
+ result.HostTTL = serviceInfo.host_ttl
121
+ result.Key = serviceInfo.key
122
+ result.Name = serviceInfo.name
123
+ result.OtherTTL = serviceInfo.other_ttl
124
+ result.Priority = serviceInfo.priority
125
+ result.Server = serviceInfo.server
126
+ result.ServerKey = serviceInfo.server_key
127
+ #result.ServiceInfo = serviceInfo
128
+ result.ServiceType = serviceType
129
+ result.Weight = serviceInfo.weight
130
+ result.Id = "\"%s\" (%s:%s)" % (result.DeviceName, result.HostIpAddress, result.HostIpPort)
131
+
132
+ # process service info propertys.
133
+ # note that the property keys and values must first be decoded to utf-8 encoding.
134
+ if serviceInfo.properties is not None:
135
+ dspProperties:dict = {}
136
+ for key, value in serviceInfo.properties.items():
137
+ keyStr = key.decode('utf-8') or ""
138
+ valueStr = value.decode('utf-8') or ""
139
+ result.Properties.append(ZeroconfProperty(keyStr, valueStr))
140
+ dspProperties[keyStr] = valueStr
141
+ # check for Spotify Connect common properties.
142
+ if keyStr.lower() == 'cpath':
143
+ result.SpotifyConnectCPath = valueStr
144
+ elif keyStr.lower() == 'version':
145
+ result.SpotifyConnectVersion = valueStr
146
+ _logsi.LogDictionary(SILevel.Debug, "Spotify Connect Zeroconf service details: \"%s\" (%s) (serviceinfo properties)" % (deviceName, serviceInfo.name), dspProperties)
147
+
148
+ # trace.
149
+ _logsi.LogObject(SILevel.Debug, "Spotify Connect Zeroconf Discovery Result: %s (object)" % (result.Id), result, excludeNonPublic=True)
150
+
151
+ # return discovery result.
152
+ return result
153
+
154
+ except Exception as ex:
155
+
156
+ # trace.
157
+ _logsi.LogException("Could not load ZeroconfDiscoveryResult instance from Zeroconf serviceinfo object.", ex, logToSystemLogger=False)
158
+
159
+ # ignore exception
160
+ return None
161
+
162
+
163
+ def OnServiceStateChange(
164
+ self,
165
+ zeroconf:Zeroconf,
166
+ service_type:str,
167
+ name:str,
168
+ state_change:ServiceStateChange
169
+ ) -> None:
170
+ """
171
+ Called by the spotify connect zeroconf ServiceBrowser when an mDNS service
172
+ state has changed (e.g. added, removed, or updated).
173
+
174
+ Args:
175
+ zeroconf (Zeroconf):
176
+ Zeroconf instance that raised the notification.
177
+ service_type (str):
178
+ Service type that was affected.
179
+ name (str):
180
+ Service name that was affected.
181
+ state_change (ServiceStateChange):
182
+ The type of service state change (add, remove, update).
183
+ """
184
+ # use lock, as multiple threads could be calling this method simultaneously.
185
+ with self._Zeroconf_Lock:
186
+
187
+ # copy passed parameters to thread-safe storage.
188
+ serviceType:str = service_type
189
+ serviceName:str = name
190
+ serviceStateChange:ServiceStateChange = state_change
191
+
192
+ apiMethodName:str = "OnServiceStateChange"
193
+ apiMethodParms:SIMethodParmListContext = None
194
+
195
+ try:
196
+
197
+ # trace.
198
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
199
+ apiMethodParms.AppendKeyValue("serviceType", serviceType)
200
+ apiMethodParms.AppendKeyValue("serviceName", serviceName)
201
+ apiMethodParms.AppendKeyValue("serviceStateChange", serviceStateChange)
202
+ _logsi.LogMethodParmList(SILevel.Verbose, "Spotify Connect Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, str(serviceStateChange)), apiMethodParms)
203
+
204
+ # call method based on serviceStateChange argument value.
205
+ if (serviceStateChange == ServiceStateChange.Added):
206
+ self.add_service(zeroconf, serviceType, serviceName, serviceStateChange)
207
+ return
208
+
209
+ if (serviceStateChange == ServiceStateChange.Removed):
210
+ self.remove_service(zeroconf, serviceType, serviceName, serviceStateChange)
211
+ return
212
+
213
+ if (serviceStateChange == ServiceStateChange.Updated):
214
+ self.update_service(zeroconf, serviceType, serviceName, serviceStateChange)
215
+ return
216
+
217
+ except Exception as ex:
218
+
219
+ # trace.
220
+ _logsi.LogException("Unhandled exception occured while processing Spotify Connect Zeroconf discovery browser ServiceStateChange notification (%s)" % (serviceStateChange), ex, logToSystemLogger=False)
221
+
222
+ # ignore exception, as nothing can be done about it.
223
+
224
+ finally:
225
+
226
+ # trace.
227
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
228
+
229
+
230
+ def add_service(
231
+ self,
232
+ zeroconf:Zeroconf,
233
+ serviceType:str,
234
+ serviceName:str,
235
+ serviceStateChange:ServiceStateChange
236
+ ) -> None:
237
+ """
238
+ Called when an mDNS service is discovered.
239
+
240
+ Args:
241
+ zeroconf (Zeroconf):
242
+ Zeroconf instance that raised the notification.
243
+ serviceType (str):
244
+ Service type that was affected.
245
+ serviceName (str):
246
+ Service name that was affected.
247
+ serviceStateChange (ServiceStateChange):
248
+ A description of the type of service state change (add, remove, update).
249
+
250
+ This method will be called in a thread-safe manner, as the caller is using
251
+ the `Zeroconf_Lock` object to control access.
252
+ """
253
+ # build discovery result instance from service state information.
254
+ # if nothing returned, then don't bother!
255
+ result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(zeroconf, serviceType, serviceName, serviceStateChange)
256
+ if (result is None):
257
+ return
258
+
259
+ # let the parent handle the event.
260
+ self._ParentDirectory.OnServiceInfoAddedUpdatedSpotifyConnect(result)
261
+ return
262
+
263
+
264
+ def remove_service(
265
+ self,
266
+ zeroconf:Zeroconf,
267
+ serviceType:str,
268
+ serviceName:str,
269
+ serviceStateChange:ServiceStateChange
270
+ ) -> None:
271
+ """
272
+ Called when an mDNS service is lost.
273
+
274
+ Args:
275
+ zeroconf (Zeroconf):
276
+ Zeroconf instance that raised the notification.
277
+ serviceType (str):
278
+ Service type that was affected.
279
+ serviceName (str):
280
+ Service name that was affected.
281
+ serviceStateChange (ServiceStateChange):
282
+ A description of the type of service state change (add, remove, update).
283
+
284
+ This method will be called in a thread-safe manner, as the caller is using
285
+ the `Zeroconf_Lock` object to control access.
286
+ """
287
+ # for service removal notifications, there will be no zeroconf service info.
288
+ # build discovery result instance from service state information.
289
+ # if nothing returned, then it's ok - build a minimal discovery instance that
290
+ # will contain the necessary details to remove the device entry.
291
+ result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(zeroconf, serviceType, serviceName, serviceStateChange)
292
+ if (result is None):
293
+ result = ZeroconfDiscoveryResult()
294
+ result.DeviceName = (serviceName.split(".")[0])
295
+ result.Key = serviceName.lower()
296
+ result.Name = serviceName
297
+ result.ServiceType = serviceType
298
+
299
+ # let the parent handle the event.
300
+ self._ParentDirectory.OnServiceInfoRemovedSpotifyConnect(result)
301
+ return
302
+
303
+
304
+ def update_service(
305
+ self,
306
+ zeroconf:Zeroconf,
307
+ serviceType:str,
308
+ serviceName:str,
309
+ serviceStateChange:ServiceStateChange
310
+ ) -> None:
311
+ """
312
+ Called when an mDNS service is updated.
313
+
314
+ Args:
315
+ zeroconf (Zeroconf):
316
+ Zeroconf instance that raised the notification.
317
+ serviceType (str):
318
+ Service type that was affected.
319
+ serviceName (str):
320
+ Service name that was affected.
321
+ serviceStateChange (ServiceStateChange):
322
+ A description of the type of service state change (add, remove, update).
323
+
324
+ This method will be called in a thread-safe manner, as the caller is using
325
+ the `Zeroconf_Lock` object to control access.
326
+ """
327
+ # build discovery result instance from service state information.
328
+ # if nothing returned, then don't bother!
329
+ result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(zeroconf, serviceType, serviceName, serviceStateChange)
330
+ if (result is None):
331
+ return
332
+
333
+ # let the parent handle the event.
334
+ self._ParentDirectory.OnServiceInfoAddedUpdatedSpotifyConnect(result)
335
+ return
@@ -223,7 +223,6 @@ class SpotifyDiscovery:
223
223
  result.Priority = serviceInfo.priority
224
224
  result.Server = serviceInfo.server
225
225
  result.ServerKey = serviceInfo.server_key
226
- result.ServiceInfo = serviceInfo
227
226
  result.ServiceType = serviceType
228
227
  result.Weight = serviceInfo.weight
229
228
  result.Id = "\"%s\" (%s:%s)" % (result.DeviceName, result.HostIpAddress, result.HostIpPort)