spotifywebapiPython 1.0.45__py3-none-any.whl → 1.0.47__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.45.dist-info → spotifywebapiPython-1.0.47.dist-info}/METADATA +6 -6
- {spotifywebapiPython-1.0.45.dist-info → spotifywebapiPython-1.0.47.dist-info}/RECORD +34 -28
- spotifywebapipython/const.py +1 -1
- spotifywebapipython/docs/__init__.html +21 -3
- spotifywebapipython/docs/index.html +20 -2
- spotifywebapipython/docs/search.js +1 -1
- spotifywebapipython/docs/spotifywebapiPython/const.html +3 -3
- spotifywebapipython/docs/spotifywebapiPython/saappmessages.html +22 -2
- spotifywebapipython/docs/spotifywebapiPython/sautils.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifyapierror.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifyapimessage.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifyauthtoken.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifyclient.html +2 -200
- spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +2 -28
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapierror.html +2 -2
- spotifywebapipython/models/__init__.py +0 -10
- spotifywebapipython/saappmessages.py +7 -1
- spotifywebapipython/spotifyclient.py +0 -249
- spotifywebapipython/spotifydiscovery.py +0 -46
- spotifywebapipython/zeroconfapi/__init__.py +21 -0
- spotifywebapipython/{models/zeroconfblobbuilder.py → zeroconfapi/blobbuilder.py} +10 -58
- spotifywebapipython/zeroconfapi/credentials.py +18 -0
- spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +24 -0
- spotifywebapipython/zeroconfapi/helpers.py +30 -0
- spotifywebapipython/zeroconfapi/spotifyzeroconfapierror.py +108 -0
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +562 -0
- spotifywebapipython/{models → zeroconfapi}/zeroconfgetinfo.py +21 -7
- spotifywebapipython/{models → zeroconfapi}/zeroconfgetinfoalias.py +1 -1
- spotifywebapipython/{models → zeroconfapi}/zeroconfgetinfodrmmediaformat.py +1 -1
- spotifywebapipython/{models → zeroconfapi}/zeroconfresponse.py +3 -3
- {spotifywebapiPython-1.0.45.dist-info → spotifywebapiPython-1.0.47.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.45.dist-info → spotifywebapiPython-1.0.47.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.45.dist-info → spotifywebapiPython-1.0.47.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# external package imports.
|
|
2
|
+
from smartinspectpython.sisession import SISession
|
|
3
|
+
|
|
4
|
+
# our package imports.
|
|
5
|
+
from ..sautils import export
|
|
6
|
+
from ..saappmessages import SAAppMessages
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@export
|
|
10
|
+
class SpotifyZeroconfApiError(Exception):
|
|
11
|
+
"""
|
|
12
|
+
Exception thrown when a Spotify ZeroConf API Request Error occurs.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, status:int, message:str, methodName:str, reason:str, logsi:SISession=None) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Initializes a new instance of the class.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
status (int):
|
|
21
|
+
HTTP status code that is also returned in the response header.
|
|
22
|
+
For further information, see Response Status Codes.
|
|
23
|
+
message (str):
|
|
24
|
+
A short description of the cause of the error.
|
|
25
|
+
methodName (str):
|
|
26
|
+
Name of the client method that executed the request.
|
|
27
|
+
reason (str):
|
|
28
|
+
HTTP reason code for the error.
|
|
29
|
+
logsi (SISession):
|
|
30
|
+
Trace session object that this exception will be logged to, or null to bypass trace logging.
|
|
31
|
+
Default is None.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# initialize base class.
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
|
|
37
|
+
# initialize class instance.
|
|
38
|
+
self._HttpReason:str = reason
|
|
39
|
+
self._Message:str = message
|
|
40
|
+
self._MethodName:str = methodName
|
|
41
|
+
self._Status:int = status
|
|
42
|
+
|
|
43
|
+
# trace.
|
|
44
|
+
if logsi is not None:
|
|
45
|
+
if (isinstance(logsi, SISession)):
|
|
46
|
+
logsi.LogException(str(self), self, logToSystemLogger=False)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def __repr__(self) -> str:
|
|
50
|
+
return self.ToString()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
return self.ToString()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def HttpReason(self) -> str:
|
|
59
|
+
"""
|
|
60
|
+
HTTP reason code for the error.
|
|
61
|
+
|
|
62
|
+
Example: `Bad Request`.
|
|
63
|
+
"""
|
|
64
|
+
return self._HttpReason
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def Message(self) -> str:
|
|
69
|
+
"""
|
|
70
|
+
A short description of the cause of the error.
|
|
71
|
+
|
|
72
|
+
Example: `invalid id`.
|
|
73
|
+
"""
|
|
74
|
+
return self._Message
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def MethodName(self) -> str:
|
|
79
|
+
"""
|
|
80
|
+
The method being executed when the error occured.
|
|
81
|
+
|
|
82
|
+
Example: `getInfo`.
|
|
83
|
+
"""
|
|
84
|
+
return self._MethodName
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def Status(self) -> int:
|
|
89
|
+
"""
|
|
90
|
+
HTTP status code that is also returned in the response header.
|
|
91
|
+
For further information, see Response Status Codes.
|
|
92
|
+
|
|
93
|
+
Example: `400`
|
|
94
|
+
"""
|
|
95
|
+
return self._Status
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def ToString(self) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Returns a UI-friendly string representation of the class.
|
|
101
|
+
"""
|
|
102
|
+
msg:str = SAAppMessages.MSG_SPOTIFY_ZEROCONF_API_ERROR.format(
|
|
103
|
+
methodname=self._MethodName,
|
|
104
|
+
status=self._Status,
|
|
105
|
+
httpreason=self._HttpReason,
|
|
106
|
+
message=self._Message
|
|
107
|
+
)
|
|
108
|
+
return msg
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
# external package imports.
|
|
2
|
+
import json
|
|
3
|
+
import requests
|
|
4
|
+
from requests.models import Response
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
# our package imports.
|
|
8
|
+
from .blobbuilder import BlobBuilder
|
|
9
|
+
from .credentials import Credentials
|
|
10
|
+
from .helpers import int_to_b64str
|
|
11
|
+
from .spotifyzeroconfapierror import SpotifyZeroconfApiError
|
|
12
|
+
from .zeroconfresponse import ZeroconfResponse
|
|
13
|
+
from .zeroconfgetinfo import ZeroconfGetInfo
|
|
14
|
+
from ..saappmessages import SAAppMessages
|
|
15
|
+
from ..sautils import export
|
|
16
|
+
from ..spotifyapierror import SpotifyApiError
|
|
17
|
+
|
|
18
|
+
# get smartinspect logger reference; create a new session for this module name.
|
|
19
|
+
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SISourceId
|
|
20
|
+
import logging
|
|
21
|
+
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
|
22
|
+
if (_logsi == None):
|
|
23
|
+
_logsi = SIAuto.Si.AddSession(__name__, True)
|
|
24
|
+
_logsi.SystemLogger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@export
|
|
28
|
+
class ZeroconfConnect:
|
|
29
|
+
"""
|
|
30
|
+
Interfaces with the Spotify Connect Zeroconf API to add and remove a device to / from the
|
|
31
|
+
Spotify Connect active device list.
|
|
32
|
+
|
|
33
|
+
More information about Spotify Connect Zeroconf API can be found here:
|
|
34
|
+
https://developer.spotify.com/documentation/commercial-hardware/implementation/guides/zeroconf
|
|
35
|
+
|
|
36
|
+
Inspiration and some of the blob encryption encoding was derived from the [zerospot](https://github.com/maraid/zerospot)
|
|
37
|
+
GitHub repository.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self,
|
|
41
|
+
hostIpv4Address:str,
|
|
42
|
+
hostIpPort:str,
|
|
43
|
+
cpath:str,
|
|
44
|
+
version:str='1.0',
|
|
45
|
+
useSSL:bool=False
|
|
46
|
+
) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Initializes a new instance of the class.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
hostIpv4Address (str):
|
|
52
|
+
IPV4 address (as a string) at which the Spotify Connect Zeroconf API can be reached
|
|
53
|
+
on the Spotify Connect device (e.g. "192.168.1.81").
|
|
54
|
+
hostIpPort (int):
|
|
55
|
+
Port number (as an integer) at which the Spotify Connect Zeroconf API can be reached
|
|
56
|
+
on the Spotify Connect device (e.g. "8200").
|
|
57
|
+
cpath (str):
|
|
58
|
+
Spotify Connect Zeroconf API CPath property value (e.g. "/zc").
|
|
59
|
+
version (str):
|
|
60
|
+
Spotify Connect Zeroconf API version number that the device supports (e.g. "2.10.0").
|
|
61
|
+
Default is '1.0'.
|
|
62
|
+
useSSL (bool):
|
|
63
|
+
True if the host device utilizes HTTPS Secure Sockets Layer (SSL) support;
|
|
64
|
+
otherwise, False to utilize HTTP.
|
|
65
|
+
Default is False (HTTP).
|
|
66
|
+
|
|
67
|
+
The URI value consists of an IP Address, port, CPath, and version value that are used to send
|
|
68
|
+
requests to / receive responses from a headless Spotify Connect device. These values can be
|
|
69
|
+
obtained from a Zeroconf discovery process.
|
|
70
|
+
"""
|
|
71
|
+
# validations.
|
|
72
|
+
if (version is None):
|
|
73
|
+
version = '1.0'
|
|
74
|
+
|
|
75
|
+
# initialize storage.
|
|
76
|
+
self._CPath:str = cpath
|
|
77
|
+
self._HostIpPort:int = hostIpPort
|
|
78
|
+
self._HostIpv4Address:str = hostIpv4Address
|
|
79
|
+
self._UseSSL:bool = useSSL
|
|
80
|
+
self._Version:str = version
|
|
81
|
+
|
|
82
|
+
# get uri used to access the Spotify Zeroconf API for the device.
|
|
83
|
+
self._Uri:str = self.GetEndpointUri()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def CPath(self) -> str:
|
|
88
|
+
"""
|
|
89
|
+
Spotify Connect Zeroconf API CPath property value (e.g. "/zc").
|
|
90
|
+
"""
|
|
91
|
+
return self._CPath
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def HostIpv4Address(self) -> str:
|
|
96
|
+
"""
|
|
97
|
+
IPV4 address (as a string) at which the Spotify Connect Zeroconf API can be reached
|
|
98
|
+
on the Spotify Connect device (e.g. "192.168.1.81").
|
|
99
|
+
"""
|
|
100
|
+
return self._HostIpv4Address
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def HostIpPort(self) -> int:
|
|
105
|
+
"""
|
|
106
|
+
Port number (as an integer) at which the Spotify Connect Zeroconf API can be reached
|
|
107
|
+
on the Spotify Connect device (e.g. "8200").
|
|
108
|
+
"""
|
|
109
|
+
return self._HostIpPort
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def Uri(self) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Spotify Connect Zeroconf API URI that will be used to access the device (e.g. "http://192.168.1.81:8200/zc").
|
|
116
|
+
"""
|
|
117
|
+
return self._Uri
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def UseSSL(self) -> str:
|
|
122
|
+
"""
|
|
123
|
+
True if the host device utilizes HTTPS Secure Sockets Layer (SSL) support;
|
|
124
|
+
otherwise, False to utilize HTTP.
|
|
125
|
+
|
|
126
|
+
Default is False (HTTP).
|
|
127
|
+
"""
|
|
128
|
+
return self._UseSSL
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def Version(self) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Spotify Connect Zeroconf API version number that the device supports (e.g. "2.10.0").
|
|
135
|
+
"""
|
|
136
|
+
return self._Version
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _CheckResponseForErrors(self, response:Response, methodName:str) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Checks the Spotify Zeroconf API response for errors.
|
|
142
|
+
|
|
143
|
+
If no errors were found, then the request response data is converted to a JSON dictionary
|
|
144
|
+
and returned to the caller.
|
|
145
|
+
|
|
146
|
+
If an error result is found, then a `SpotifyZeroconfApiError` is raised to inform the
|
|
147
|
+
user of the error.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
response (Response):
|
|
151
|
+
Spotify Web API http response object.
|
|
152
|
+
methodName (str):
|
|
153
|
+
method name that made the request (for trace purposes).
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
SpotifyZeroconfApiError:
|
|
157
|
+
If the Spotify Zeroconf API request response contains error information.
|
|
158
|
+
"""
|
|
159
|
+
responseData:dict = None
|
|
160
|
+
responseUTF8:str = None
|
|
161
|
+
contentType:str = None
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
|
|
165
|
+
# trace.
|
|
166
|
+
if _logsi.IsOn(SILevel.Debug):
|
|
167
|
+
_logsi.LogObject(SILevel.Debug, 'ZeroconfConnect http response object - type="%s", module="%s"' % (type(response).__name__, type(response).__module__), response)
|
|
168
|
+
|
|
169
|
+
# safely get the response url value.
|
|
170
|
+
# for some reason, the 'url' attribute is not present sometimes if a redirect occurs on the request.
|
|
171
|
+
responseUrl:str = None
|
|
172
|
+
if hasattr(response, 'url'):
|
|
173
|
+
responseUrl = response.url
|
|
174
|
+
elif hasattr(response, '_request_url'):
|
|
175
|
+
responseUrl = response._request_url
|
|
176
|
+
else:
|
|
177
|
+
try:
|
|
178
|
+
responseUrl = response.geturl()
|
|
179
|
+
except Exception:
|
|
180
|
+
_logsi.LogWarning('HTTPResponse method "geturl()" could not be called - defaulting to "unknown response url"')
|
|
181
|
+
responseUrl = 'Unknown response url'
|
|
182
|
+
|
|
183
|
+
# trace.
|
|
184
|
+
if _logsi.IsOn(SILevel.Debug):
|
|
185
|
+
_logsi.LogObject(SILevel.Debug, "ZeroconfConnect http response [%s-%s]: '%s' (response)" % (response.status_code, response.reason, responseUrl), response)
|
|
186
|
+
if (response.headers):
|
|
187
|
+
_logsi.LogCollection(SILevel.Debug, "ZeroconfConnect http response [%s-%s]: '%s' (headers)" % (response.status_code, response.reason, responseUrl), response.headers.items())
|
|
188
|
+
|
|
189
|
+
if response.content is not None:
|
|
190
|
+
|
|
191
|
+
# do response headers contain a content-type value?
|
|
192
|
+
# if so, we will use it to determine how to convert the response data.
|
|
193
|
+
if response.headers:
|
|
194
|
+
if 'content-type' in response.headers:
|
|
195
|
+
contentType = response.headers['content-type']
|
|
196
|
+
|
|
197
|
+
# do we have response data?
|
|
198
|
+
if len(response.content) == 0:
|
|
199
|
+
|
|
200
|
+
# some requests will not return a response, which is ok.
|
|
201
|
+
responseData = None
|
|
202
|
+
_logsi.LogVerbose("ZeroconfConnect http response [%s-%s]: '%s' (no data)" % (response.status_code, response.reason, responseUrl))
|
|
203
|
+
|
|
204
|
+
elif (contentType is not None) and (contentType.find('json') > -1):
|
|
205
|
+
|
|
206
|
+
# response is json.
|
|
207
|
+
# do not use the "response.json()" method to parse JSON responses, as it is unreliable!
|
|
208
|
+
data = response.content.decode('utf-8')
|
|
209
|
+
responseData = json.loads(data)
|
|
210
|
+
|
|
211
|
+
if _logsi.IsOn(SILevel.Verbose):
|
|
212
|
+
if isinstance(responseData, dict):
|
|
213
|
+
_logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json dict)" % (response.status_code, response.reason, responseUrl), responseData)
|
|
214
|
+
elif isinstance(responseData, list):
|
|
215
|
+
_logsi.LogArray(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json array)" % (response.status_code, response.reason, responseUrl), responseData)
|
|
216
|
+
else:
|
|
217
|
+
_logsi.LogObject(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json object)" % (response.status_code, response.reason, responseUrl), responseData)
|
|
218
|
+
|
|
219
|
+
else:
|
|
220
|
+
|
|
221
|
+
# no - treat it as utf-8 encoded data.
|
|
222
|
+
responseUTF8 = response.content.decode('utf-8')
|
|
223
|
+
_logsi.LogText(SILevel.Error, "ZeroconfConnect http response [%s-%s]: '%s' (utf-8)" % (response.status_code, response.reason, responseUrl), responseUTF8)
|
|
224
|
+
responseData = responseUTF8
|
|
225
|
+
|
|
226
|
+
except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
|
|
227
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
228
|
+
except Exception as ex:
|
|
229
|
+
|
|
230
|
+
_logsi.LogException("ZeroconfConnect http response could not be converted to JSON and will be converted to utf-8.\nConversion exception returned was:\n{ex}".format(ex=str(ex)), ex, logToSystemLogger=False)
|
|
231
|
+
|
|
232
|
+
# if json conversion failed, then convert to utf-8 response.
|
|
233
|
+
if response.content is not None:
|
|
234
|
+
responseUTF8 = response.content.decode('utf-8')
|
|
235
|
+
_logsi.LogText(SILevel.Error, "ZeroconfConnect http response [%s-%s]: '%s' (utf-8)" % (response.status_code, response.reason, responseUrl), responseUTF8)
|
|
236
|
+
|
|
237
|
+
# at this point we don't know what Spotify Web Api returned, so let's
|
|
238
|
+
# just raise a new exception with the non-JSON response data.
|
|
239
|
+
raise SpotifyZeroconfApiError(response.status_code, responseUTF8, methodName, response.reason, _logsi)
|
|
240
|
+
|
|
241
|
+
errCode:str = None
|
|
242
|
+
errMessage:str = None
|
|
243
|
+
|
|
244
|
+
# if response is not in the 2xx range then it's an error, even
|
|
245
|
+
# though no json 'error' response body was returned! this is
|
|
246
|
+
# usually due to a '405 - method not allowed' or '403-Forbidden',
|
|
247
|
+
# or '503 Server Error' response.
|
|
248
|
+
if response.status_code > 299:
|
|
249
|
+
|
|
250
|
+
errCode = response.status_code
|
|
251
|
+
errMessage = response.reason
|
|
252
|
+
|
|
253
|
+
if responseUTF8 is not None:
|
|
254
|
+
errMessage = responseUTF8
|
|
255
|
+
|
|
256
|
+
raise SpotifyZeroconfApiError(errCode, errMessage, methodName, response.reason, _logsi)
|
|
257
|
+
|
|
258
|
+
# no errors found - set message object response data.
|
|
259
|
+
return responseData
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def Connect(self,
|
|
263
|
+
username:str,
|
|
264
|
+
password:str,
|
|
265
|
+
) -> ZeroconfResponse:
|
|
266
|
+
"""
|
|
267
|
+
Calls the `addUser` Spotify Zeroconf API endpoint to issue a call to SpConnectionLoginBlob. If successful,
|
|
268
|
+
the associated device id is added to the Spotify Connect active device list for the specified user account.
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
username (str):
|
|
273
|
+
Spotify Connect user name to login with.
|
|
274
|
+
password (str):
|
|
275
|
+
Spotify Connect user password to login with.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
A `ZeroconfResponse` object that indicates success or failure (see notes below).
|
|
279
|
+
|
|
280
|
+
Raises:
|
|
281
|
+
SpotifyWebApiError:
|
|
282
|
+
If the Spotify Zeroconf API request response contains error information.
|
|
283
|
+
|
|
284
|
+
This will first issue a call to the `getInfo` Spotify Zeroconf API endpoint to retrieve the Spotify
|
|
285
|
+
Connect device id associated with the device. It will then issue a call to the `addUser` Spotify Zeroconf
|
|
286
|
+
API endpoint to add the user to the device.
|
|
287
|
+
|
|
288
|
+
The login (on the device) is performed asynchronously, so the return result only indicates whether the library
|
|
289
|
+
is able to perform the login attempt. You should issue a call to the Spotify Web API `Get Available Devices`
|
|
290
|
+
endpoint to check the current device list to ensure that the device id was successfully added or not.
|
|
291
|
+
|
|
292
|
+
<details>
|
|
293
|
+
<summary>Sample Code</summary>
|
|
294
|
+
```python
|
|
295
|
+
.. include:: ../../docs/include/samplecode/ZeroconfConnect/Connect.py
|
|
296
|
+
```
|
|
297
|
+
</details>
|
|
298
|
+
"""
|
|
299
|
+
apiMethodName:str = 'Connect'
|
|
300
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
301
|
+
result:ZeroconfResponse = None
|
|
302
|
+
response:Response = None
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
|
|
306
|
+
# trace.
|
|
307
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
308
|
+
apiMethodParms.AppendKeyValue("HostIpv4Address", self._HostIpv4Address)
|
|
309
|
+
apiMethodParms.AppendKeyValue("HostIpPort", self._HostIpPort)
|
|
310
|
+
apiMethodParms.AppendKeyValue("CPath", self._CPath)
|
|
311
|
+
apiMethodParms.AppendKeyValue("Version", self._Version)
|
|
312
|
+
apiMethodParms.AppendKeyValue("Uri", self._Uri)
|
|
313
|
+
apiMethodParms.AppendKeyValue("username", username)
|
|
314
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Connecting device to Spotify Connect using specified Username and Password", apiMethodParms)
|
|
315
|
+
|
|
316
|
+
# validations.
|
|
317
|
+
if (username is None) or (not isinstance(username,str)):
|
|
318
|
+
raise SpotifyApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % (apiMethodName, 'username'), logsi=_logsi)
|
|
319
|
+
if (password is None) or (not isinstance(password,str)):
|
|
320
|
+
raise SpotifyApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % (apiMethodName, 'password'), logsi=_logsi)
|
|
321
|
+
|
|
322
|
+
# get the current device id from the device via Spotify ZeroConf API `getInfo` endpoint.
|
|
323
|
+
info:ZeroconfGetInfo = self.GetInformation()
|
|
324
|
+
|
|
325
|
+
# formulate the blob.
|
|
326
|
+
credentials:Credentials = Credentials(username, password)
|
|
327
|
+
builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
|
|
328
|
+
blob = builder.build()
|
|
329
|
+
|
|
330
|
+
# execute spotify zeroconf api request.
|
|
331
|
+
response = requests.post(
|
|
332
|
+
self._Uri,
|
|
333
|
+
timeout=10,
|
|
334
|
+
headers={
|
|
335
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
336
|
+
},
|
|
337
|
+
data={
|
|
338
|
+
'action': 'addUser',
|
|
339
|
+
'VERSION': self._Version,
|
|
340
|
+
'userName': credentials.username.decode('ascii'),
|
|
341
|
+
'clientKey': int_to_b64str(builder.dh_keys.public_key),
|
|
342
|
+
'blob': blob
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# check response for errors, and return json response.
|
|
347
|
+
responseData:dict = self._CheckResponseForErrors(response, apiMethodName)
|
|
348
|
+
|
|
349
|
+
# process results.
|
|
350
|
+
result = ZeroconfResponse(root=responseData)
|
|
351
|
+
|
|
352
|
+
# trace.
|
|
353
|
+
_logsi.LogObject(SILevel.Verbose, '%s result - type="%s"' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
354
|
+
return result
|
|
355
|
+
|
|
356
|
+
except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
|
|
357
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
358
|
+
except Exception as ex:
|
|
359
|
+
|
|
360
|
+
# format unhandled exception.
|
|
361
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
362
|
+
|
|
363
|
+
finally:
|
|
364
|
+
|
|
365
|
+
# close the response (if needed).
|
|
366
|
+
if response is not None:
|
|
367
|
+
response.close()
|
|
368
|
+
|
|
369
|
+
# trace.
|
|
370
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def Disconnect(self) -> ZeroconfResponse:
|
|
374
|
+
"""
|
|
375
|
+
Calls the `resetUsers` Spotify Zeroconf API endpoint to issue a call to SpConnectionLogout.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
A `ZeroconfResponse` object that indicates success or failure (see notes below).
|
|
379
|
+
|
|
380
|
+
Raises:
|
|
381
|
+
SpotifyWebApiError:
|
|
382
|
+
If the Spotify Zeroconf API request response contains error information.
|
|
383
|
+
|
|
384
|
+
The currently logged in user (if any) will be logged out of Spotify Connect, and the
|
|
385
|
+
device id removed from the active Spotify Connect device list.
|
|
386
|
+
|
|
387
|
+
<details>
|
|
388
|
+
<summary>Sample Code</summary>
|
|
389
|
+
```python
|
|
390
|
+
.. include:: ../../docs/include/samplecode/ZeroconfConnect/Disconnect.py
|
|
391
|
+
```
|
|
392
|
+
</details>
|
|
393
|
+
"""
|
|
394
|
+
apiMethodName:str = 'Disconnect'
|
|
395
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
396
|
+
result:ZeroconfResponse = None
|
|
397
|
+
response:Response = None
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
|
|
401
|
+
# trace.
|
|
402
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
403
|
+
apiMethodParms.AppendKeyValue("HostIpv4Address", self._HostIpv4Address)
|
|
404
|
+
apiMethodParms.AppendKeyValue("HostIpPort", self._HostIpPort)
|
|
405
|
+
apiMethodParms.AppendKeyValue("CPath", self._CPath)
|
|
406
|
+
apiMethodParms.AppendKeyValue("Version", self._Version)
|
|
407
|
+
apiMethodParms.AppendKeyValue("Uri", self._Uri)
|
|
408
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Disconnecting device from Spotify Connect", apiMethodParms)
|
|
409
|
+
|
|
410
|
+
# execute spotify zeroconf api request.
|
|
411
|
+
response = requests.post(
|
|
412
|
+
self._Uri,
|
|
413
|
+
timeout=10,
|
|
414
|
+
headers={
|
|
415
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
416
|
+
},
|
|
417
|
+
data={
|
|
418
|
+
'action': 'resetUsers',
|
|
419
|
+
'VERSION': self._Version,
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# check response for errors, and return json response.
|
|
424
|
+
responseData:dict = self._CheckResponseForErrors(response, apiMethodName)
|
|
425
|
+
|
|
426
|
+
# process results.
|
|
427
|
+
result = ZeroconfResponse(root=responseData)
|
|
428
|
+
|
|
429
|
+
# just in case user tries to issue a connect immediately after this, give the device
|
|
430
|
+
# a little time to reset the connection so the subsequent connect does not fail.
|
|
431
|
+
time.sleep(0.5)
|
|
432
|
+
|
|
433
|
+
# trace.
|
|
434
|
+
_logsi.LogObject(SILevel.Verbose, '%s result - type="%s"' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
435
|
+
return result
|
|
436
|
+
|
|
437
|
+
except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
|
|
438
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
439
|
+
except Exception as ex:
|
|
440
|
+
|
|
441
|
+
# format unhandled exception.
|
|
442
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
443
|
+
|
|
444
|
+
finally:
|
|
445
|
+
|
|
446
|
+
# close the response (if needed).
|
|
447
|
+
if response is not None:
|
|
448
|
+
response.close()
|
|
449
|
+
|
|
450
|
+
# trace.
|
|
451
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def GetEndpointUri(self) -> str:
|
|
455
|
+
"""
|
|
456
|
+
Gets a Spotify Zeroconf API endpoint uri.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
A string containing the endpoint uri.
|
|
460
|
+
"""
|
|
461
|
+
protocol:str = 'http'
|
|
462
|
+
if (self._UseSSL):
|
|
463
|
+
protocol = 'https'
|
|
464
|
+
|
|
465
|
+
return "{protocol}://{ip}:{port}{cpath}".format(
|
|
466
|
+
protocol=protocol,
|
|
467
|
+
ip=self._HostIpv4Address,
|
|
468
|
+
port=self._HostIpPort,
|
|
469
|
+
cpath=self._CPath,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def GetInformation(self) -> ZeroconfGetInfo:
|
|
474
|
+
"""
|
|
475
|
+
Calls the `getInfo` Spotify Zeroconf API endpoint to return information about the device.
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
A `ZeroconfGetInfo` object that indicates success or failure (see notes below), as well
|
|
479
|
+
as the device information.
|
|
480
|
+
|
|
481
|
+
Raises:
|
|
482
|
+
SpotifyWebApiError:
|
|
483
|
+
If the Spotify Zeroconf API request response contains error information.
|
|
484
|
+
|
|
485
|
+
<details>
|
|
486
|
+
<summary>Sample Code</summary>
|
|
487
|
+
```python
|
|
488
|
+
.. include:: ../../docs/include/samplecode/ZeroconfConnect/GetInformation.py
|
|
489
|
+
```
|
|
490
|
+
</details>
|
|
491
|
+
"""
|
|
492
|
+
apiMethodName:str = 'GetInformation'
|
|
493
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
494
|
+
result:ZeroconfGetInfo = None
|
|
495
|
+
response:Response = None
|
|
496
|
+
|
|
497
|
+
try:
|
|
498
|
+
|
|
499
|
+
# trace.
|
|
500
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
501
|
+
apiMethodParms.AppendKeyValue("HostIpv4Address", self._HostIpv4Address)
|
|
502
|
+
apiMethodParms.AppendKeyValue("HostIpPort", self._HostIpPort)
|
|
503
|
+
apiMethodParms.AppendKeyValue("CPath", self._CPath)
|
|
504
|
+
apiMethodParms.AppendKeyValue("Version", self._Version)
|
|
505
|
+
apiMethodParms.AppendKeyValue("Uri", self._Uri)
|
|
506
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Get information from Spotify Connect for device", apiMethodParms)
|
|
507
|
+
|
|
508
|
+
# execute spotify zeroconf api request.
|
|
509
|
+
response = requests.get(
|
|
510
|
+
self._Uri,
|
|
511
|
+
timeout=10,
|
|
512
|
+
headers={
|
|
513
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
514
|
+
},
|
|
515
|
+
params={
|
|
516
|
+
'action': 'getInfo',
|
|
517
|
+
'VERSION': self._Version,
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
# check response for errors, and return json response.
|
|
521
|
+
responseData:dict = self._CheckResponseForErrors(response, apiMethodName)
|
|
522
|
+
|
|
523
|
+
# process results.
|
|
524
|
+
result = ZeroconfGetInfo(root=responseData)
|
|
525
|
+
|
|
526
|
+
# trace.
|
|
527
|
+
_logsi.LogObject(SILevel.Verbose, '%s result - type="%s"' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
528
|
+
return result
|
|
529
|
+
|
|
530
|
+
except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
|
|
531
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
532
|
+
except Exception as ex:
|
|
533
|
+
|
|
534
|
+
# format unhandled exception.
|
|
535
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
536
|
+
|
|
537
|
+
finally:
|
|
538
|
+
|
|
539
|
+
# close the response (if needed).
|
|
540
|
+
if response is not None:
|
|
541
|
+
response.close()
|
|
542
|
+
|
|
543
|
+
# trace.
|
|
544
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def ToString(self, includeTitle:bool=False) -> str:
|
|
548
|
+
"""
|
|
549
|
+
Returns a displayable string representation of the class.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
includeTitle (str):
|
|
553
|
+
True to include the class name title prefix.
|
|
554
|
+
"""
|
|
555
|
+
msg:str = ''
|
|
556
|
+
if includeTitle:
|
|
557
|
+
msg = 'ZeroconfConnect:'
|
|
558
|
+
|
|
559
|
+
msg = '%s\n URI="%s"' % (msg, str(self._Uri))
|
|
560
|
+
if self._CPath is not None: msg = '%s\n CPath="%s"' % (msg, str(self._CPath))
|
|
561
|
+
if self._Version is not None: msg = '%s\n Version="%s"' % (msg, str(self._Version))
|
|
562
|
+
return msg
|