spotifywebapiPython 1.0.131__py3-none-any.whl → 1.0.133__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of spotifywebapiPython might be problematic. Click here for more details.

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