spotifywebapiPython 1.0.36__py3-none-any.whl → 1.0.147__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.36.dist-info → spotifywebapiPython-1.0.147.dist-info}/LICENSE +1 -1
- {spotifywebapiPython-1.0.36.dist-info → spotifywebapiPython-1.0.147.dist-info}/METADATA +7 -6
- spotifywebapiPython-1.0.147.dist-info/RECORD +117 -0
- spotifywebapipython/__init__.py +2 -0
- spotifywebapipython/const.py +19 -1
- spotifywebapipython/docs/__init__.html +790 -3
- spotifywebapipython/docs/index.html +21 -3
- spotifywebapipython/docs/search.js +1 -1
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/const.html +72 -4
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/saappmessages.html +77 -3
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/sautils.html +63 -3
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifyapierror.html +3 -3
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifyapimessage.html +20 -3
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifyauthtoken.html +39 -3
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifyclient.html +2553 -364
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifydiscovery.html +85 -52
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifywebapiauthenticationerror.html +3 -3
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifywebapierror.html +3 -3
- spotifywebapipython/models/__init__.py +8 -0
- spotifywebapipython/models/albumpagesaved.py +2 -1
- spotifywebapipython/models/albumpagesimplified.py +2 -1
- spotifywebapipython/models/albumsimplified.py +4 -5
- spotifywebapipython/models/artist.py +4 -5
- spotifywebapipython/models/artistinfo.py +282 -0
- spotifywebapipython/models/artistinfotourevent.py +127 -0
- spotifywebapipython/models/artistpage.py +2 -1
- spotifywebapipython/models/artistsimplified.py +4 -0
- spotifywebapipython/models/audiobookpagesimplified.py +2 -1
- spotifywebapipython/models/audiobooksimplified.py +4 -5
- spotifywebapipython/models/category.py +16 -0
- spotifywebapipython/models/categorypage.py +2 -1
- spotifywebapipython/models/chapterpagesimplified.py +2 -1
- spotifywebapipython/models/chaptersimplified.py +6 -7
- spotifywebapipython/models/context.py +1 -3
- spotifywebapipython/models/device.py +85 -2
- spotifywebapipython/models/episodepagesaved.py +2 -1
- spotifywebapipython/models/episodepagesimplified.py +2 -1
- spotifywebapipython/models/episodesaved.py +2 -2
- spotifywebapipython/models/episodesimplified.py +5 -6
- spotifywebapipython/models/imageobject.py +98 -0
- spotifywebapipython/models/pageobject.py +29 -4
- spotifywebapipython/models/playeractions.py +33 -11
- spotifywebapipython/models/playerplaystate.py +128 -13
- spotifywebapipython/models/playerqueueinfo.py +61 -4
- spotifywebapipython/models/playhistorypage.py +2 -1
- spotifywebapipython/models/playlist.py +6 -3
- spotifywebapipython/models/playlistpage.py +2 -1
- spotifywebapipython/models/playlistpagesimplified.py +2 -1
- spotifywebapipython/models/playlistsimplified.py +49 -7
- spotifywebapipython/models/playlisttrack.py +1 -0
- spotifywebapipython/models/playlisttracksummary.py +3 -1
- spotifywebapipython/models/searchresponse.py +1 -1
- spotifywebapipython/models/showpagesaved.py +2 -1
- spotifywebapipython/models/showpagesimplified.py +2 -1
- spotifywebapipython/models/showsaved.py +2 -2
- spotifywebapipython/models/showsimplified.py +8 -5
- spotifywebapipython/models/spotifyconnectdevice.py +188 -0
- spotifywebapipython/models/spotifyconnectdevices.py +628 -0
- spotifywebapipython/models/track.py +2 -1
- spotifywebapipython/models/trackpage.py +42 -1
- spotifywebapipython/models/trackpagesaved.py +2 -1
- spotifywebapipython/models/trackpagesimplified.py +2 -1
- spotifywebapipython/models/trackrecommendations.py +5 -5
- spotifywebapipython/models/tracksimplified.py +10 -3
- spotifywebapipython/models/userprofile.py +25 -1
- spotifywebapipython/models/userprofilesimplified.py +4 -5
- spotifywebapipython/models/zeroconfdiscoveryresult.py +599 -0
- spotifywebapipython/models/zeroconfproperty.py +93 -0
- spotifywebapipython/oauthcli/authclient.py +119 -19
- spotifywebapipython/saappmessages.py +21 -1
- spotifywebapipython/sautils.py +88 -1
- spotifywebapipython/spotifyapimessage.py +18 -1
- spotifywebapipython/spotifyauthtoken.py +26 -0
- spotifywebapipython/spotifyclient.py +4302 -516
- spotifywebapipython/spotifyconnectdeviceeventargs.py +67 -0
- spotifywebapipython/spotifydiscovery.py +326 -136
- spotifywebapipython/zeroconfapi/__init__.py +23 -0
- spotifywebapipython/zeroconfapi/authenticationtypes.py +38 -0
- spotifywebapipython/zeroconfapi/blobbuilder.py +370 -0
- spotifywebapipython/zeroconfapi/credentials.py +37 -0
- spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +89 -0
- spotifywebapipython/zeroconfapi/helpers.py +64 -0
- spotifywebapipython/zeroconfapi/spotifyzeroconfapierror.py +108 -0
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +1315 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +619 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfoalias.py +105 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfodrmmediaformat.py +90 -0
- spotifywebapipython/zeroconfapi/zeroconfresponse.py +141 -0
- spotifywebapiPython-1.0.36.dist-info/RECORD +0 -98
- {spotifywebapiPython-1.0.36.dist-info → spotifywebapiPython-1.0.147.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.36.dist-info → spotifywebapiPython-1.0.147.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# our package imports.
|
|
2
|
+
from .models import SpotifyConnectDevice
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SpotifyConnectDeviceEventArgs:
|
|
6
|
+
"""
|
|
7
|
+
Class used by various methods that inform interested parties that a Spotify
|
|
8
|
+
Connect device update has occured.
|
|
9
|
+
|
|
10
|
+
Threadsafety:
|
|
11
|
+
This class is fully thread-safe.
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, device:SpotifyConnectDevice) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Initializes a new instance of the class.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
device (SpotifyConnectDevice):
|
|
19
|
+
Spotify Connect device object that was affected.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# initialize instance.
|
|
23
|
+
self._DeviceObject:SpotifyConnectDevice = device
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def DeviceObject(self) -> SpotifyConnectDevice:
|
|
28
|
+
"""
|
|
29
|
+
Spotify Connect device object that was affected.
|
|
30
|
+
"""
|
|
31
|
+
return self._DeviceObject
|
|
32
|
+
|
|
33
|
+
@DeviceObject.setter
|
|
34
|
+
def DeviceObject(self, value:str) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Sets the DeviceObject property value.
|
|
37
|
+
"""
|
|
38
|
+
if (isinstance(value, SpotifyConnectDevice)):
|
|
39
|
+
self._DeviceObject = value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Returns a string representation of the object.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A string in the form of "SpotifyConnectDeviceEventArgs: {status}".
|
|
48
|
+
"""
|
|
49
|
+
return str.format("SpotifyConnectDeviceEventArgs: {0}", self._DeviceObject or "")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SpotifyConnectDeviceEventHandler:
|
|
53
|
+
"""
|
|
54
|
+
Event handler type definition for an event.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, sender:object, e:SpotifyConnectDeviceEventArgs) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Initializes a new instance of the class.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
sender (object):
|
|
63
|
+
The object which fired the event.
|
|
64
|
+
e (SpotifyConnectDeviceEventArgs):
|
|
65
|
+
Arguments that contain detailed information related to the event,
|
|
66
|
+
and canceling of its processing.
|
|
67
|
+
"""
|
|
@@ -3,13 +3,16 @@ try:
|
|
|
3
3
|
from queue import Queue, Empty
|
|
4
4
|
except ImportError:
|
|
5
5
|
from Queue import Queue, Empty
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
6
8
|
from zeroconf import Zeroconf, ServiceBrowser, ServiceInfo, ServiceStateChange, IPVersion
|
|
7
9
|
|
|
8
10
|
# our package imports.
|
|
9
11
|
from .saappmessages import SAAppMessages
|
|
12
|
+
from .spotifyapierror import SpotifyApiError
|
|
10
13
|
from .sautils import export
|
|
11
14
|
from .models.device import Device
|
|
12
|
-
from .
|
|
15
|
+
from .models import ZeroconfDiscoveryResult, ZeroconfProperty
|
|
13
16
|
|
|
14
17
|
# get smartinspect logger reference; create a new session for this module name.
|
|
15
18
|
from smartinspectpython.siauto import SIAuto, SILevel, SISession
|
|
@@ -30,42 +33,41 @@ class SpotifyDiscovery:
|
|
|
30
33
|
Click the **Sample Code** links in the individual methods for sample code examples.
|
|
31
34
|
"""
|
|
32
35
|
|
|
33
|
-
def __init__(
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
zeroconfClient:Zeroconf=None,
|
|
39
|
+
printToConsole:bool=False
|
|
40
|
+
) -> None:
|
|
34
41
|
"""
|
|
35
42
|
Initializes a new instance of the class.
|
|
36
43
|
|
|
37
44
|
Args:
|
|
38
|
-
|
|
39
|
-
A
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
areDevicesVerified (bool):
|
|
43
|
-
True to create a `SpotifyClient` instance for discovered devices, which
|
|
44
|
-
verifies that the device can be accessed and basic information obtained
|
|
45
|
-
about its capabilities; otherwise, False to just identify the IPV4 Address,
|
|
46
|
-
Port, and Device Name.
|
|
47
|
-
Default is False.
|
|
45
|
+
zeroconfClient (Zeroconf)
|
|
46
|
+
A Zeroconf client instance that will be used to discover Spotify Connect devices,
|
|
47
|
+
or null to create a new instance of Zeroconf.
|
|
48
|
+
Default is null.
|
|
48
49
|
printToConsole (bool):
|
|
49
50
|
True to print discovered device information to the console as the devices
|
|
50
51
|
are discovered; otherwise, False to not print anything to the console.
|
|
51
52
|
Default is False.
|
|
52
|
-
|
|
53
|
-
Specify False for the `areDevicesVerified` argument if you want to speed up
|
|
54
|
-
device discovery, as it takes extra time to verify device connections as they
|
|
55
|
-
are discovered.
|
|
56
53
|
"""
|
|
57
54
|
# validations.
|
|
58
|
-
if
|
|
59
|
-
raise SpotifyApiError(SAAppMessages.ARGUMENT_TYPE_ERROR % ("__init__", '
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
if zeroconfClient is not None and (not isinstance(zeroconfClient, Zeroconf)):
|
|
56
|
+
raise SpotifyApiError(SAAppMessages.ARGUMENT_TYPE_ERROR % ("__init__", 'zeroconfClient', 'Zeroconf', type(zeroconfClient).__name__), logsi=_logsi)
|
|
57
|
+
|
|
58
|
+
# create the zeroconf client if one was not specified.
|
|
59
|
+
if zeroconfClient is None:
|
|
60
|
+
_logsi.LogVerbose("Creating new Zeroconf instance for discovery")
|
|
61
|
+
zeroconfClient = Zeroconf()
|
|
62
|
+
else:
|
|
63
|
+
_logsi.LogObject(SILevel.Verbose, "Using existing Zeroconf instance for discovery", zeroconfClient)
|
|
62
64
|
|
|
63
65
|
# initialize instance properties.
|
|
64
|
-
self._AreDevicesVerified:bool = areDevicesVerified
|
|
65
66
|
self._DiscoveredDeviceNames:dict = {}
|
|
67
|
+
self._DiscoveryResults:list[ZeroconfDiscoveryResult] = []
|
|
66
68
|
self._PrintToConsole:bool = printToConsole
|
|
67
|
-
self.
|
|
68
|
-
self.
|
|
69
|
+
self._ZeroconfClient = zeroconfClient
|
|
70
|
+
self._Zeroconf_Lock = threading.Lock()
|
|
69
71
|
|
|
70
72
|
|
|
71
73
|
def __getitem__(self, key):
|
|
@@ -89,29 +91,25 @@ class SpotifyDiscovery:
|
|
|
89
91
|
return self.ToString()
|
|
90
92
|
|
|
91
93
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
def ContainsId(self, value:str) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Returns True if the `DiscoveryResults` collection contains the specified id value;
|
|
97
|
+
otherwise, False.
|
|
94
98
|
"""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
If True, then the `VerifiedDevices` property will contain a `Device`
|
|
101
|
-
instance for each device that was detected as part of the discovery process
|
|
102
|
-
and was found in the Spotify User's player devices list.
|
|
103
|
-
"""
|
|
104
|
-
return self._AreDevicesVerified
|
|
105
|
-
|
|
99
|
+
item:ZeroconfDiscoveryResult = self.GetResultById(value)
|
|
100
|
+
if item is not None:
|
|
101
|
+
return True
|
|
102
|
+
return False
|
|
103
|
+
|
|
106
104
|
|
|
107
105
|
@property
|
|
108
106
|
def DiscoveredDeviceNames(self) -> dict:
|
|
109
107
|
"""
|
|
110
108
|
A dictionary of discovered device names that were detected by the discovery process.
|
|
111
109
|
|
|
112
|
-
Dictionary keys will be in the form of "address:port", where "
|
|
113
|
-
|
|
114
|
-
is listening on.
|
|
110
|
+
Dictionary keys will be in the form of "'name' (address:port)", where "name" is the
|
|
111
|
+
device name, "address" is the device server address and the "port" is the ipv4 port
|
|
112
|
+
number the Spotify Connect device is listening on.
|
|
115
113
|
|
|
116
114
|
Dictionary values will be the device names (e.g. "Web Player (Chrome)", etc.). This SHOULD
|
|
117
115
|
match the name of the device as displayed in the Spotify App, but is not guaranteed.
|
|
@@ -120,28 +118,159 @@ class SpotifyDiscovery:
|
|
|
120
118
|
|
|
121
119
|
|
|
122
120
|
@property
|
|
123
|
-
def
|
|
121
|
+
def DiscoveryResults(self) -> list[ZeroconfDiscoveryResult]:
|
|
122
|
+
"""
|
|
123
|
+
An array of `ZeroconfDiscoveryResult` items that contain discovery details for
|
|
124
|
+
each service that was discovered.
|
|
125
|
+
"""
|
|
126
|
+
return self._DiscoveryResults
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def GetIndexByKey(self, value:str) -> int:
|
|
130
|
+
"""
|
|
131
|
+
Returns the index of the `DiscoveryResults` collection that contains
|
|
132
|
+
the specified device key value; otherwise, -1.
|
|
124
133
|
"""
|
|
125
|
-
|
|
134
|
+
result:int = -1
|
|
135
|
+
if value is None:
|
|
136
|
+
return result
|
|
126
137
|
|
|
127
|
-
|
|
138
|
+
# convert case for comparison.
|
|
139
|
+
value = value.lower()
|
|
140
|
+
|
|
141
|
+
# process all discovered devices.
|
|
142
|
+
for i in range(len(self._DiscoveryResults)):
|
|
143
|
+
if (self._DiscoveryResults[i].Key.lower() == value):
|
|
144
|
+
result = i
|
|
145
|
+
break
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def GetResultById(self, value:str) -> bool:
|
|
150
|
+
"""
|
|
151
|
+
Returns a `ZeroconfDiscoveryResult` instance if the `DiscoveryResults` collection contains
|
|
152
|
+
the specified device id value; otherwise, None.
|
|
153
|
+
"""
|
|
154
|
+
result:ZeroconfDiscoveryResult = None
|
|
155
|
+
if value is None:
|
|
156
|
+
return result
|
|
128
157
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
is listening on.
|
|
158
|
+
# convert case for comparison.
|
|
159
|
+
value = value.lower()
|
|
132
160
|
|
|
133
|
-
|
|
134
|
-
|
|
161
|
+
# process all discovered devices.
|
|
162
|
+
item:ZeroconfDiscoveryResult
|
|
163
|
+
for item in self._DiscoveryResults:
|
|
164
|
+
if (item.Id.lower() == value):
|
|
165
|
+
result = item
|
|
166
|
+
break
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def ZeroconfClient(self) -> Zeroconf:
|
|
172
|
+
"""
|
|
173
|
+
Zeroconf client instance that will be used to discover Spotify Connect devices.
|
|
135
174
|
"""
|
|
136
|
-
return self.
|
|
175
|
+
return self._ZeroconfClient
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _GetZeroconfDiscoveryResult(
|
|
179
|
+
self,
|
|
180
|
+
zeroconf:Zeroconf,
|
|
181
|
+
serviceType:str,
|
|
182
|
+
serviceName:str,
|
|
183
|
+
serviceStateChange:ServiceStateChange,
|
|
184
|
+
serviceStateChangeDesc:str
|
|
185
|
+
) -> ZeroconfDiscoveryResult:
|
|
186
|
+
"""
|
|
187
|
+
Builds a `ZeroconfDiscoveryResult` instance from Zeroconf Service Information data.
|
|
188
|
+
"""
|
|
189
|
+
result:ZeroconfDiscoveryResult = None
|
|
137
190
|
|
|
191
|
+
try:
|
|
138
192
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
193
|
+
# trace.
|
|
194
|
+
_logsi.EnterMethod(SILevel.Debug)
|
|
195
|
+
|
|
196
|
+
# get device service information instance; if none, then we are done.
|
|
197
|
+
deviceName:str = (serviceName.split(".")[0])
|
|
198
|
+
serviceInfo:ServiceInfo = zeroconf.get_service_info(serviceType, serviceName)
|
|
199
|
+
if serviceInfo is None:
|
|
200
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification ignored: \"%s\" (%s:%s) - no serviceinfo data" % (serviceName, serviceType, serviceStateChange))
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
# get list of displayable (parsed) ipv4 addresses; if none, then we are done.
|
|
204
|
+
ipAddressList = serviceInfo.parsed_addresses(IPVersion.V4Only)
|
|
205
|
+
if (ipAddressList is None):
|
|
206
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification ignored: \"%s\" (%s:%s) - no IP address list" % (serviceName, serviceType, serviceStateChange))
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
# trace.
|
|
210
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf service details: \"%s\" %s (serviceinfo object)" % (deviceName, ipAddressList), serviceInfo)
|
|
211
|
+
|
|
212
|
+
# create new discovery result instance.
|
|
213
|
+
result:ZeroconfDiscoveryResult = ZeroconfDiscoveryResult()
|
|
214
|
+
result.DeviceName = deviceName
|
|
215
|
+
result.Domain = '.local'
|
|
216
|
+
for item in ipAddressList:
|
|
217
|
+
result.HostIpAddresses.append(item)
|
|
218
|
+
result.HostIpPort = serviceInfo.port
|
|
219
|
+
result.HostTTL = serviceInfo.host_ttl
|
|
220
|
+
result.Key = serviceInfo.key
|
|
221
|
+
result.Name = serviceInfo.name
|
|
222
|
+
result.OtherTTL = serviceInfo.other_ttl
|
|
223
|
+
result.Priority = serviceInfo.priority
|
|
224
|
+
result.Server = serviceInfo.server
|
|
225
|
+
result.ServerKey = serviceInfo.server_key
|
|
226
|
+
result.ServiceInfo = serviceInfo
|
|
227
|
+
result.ServiceType = serviceType
|
|
228
|
+
result.Weight = serviceInfo.weight
|
|
229
|
+
result.Id = "\"%s\" (%s:%s)" % (result.DeviceName, result.HostIpAddress, result.HostIpPort)
|
|
230
|
+
|
|
231
|
+
# process service info propertys.
|
|
232
|
+
# note that the property keys and values must first be decoded to utf-8 encoding.
|
|
233
|
+
if serviceInfo.properties is not None:
|
|
234
|
+
dspProperties:dict = {}
|
|
235
|
+
for key, value in serviceInfo.properties.items():
|
|
236
|
+
keyStr = key.decode('utf-8')
|
|
237
|
+
valueStr = value.decode('utf-8')
|
|
238
|
+
result.Properties.append(ZeroconfProperty(keyStr, valueStr))
|
|
239
|
+
dspProperties[keyStr] = valueStr
|
|
240
|
+
# check for Spotify Connect common properties.
|
|
241
|
+
if keyStr.lower() == 'cpath':
|
|
242
|
+
result.SpotifyConnectCPath = valueStr
|
|
243
|
+
elif keyStr.lower() == 'version':
|
|
244
|
+
result.SpotifyConnectVersion = valueStr
|
|
245
|
+
_logsi.LogDictionary(SILevel.Verbose, "Spotify Connect Zeroconf service details: \"%s\" %s (serviceinfo properties)" % (deviceName, ipAddressList), dspProperties)
|
|
246
|
+
|
|
247
|
+
# trace.
|
|
248
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf Discovery Result: %s (object)" % (result.Id), result, excludeNonPublic=True)
|
|
249
|
+
|
|
250
|
+
# return discovery result.
|
|
251
|
+
return result
|
|
252
|
+
|
|
253
|
+
except Exception as ex:
|
|
254
|
+
|
|
255
|
+
# trace.
|
|
256
|
+
_logsi.LogException("Could not load ZeroconfDiscoveryResult instance from Zeroconf serviceinfo object.", ex, logToSystemLogger=False)
|
|
257
|
+
|
|
258
|
+
# ignore exception
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
finally:
|
|
262
|
+
|
|
263
|
+
# trace.
|
|
264
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _OnServiceStateChange(
|
|
268
|
+
self,
|
|
269
|
+
zeroconf:Zeroconf,
|
|
270
|
+
service_type:str,
|
|
271
|
+
name:str,
|
|
272
|
+
state_change:ServiceStateChange
|
|
273
|
+
) -> None:
|
|
145
274
|
"""
|
|
146
275
|
Called by the zeroconf ServiceBrowser when a service state has changed (e.g. added,
|
|
147
276
|
removed, or updated). In our case, we only care about devices that were added.
|
|
@@ -149,84 +278,134 @@ class SpotifyDiscovery:
|
|
|
149
278
|
IMPORTANT - This method is executed on a different thread, so be careful about
|
|
150
279
|
multi-threaded operations!
|
|
151
280
|
"""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
281
|
+
|
|
282
|
+
# this method can be tested by issuing the following commands from an MS-DOS
|
|
283
|
+
# command prompt to simulate Zeroconf registration updates.
|
|
284
|
+
|
|
285
|
+
# - add a new Spotify Connect instance registration:
|
|
286
|
+
# > dns-sd -R "MyTestSC1" _spotify-connect._tcp. local 8080 cpath=/zcXXX version=1.0
|
|
155
287
|
|
|
156
|
-
#
|
|
157
|
-
if
|
|
158
|
-
|
|
288
|
+
# - update a new Spotify Connect instance registration;
|
|
289
|
+
# - if you change the name, service type, or port number, a NEW registration is created!
|
|
290
|
+
# - for updates, the only thing you can really update is the TXT record properties.
|
|
291
|
+
# > dns-sd -R "MyTestSC1" _spotify-connect._tcp. local 8080 cpath=/zcYYY version=1.0
|
|
292
|
+
|
|
293
|
+
# - remove an existing Spotify Connect instance registration;
|
|
294
|
+
# > the only way to remove an entry is to kill the process the entry was created under!
|
|
295
|
+
|
|
296
|
+
# enter the following section one thread at a time (threadsafe).
|
|
297
|
+
with self._Zeroconf_Lock:
|
|
298
|
+
|
|
299
|
+
# copy passed parameters to thread-safe storage.
|
|
300
|
+
serviceType:str = service_type
|
|
301
|
+
serviceName:str = name
|
|
302
|
+
serviceStateChange:ServiceStateChange = state_change
|
|
303
|
+
serviceStateChangeDesc:str = None
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
|
|
307
|
+
# trace.
|
|
308
|
+
_logsi.EnterMethod(SILevel.Debug)
|
|
309
|
+
|
|
310
|
+
# process by the state change value.
|
|
311
|
+
if (serviceStateChange is ServiceStateChange.Added) \
|
|
312
|
+
or (serviceStateChange is ServiceStateChange.Updated):
|
|
313
|
+
|
|
314
|
+
# set service state change description.
|
|
315
|
+
serviceStateChangeDesc = "Added"
|
|
316
|
+
if (serviceStateChange is ServiceStateChange.Updated):
|
|
317
|
+
serviceStateChangeDesc = "Updated"
|
|
318
|
+
|
|
319
|
+
# trace.
|
|
320
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, serviceStateChangeDesc))
|
|
321
|
+
|
|
322
|
+
# build discovery result instance from service state information.
|
|
323
|
+
# if nothing returned, then don't bother!
|
|
324
|
+
result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(zeroconf, serviceType, serviceName, serviceStateChange, serviceStateChangeDesc)
|
|
325
|
+
if (result is None):
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
# add device name to list of registered names.
|
|
329
|
+
self._DiscoveredDeviceNames[result.Id] = result.DeviceName
|
|
330
|
+
|
|
331
|
+
# add / update discovered results by serviceinfo key value.
|
|
332
|
+
# we use the serviceinfo KEY value (not SERVERKEY) since some manufacturers use random ip port
|
|
333
|
+
# numbers, and the port number could have been updated. in this case, the key SHOULD remain the same.
|
|
334
|
+
# note that a different serviceinfo key SHOULD be created for speaker groups; at least they are for BOSE devices.
|
|
335
|
+
idx:int = self.GetIndexByKey(result.Key)
|
|
336
|
+
if (idx == -1):
|
|
337
|
+
self._DiscoveryResults.append(result) # add new result
|
|
338
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf service discovery result was added: %s (%s)" % (result.Id, serviceStateChangeDesc))
|
|
339
|
+
else:
|
|
340
|
+
self._DiscoveryResults[idx] = result # update existing result
|
|
341
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf service discovery result was updated: %s (%s)" % (result.Id, serviceStateChangeDesc))
|
|
342
|
+
|
|
343
|
+
# trace.
|
|
344
|
+
if (self._PrintToConsole == True):
|
|
345
|
+
print("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
|
|
346
|
+
|
|
347
|
+
elif (serviceStateChange is ServiceStateChange.Removed):
|
|
348
|
+
|
|
349
|
+
# set service state change description.
|
|
350
|
+
serviceStateChangeDesc = "Removed"
|
|
351
|
+
|
|
352
|
+
# trace.
|
|
353
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, serviceStateChangeDesc))
|
|
354
|
+
|
|
355
|
+
# build discovery result instance from service state information.
|
|
356
|
+
# if nothing returned, then don't bother!
|
|
357
|
+
result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(zeroconf, serviceType, serviceName, serviceStateChange, serviceStateChangeDesc)
|
|
358
|
+
if (result is None):
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
# remove device name from list of registered names.
|
|
362
|
+
self._DiscoveredDeviceNames.pop(result.Id, None)
|
|
363
|
+
|
|
364
|
+
# remove device from results collection;
|
|
365
|
+
# the reversed() function creates an iterator that traverses the list in reverse order.
|
|
366
|
+
# This ensures that removing an element doesn't affect the indices of the subsequent
|
|
367
|
+
# elements we're going to iterate over.
|
|
368
|
+
resultCompare:str = result.Id.lower()
|
|
369
|
+
for i in reversed(range(len(self._DiscoveryResults))):
|
|
370
|
+
if (self._DiscoveryResults[i].Id.lower() == resultCompare):
|
|
371
|
+
|
|
372
|
+
# remove discovery result.
|
|
373
|
+
result = self._DiscoveryResults.pop(i)
|
|
374
|
+
|
|
375
|
+
# trace.
|
|
376
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
|
|
377
|
+
if (self._PrintToConsole == True):
|
|
378
|
+
print("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
|
|
379
|
+
|
|
380
|
+
break
|
|
381
|
+
|
|
382
|
+
except Exception as ex:
|
|
159
383
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if serviceInfo is None:
|
|
163
|
-
return
|
|
384
|
+
# trace.
|
|
385
|
+
_logsi.LogException("Unhandled exception occured while processing Spotify Connect Zeroconf discovery service notification (%s)" % (serviceStateChange), ex, logToSystemLogger=False)
|
|
164
386
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (ipAddressList is None):
|
|
168
|
-
return
|
|
387
|
+
# ignore exception
|
|
388
|
+
return None
|
|
169
389
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
print("Discovered Spotify Connect device: %s - %s" % (deviceKey, deviceName))
|
|
178
|
-
_logsi.LogObject(SILevel.Verbose, "Discovered Spotify Connect device ServiceInfo: %s - %s" % (deviceKey, deviceName), serviceInfo)
|
|
179
|
-
|
|
180
|
-
# # trace service info propertys if desired.
|
|
181
|
-
# # note that the property keys and values must first be decoded to utf-8 encoding.
|
|
182
|
-
# if _logsi.IsOn(SILevel.Verbose):
|
|
183
|
-
# if serviceInfo.properties is not None:
|
|
184
|
-
# dspProperties:dict = {}
|
|
185
|
-
# for key, value in serviceInfo.properties.items():
|
|
186
|
-
# dspProperties[key.decode('utf-8')] = value.decode('utf-8')
|
|
187
|
-
# _logsi.LogDictionary(SILevel.Verbose, "Discovered Spotify Connect device ServiceInfo.Properties: '%s' (%s:%i)" % (deviceName, deviceIpAddress, devicePort), dspProperties)
|
|
188
|
-
|
|
189
|
-
# add the device name to the list (if not already added) using its
|
|
190
|
-
# ipv4 address and port number as the key.
|
|
191
|
-
if deviceKey not in self._DiscoveredDeviceNames.keys():
|
|
192
|
-
self._DiscoveredDeviceNames[deviceKey] = deviceName
|
|
193
|
-
|
|
194
|
-
# are we verifying devices connections? if so, then create a SpotifyClient
|
|
195
|
-
# object, which will verify the connection and gather basic capabilities of the device.
|
|
196
|
-
# we will also add the SpotifyClient instance using the same key as the device name list.
|
|
197
|
-
if self._AreDevicesVerified == True:
|
|
198
|
-
|
|
199
|
-
if deviceKey not in self._VerifiedDevices.keys():
|
|
200
|
-
|
|
201
|
-
#device:SpotifyClient = SpotifyClient(host=deviceIpAddress, port=devicePort)
|
|
202
|
-
|
|
203
|
-
# get Spotify Connect player device by it's Name value.
|
|
204
|
-
device:Device = self._SpotifyClient.GetPlayerDevice(deviceName)
|
|
205
|
-
if device is not None:
|
|
206
|
-
self._VerifiedDevices[deviceKey] = device
|
|
207
|
-
|
|
208
|
-
elif (serviceStateChange is ServiceStateChange.Removed):
|
|
209
|
-
_logsi.LogVerbose("Discovered Spotify Connect device removal (ignored): '%s' (%s:%s)", serviceName, serviceType, serviceStateChange)
|
|
210
|
-
pass
|
|
211
|
-
|
|
212
|
-
elif (serviceStateChange is ServiceStateChange.Updated):
|
|
213
|
-
_logsi.LogVerbose("Discovered Spotify Connect device update (ignored): '%s' (%s:%s)", serviceName, serviceType, serviceStateChange)
|
|
214
|
-
pass
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def DiscoverDevices(self, timeout:int=5) -> dict:
|
|
390
|
+
finally:
|
|
391
|
+
|
|
392
|
+
# trace.
|
|
393
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def DiscoverDevices(self, timeout:float=2) -> dict:
|
|
218
397
|
"""
|
|
219
398
|
Discover Spotify Connect devices on the local network via the
|
|
220
399
|
ZeroConf (aka MDNS) service.
|
|
221
400
|
|
|
222
401
|
Args:
|
|
223
|
-
timeout (
|
|
402
|
+
timeout (float):
|
|
224
403
|
Maximum amount of time to wait (in seconds) for the
|
|
225
404
|
discovery to complete.
|
|
226
|
-
Default is
|
|
405
|
+
Default is 2 seconds.
|
|
227
406
|
|
|
228
407
|
Returns:
|
|
229
|
-
A dictionary of
|
|
408
|
+
A dictionary of `ZeroconfDiscoveryResult` objects.
|
|
230
409
|
|
|
231
410
|
<details>
|
|
232
411
|
<summary>Sample Code</summary>
|
|
@@ -235,26 +414,43 @@ class SpotifyDiscovery:
|
|
|
235
414
|
```
|
|
236
415
|
</details>
|
|
237
416
|
"""
|
|
417
|
+
browser:ServiceBrowser = None
|
|
418
|
+
|
|
238
419
|
# using Queue as a timer (timeout functionality).
|
|
239
420
|
discoveryQueueTimer = Queue()
|
|
240
|
-
|
|
241
|
-
# create the zeroconf service and our listener callback.
|
|
242
|
-
zeroconf:Zeroconf = Zeroconf()
|
|
243
|
-
|
|
244
|
-
# create the zeroconf service browser that will start device discovery.
|
|
245
|
-
_logsi.LogVerbose("Discovery of Spotify Connect devices via Zeroconf is starting ...")
|
|
246
|
-
ServiceBrowser(zeroconf, "_spotify-connect._tcp.local.", handlers=[self._OnServiceStateChange])
|
|
247
|
-
|
|
421
|
+
|
|
248
422
|
try:
|
|
249
|
-
|
|
423
|
+
|
|
424
|
+
# create the zeroconf service browser that will start device discovery.
|
|
425
|
+
_logsi.LogVerbose("Discovery of Spotify Connect devices via Zeroconf is starting")
|
|
426
|
+
browser = ServiceBrowser(self._ZeroconfClient, "_spotify-connect._tcp.local.", handlers=[self._OnServiceStateChange])
|
|
427
|
+
|
|
428
|
+
# trace.
|
|
429
|
+
_logsi.LogObject(SILevel.Verbose, "ZeroconfClient object after ServiceBrowser creation", self._ZeroconfClient)
|
|
430
|
+
|
|
431
|
+
# give the ServiceBrowser time to discover.
|
|
250
432
|
discoveryQueueTimer.get(timeout=timeout)
|
|
251
433
|
|
|
252
434
|
except Empty:
|
|
253
435
|
|
|
254
436
|
# this is not really an exception, but more of an indicator that
|
|
255
437
|
# the timeout has been reached.
|
|
256
|
-
_logsi.LogVerbose("Discovery of Spotify Connect devices via Zeroconf has
|
|
438
|
+
_logsi.LogVerbose("Discovery of Spotify Connect devices via Zeroconf has reached its timeout of %f seconds" % timeout)
|
|
439
|
+
|
|
440
|
+
finally:
|
|
441
|
+
|
|
442
|
+
if (browser is not None):
|
|
443
|
+
|
|
444
|
+
# remove the zeroconf service browser with associated listener and free resources.
|
|
445
|
+
_logsi.LogVerbose("Releasing Zeroconf ServiceBrowser instance")
|
|
446
|
+
browser.cancel()
|
|
447
|
+
del browser
|
|
448
|
+
_logsi.LogObject(SILevel.Verbose, "ZeroconfClient object after ServiceBrowser resources released", self._ZeroconfClient)
|
|
449
|
+
|
|
450
|
+
# trace.
|
|
451
|
+
_logsi.LogVerbose("Discovery of Spotify Connect devices via Zeroconf has ended")
|
|
257
452
|
|
|
453
|
+
# return result to caller.
|
|
258
454
|
return self._DiscoveredDeviceNames
|
|
259
455
|
|
|
260
456
|
|
|
@@ -273,12 +469,6 @@ class SpotifyDiscovery:
|
|
|
273
469
|
if includeItems == True:
|
|
274
470
|
|
|
275
471
|
for key, deviceName in self._DiscoveredDeviceNames.items():
|
|
276
|
-
|
|
277
|
-
device:Device
|
|
278
|
-
verifiedStatus:str = ''
|
|
279
|
-
device = self._VerifiedDevices.get(key, None)
|
|
280
|
-
if device is not None:
|
|
281
|
-
verifiedStatus = ' (verified - ID=%s)' % device.Id
|
|
282
|
-
msg = "%s\n- %s - %s %s" % (msg, key, deviceName, verifiedStatus)
|
|
472
|
+
msg = "%s\n- %s" % (msg, key)
|
|
283
473
|
|
|
284
474
|
return msg
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# import all classes from the namespace.
|
|
2
|
+
from .authenticationtypes import AuthenticationTypes
|
|
3
|
+
from .blobbuilder import BlobBuilder
|
|
4
|
+
from .credentials import Credentials
|
|
5
|
+
from .spotifyzeroconfapierror import SpotifyZeroconfApiError
|
|
6
|
+
from .zeroconfconnect import ZeroconfConnect
|
|
7
|
+
from .zeroconfresponse import ZeroconfResponse
|
|
8
|
+
from .zeroconfgetinfo import ZeroconfGetInfo
|
|
9
|
+
from .zeroconfgetinfoalias import ZeroconfGetInfoAlias
|
|
10
|
+
from .zeroconfgetinfodrmmediaformat import ZeroconfGetInfoDrmMediaFormat
|
|
11
|
+
|
|
12
|
+
# all classes to import when "import *" is specified.
|
|
13
|
+
__all__ = [
|
|
14
|
+
'AuthenticationTypes',
|
|
15
|
+
'BlobBuilder',
|
|
16
|
+
'Credentials',
|
|
17
|
+
'SpotifyZeroconfApiError',
|
|
18
|
+
'ZeroconfConnect',
|
|
19
|
+
'ZeroconfGetInfo',
|
|
20
|
+
'ZeroconfGetInfoAlias',
|
|
21
|
+
'ZeroconfGetInfoDrmMediaFormat',
|
|
22
|
+
'ZeroconfResponse',
|
|
23
|
+
]
|