spotifywebapiPython 1.0.242__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.
- spotifywebapipython/__init__.py +35 -0
- spotifywebapipython/const.py +114 -0
- spotifywebapipython/docs/__init__.html +2156 -0
- spotifywebapipython/docs/codecopy.js +70 -0
- spotifywebapipython/docs/index.html +514 -0
- spotifywebapipython/docs/search.js +46 -0
- spotifywebapipython/docs/spotifywebapipython/const.html +636 -0
- spotifywebapipython/docs/spotifywebapipython/saappmessages.html +540 -0
- spotifywebapipython/docs/spotifywebapipython/sautils.html +620 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +341 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +598 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +612 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +16557 -0
- spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +518 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +421 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +400 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +635 -0
- spotifywebapipython/docs/spotifywebapipython.ico +0 -0
- spotifywebapipython/docs/spotifywebapipython_OLD.ico +0 -0
- spotifywebapipython/models/__init__.py +121 -0
- spotifywebapipython/models/album.py +170 -0
- spotifywebapipython/models/albumpagesaved.py +85 -0
- spotifywebapipython/models/albumpagesimplified.py +88 -0
- spotifywebapipython/models/albumsaved.py +102 -0
- spotifywebapipython/models/albumsimplified.py +308 -0
- spotifywebapipython/models/artist.py +146 -0
- spotifywebapipython/models/artistinfo.py +282 -0
- spotifywebapipython/models/artistinfotourevent.py +127 -0
- spotifywebapipython/models/artistpage.py +70 -0
- spotifywebapipython/models/artistsimplified.py +163 -0
- spotifywebapipython/models/audiobook.py +87 -0
- spotifywebapipython/models/audiobookpagesimplified.py +88 -0
- spotifywebapipython/models/audiobooksimplified.py +362 -0
- spotifywebapipython/models/audiofeatures.py +333 -0
- spotifywebapipython/models/author.py +67 -0
- spotifywebapipython/models/category.py +168 -0
- spotifywebapipython/models/categorypage.py +70 -0
- spotifywebapipython/models/chapter.py +84 -0
- spotifywebapipython/models/chapterpagesimplified.py +88 -0
- spotifywebapipython/models/chaptersimplified.py +387 -0
- spotifywebapipython/models/context.py +113 -0
- spotifywebapipython/models/copyright.py +79 -0
- spotifywebapipython/models/device.py +301 -0
- spotifywebapipython/models/episode.py +84 -0
- spotifywebapipython/models/episodepagesaved.py +85 -0
- spotifywebapipython/models/episodepagesimplified.py +88 -0
- spotifywebapipython/models/episodesaved.py +99 -0
- spotifywebapipython/models/episodesimplified.py +370 -0
- spotifywebapipython/models/explicitcontent.py +80 -0
- spotifywebapipython/models/externalids.py +91 -0
- spotifywebapipython/models/externalurls.py +104 -0
- spotifywebapipython/models/followers.py +81 -0
- spotifywebapipython/models/imageobject.py +193 -0
- spotifywebapipython/models/imagevibrantcolors.py +182 -0
- spotifywebapipython/models/linkedfrom.py +165 -0
- spotifywebapipython/models/narrator.py +67 -0
- spotifywebapipython/models/owner.py +162 -0
- spotifywebapipython/models/pageobject.py +343 -0
- spotifywebapipython/models/playeractions.py +196 -0
- spotifywebapipython/models/playerlastplayedinfo.py +334 -0
- spotifywebapipython/models/playerplaystate.py +467 -0
- spotifywebapipython/models/playerqueueinfo.py +207 -0
- spotifywebapipython/models/playhistory.py +136 -0
- spotifywebapipython/models/playhistorypage.py +85 -0
- spotifywebapipython/models/playlist.py +125 -0
- spotifywebapipython/models/playlistpage.py +85 -0
- spotifywebapipython/models/playlistpagesimplified.py +107 -0
- spotifywebapipython/models/playlistsimplified.py +334 -0
- spotifywebapipython/models/playlisttrack.py +143 -0
- spotifywebapipython/models/playlisttracksummary.py +80 -0
- spotifywebapipython/models/recommendationseed.py +135 -0
- spotifywebapipython/models/restrictions.py +86 -0
- spotifywebapipython/models/resumepoint.py +79 -0
- spotifywebapipython/models/searchresponse.py +391 -0
- spotifywebapipython/models/searchresultbase.py +299 -0
- spotifywebapipython/models/show.py +87 -0
- spotifywebapipython/models/showpagesaved.py +85 -0
- spotifywebapipython/models/showpagesimplified.py +88 -0
- spotifywebapipython/models/showsaved.py +99 -0
- spotifywebapipython/models/showsimplified.py +320 -0
- spotifywebapipython/models/spotifyconnectdevice.py +321 -0
- spotifywebapipython/models/spotifyconnectdevices.py +441 -0
- spotifywebapipython/models/track.py +142 -0
- spotifywebapipython/models/trackpage.py +110 -0
- spotifywebapipython/models/trackpagesaved.py +85 -0
- spotifywebapipython/models/trackpagesimplified.py +88 -0
- spotifywebapipython/models/trackrecommendations.py +88 -0
- spotifywebapipython/models/tracksaved.py +99 -0
- spotifywebapipython/models/tracksimplified.py +398 -0
- spotifywebapipython/models/userprofile.py +184 -0
- spotifywebapipython/models/userprofilesimplified.py +206 -0
- spotifywebapipython/models/zeroconfdiscoveryresult.py +592 -0
- spotifywebapipython/models/zeroconfproperty.py +93 -0
- spotifywebapipython/oauthcli/__init__.py +7 -0
- spotifywebapipython/oauthcli/authclient.py +1229 -0
- spotifywebapipython/saappmessages.py +86 -0
- spotifywebapipython/sautils.py +285 -0
- spotifywebapipython/spotifyapierror.py +66 -0
- spotifywebapipython/spotifyapimessage.py +269 -0
- spotifywebapipython/spotifyauthtoken.py +264 -0
- spotifywebapipython/spotifyclient.py +17885 -0
- spotifywebapipython/spotifyconnect/__init__.py +19 -0
- spotifywebapipython/spotifyconnect/spotifyconnectdeviceeventargs.py +67 -0
- spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +2564 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +467 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +701 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +359 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastmultizonelistener.py +212 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +20 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconflistener.py +335 -0
- spotifywebapipython/spotifydiscovery.py +473 -0
- spotifywebapipython/spotifymediatypes.py +53 -0
- spotifywebapipython/spotifywebapiauthenticationerror.py +122 -0
- spotifywebapipython/spotifywebapierror.py +106 -0
- spotifywebapipython/spotifywebplayertoken.py +743 -0
- spotifywebapipython/vibrant/__init__.py +17 -0
- spotifywebapipython/vibrant/generator.py +261 -0
- spotifywebapipython/vibrant/image.py +100 -0
- spotifywebapipython/vibrant/main.py +34 -0
- spotifywebapipython/vibrant/models.py +93 -0
- spotifywebapipython/vibrant/utils.py +56 -0
- 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 +1327 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +572 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfoalias.py +109 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfodrmmediaformat.py +90 -0
- spotifywebapipython/zeroconfapi/zeroconfresponse.py +195 -0
- spotifywebapipython-1.0.242.dist-info/LICENSE +22 -0
- spotifywebapipython-1.0.242.dist-info/METADATA +219 -0
- spotifywebapipython-1.0.242.dist-info/RECORD +138 -0
- spotifywebapipython-1.0.242.dist-info/WHEEL +5 -0
- spotifywebapipython-1.0.242.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
from Crypto.Cipher import AES
|
|
4
|
+
from Crypto.Util import Counter
|
|
5
|
+
from Crypto.Protocol.KDF import PBKDF2
|
|
6
|
+
from Crypto.Hash import SHA1
|
|
7
|
+
from base64 import b64decode, b64encode
|
|
8
|
+
|
|
9
|
+
from .credentials import Credentials
|
|
10
|
+
from .cryptodiffiehellman import CryptoDiffieHellman
|
|
11
|
+
from .helpers import write_bytes, write_int, byte_list_to_int, b64_to_int, int_to_bytes, string_to_int
|
|
12
|
+
|
|
13
|
+
# get smartinspect logger reference; create a new session for this module name.
|
|
14
|
+
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SISourceId
|
|
15
|
+
import logging
|
|
16
|
+
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
|
17
|
+
if (_logsi == None):
|
|
18
|
+
_logsi = SIAuto.Si.AddSession(__name__, True)
|
|
19
|
+
#_logsi.SystemLogger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BlobBuilder:
|
|
23
|
+
"""
|
|
24
|
+
Builds a Spotify Connect Zeroconf Blob object.
|
|
25
|
+
|
|
26
|
+
Maximum length of the zeroconf blob (SP_MAX_ZEROCONF_BLOB_LENGTH)
|
|
27
|
+
is 2047 characters (not counting terminating NULL).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
AES_KEY_SIZE:int = 16 # 16 byte / 128 bit key size for AES encryption
|
|
31
|
+
AES_BLOCK_SIZE:int = 16 # 16 byte / 128 bit block size for AES encryption
|
|
32
|
+
|
|
33
|
+
def __init__(self,
|
|
34
|
+
credentials:Credentials,
|
|
35
|
+
device_id:str,
|
|
36
|
+
remotePublicKeyBase64String:str,
|
|
37
|
+
originDeviceName:str=None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Initializes a new instance of the class.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
credentials (Credentials):
|
|
44
|
+
deviceId (str):
|
|
45
|
+
remotePublicKeyBase64String (str):
|
|
46
|
+
Remote public key value in a base64-encoded string format.
|
|
47
|
+
originDeviceName (str):
|
|
48
|
+
"""
|
|
49
|
+
# validation.
|
|
50
|
+
if (originDeviceName is None) or (not isinstance(originDeviceName,str)):
|
|
51
|
+
originDeviceName:str = 'ha-spotifyplus'
|
|
52
|
+
|
|
53
|
+
# initialize storage.
|
|
54
|
+
self._EncryptedBlob:bytes = b''
|
|
55
|
+
self._EncryptedBlobSigned:bytes = b''
|
|
56
|
+
self._EncryptedBlobSignedBase64String:str = ''
|
|
57
|
+
self._RemotePublicKeyBase64String:str = remotePublicKeyBase64String
|
|
58
|
+
self._RemotePublicKey:int = b64_to_int(remotePublicKeyBase64String)
|
|
59
|
+
|
|
60
|
+
# formulate origin device information.
|
|
61
|
+
self._OriginDeviceName:str = originDeviceName
|
|
62
|
+
self._OriginDeviceId:str = hashlib.sha1(bytes(self._OriginDeviceName, 'ascii')).digest().hex()
|
|
63
|
+
|
|
64
|
+
self.credentials:Credentials = credentials
|
|
65
|
+
self.device_id:bytes = bytes(device_id, 'ascii')
|
|
66
|
+
self.dh_keys = CryptoDiffieHellman()
|
|
67
|
+
self._blob:bytes = b''
|
|
68
|
+
self._decrypted_blob:bytes = b''
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def EncryptedBlob(self) -> bytes:
|
|
73
|
+
"""
|
|
74
|
+
Returns the unsigned encrypted blob as a bytes object.
|
|
75
|
+
"""
|
|
76
|
+
return self._EncryptedBlob
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def EncryptedBlobSigned(self) -> bytes:
|
|
81
|
+
"""
|
|
82
|
+
Returns the signed encrypted blob as a bytes object.
|
|
83
|
+
"""
|
|
84
|
+
return self._EncryptedBlobSigned
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def EncryptedBlobSignedBase64String(self) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Returns the signed encrypted blob as a base64 encoded string.
|
|
91
|
+
"""
|
|
92
|
+
return self._EncryptedBlobSignedBase64String
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def RemotePublicKeyBase64String(self) -> str:
|
|
97
|
+
"""
|
|
98
|
+
Returns the remote public key value in a base64 encoded string format.
|
|
99
|
+
"""
|
|
100
|
+
return self._RemotePublicKeyBase64String
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def RemotePublicKey(self) -> int:
|
|
105
|
+
"""
|
|
106
|
+
Returns the remote public key key value in an integer format.
|
|
107
|
+
"""
|
|
108
|
+
return self._RemotePublicKey
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def build(self):
|
|
112
|
+
"""
|
|
113
|
+
Builds the blob, encrypts it, and returns the signed encrypted blob as a base64 encoded string.
|
|
114
|
+
"""
|
|
115
|
+
self._BuildBlob()
|
|
116
|
+
self._Encrypt()
|
|
117
|
+
return self._EncryptedBlobSignedBase64String
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _BuildBlob(self): # -> bytes
|
|
121
|
+
|
|
122
|
+
apiMethodName:str = 'BlobBuilder._BuildBlob'
|
|
123
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
|
|
127
|
+
# trace.
|
|
128
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
129
|
+
apiMethodParms.AppendKeyValue("device_id", self.device_id)
|
|
130
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Building Spotify Connect ZeroConf addUser Blob", apiMethodParms)
|
|
131
|
+
|
|
132
|
+
# create the blob data that will be sent to the device.
|
|
133
|
+
blob = bytearray()
|
|
134
|
+
write_int(0x49, blob) # 'I' (eye-catcher)
|
|
135
|
+
write_bytes(self.credentials.username, blob) # username data (length + value)
|
|
136
|
+
write_int(0x50, blob) # 'P' (eye-catcher)
|
|
137
|
+
write_int(self.credentials.auth_type, blob) # auth_type
|
|
138
|
+
write_int(0x51, blob) # 'Q' (eye-catcher)
|
|
139
|
+
write_bytes(self.credentials.password, blob) # password data (length + value)
|
|
140
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data, Unencrypted Bytes", blob)
|
|
141
|
+
|
|
142
|
+
# padding
|
|
143
|
+
n_zeros = BlobBuilder.AES_BLOCK_SIZE - (len(blob) % BlobBuilder.AES_BLOCK_SIZE) - 1
|
|
144
|
+
blob.extend([0] * n_zeros)
|
|
145
|
+
blob.append(n_zeros + 1)
|
|
146
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data, Unencrypted Bytes (with padding)", blob)
|
|
147
|
+
|
|
148
|
+
# not sure what the following is really trying to accomplish.
|
|
149
|
+
# it leaves the first 16 bytes as-is, then starts performing a Bitwise XOR on
|
|
150
|
+
# on each byte remaining in the buffer (starting at offset 0x11 / byte 17).
|
|
151
|
+
blen = len(blob)
|
|
152
|
+
for i in range(blen - 0x11, -1, -1):
|
|
153
|
+
blob[blen - i - 1] ^= blob[blen - i - 0x11]
|
|
154
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data, Unencrypted Bytes after XOR of bytes 16 thru %d" % (blen), blob)
|
|
155
|
+
|
|
156
|
+
# secret is the deviceId value.
|
|
157
|
+
secret = hashlib.sha1(self.device_id).digest()
|
|
158
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data Secret, SHA1 Digest", secret)
|
|
159
|
+
|
|
160
|
+
# create a unique, strong hash for the given secret.
|
|
161
|
+
keys = PBKDF2(secret, self.credentials.username, 20, count=0x100, hmac_hash_module=SHA1)
|
|
162
|
+
key = bytearray(hashlib.sha1(keys).digest()[:20])
|
|
163
|
+
key.extend(bytearray([0x00, 0x00, 0x00, 0x14]))
|
|
164
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data Encryption Key", key)
|
|
165
|
+
|
|
166
|
+
# create a new AES cipher that will be used to encrypt the data.
|
|
167
|
+
# use the hashed secret as the encryption key.
|
|
168
|
+
# data will be encrypted in 16-byte blocks (or chunks).
|
|
169
|
+
encrypted_blob = bytearray()
|
|
170
|
+
cipher = AES.new(key, mode=AES.MODE_ECB)
|
|
171
|
+
|
|
172
|
+
def chunker(seq, size):
|
|
173
|
+
return (seq[pos:pos + size] for pos in range(0, len(seq), size))
|
|
174
|
+
|
|
175
|
+
# encrypt the data.
|
|
176
|
+
for chunk in chunker(blob, BlobBuilder.AES_BLOCK_SIZE):
|
|
177
|
+
encrypted_blob.extend(cipher.encrypt(chunk))
|
|
178
|
+
|
|
179
|
+
# trace.
|
|
180
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted Bytes", encrypted_blob)
|
|
181
|
+
|
|
182
|
+
# convert encrypted bytes to a base64-encoded string so we can
|
|
183
|
+
# send it to the device via an http request.
|
|
184
|
+
self._blob = b64encode(encrypted_blob)
|
|
185
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted base64-encoded bytes", self._blob)
|
|
186
|
+
|
|
187
|
+
finally:
|
|
188
|
+
|
|
189
|
+
# trace.
|
|
190
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _GetIVectorAES128(self) -> bytes:
|
|
194
|
+
"""
|
|
195
|
+
Returns a new initialization vector for AES (128-bit) cryptographic functions.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
A new initialization vector of 16 bytes (128-bits).</returns>
|
|
199
|
+
"""
|
|
200
|
+
iv_data:list = [253, 81, 222, 19, 70, 203, 45, 89, 141, 68, 210, 240, 93, 20, 76, 30]
|
|
201
|
+
return int.from_bytes(bytes(iv_data), 'big')
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _Encrypt(self):
|
|
205
|
+
"""
|
|
206
|
+
Encrypts the `_blob` argument and
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
apiMethodName:str = 'BlobBuilder.Encrypt'
|
|
210
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
|
|
214
|
+
# trace.
|
|
215
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
216
|
+
apiMethodParms.AppendKeyValue("RemotePublicKeyBase64String", self.RemotePublicKeyBase64String)
|
|
217
|
+
apiMethodParms.AppendKeyValue("RemotePublicKey", self.RemotePublicKey)
|
|
218
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Encrypt Spotify Connect ZeroConf addUser Blob", apiMethodParms)
|
|
219
|
+
|
|
220
|
+
# trace.
|
|
221
|
+
_logsi.LogInt(SILevel.Verbose, "Remote Public Key: %s" % self.RemotePublicKey, self.RemotePublicKey)
|
|
222
|
+
|
|
223
|
+
# create shared key.
|
|
224
|
+
shared_key:int = self.dh_keys.SharedSecret(self.RemotePublicKey)
|
|
225
|
+
_logsi.LogInt(SILevel.Verbose, "Shared Key: %s" % shared_key, shared_key)
|
|
226
|
+
|
|
227
|
+
b_shared_key:bytes = int_to_bytes(shared_key)
|
|
228
|
+
_logsi.LogBinary(SILevel.Verbose, "Shared Key bytes", b_shared_key)
|
|
229
|
+
base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
|
|
230
|
+
_logsi.LogBinary(SILevel.Verbose, "Base Key bytes", base_key)
|
|
231
|
+
|
|
232
|
+
# create encryption key (16 byte / 128 bit key size for AES encryption).
|
|
233
|
+
encryption_key:bytes = hmac.new(base_key, b'encryption', 'sha1').digest()
|
|
234
|
+
encryption_key = encryption_key[:BlobBuilder.AES_KEY_SIZE]
|
|
235
|
+
_logsi.LogBinary(SILevel.Verbose, "Encryption Key (AES128-bit) bytes", encryption_key)
|
|
236
|
+
|
|
237
|
+
# create new AES encryption cipher, and encrypt the data.
|
|
238
|
+
iv:list[int] = [253, 81, 222, 19, 70, 203, 45, 89, 141, 68, 210, 240, 93, 20, 76, 30]
|
|
239
|
+
ctr_e:dict = Counter.new(128, initial_value=self._GetIVectorAES128())
|
|
240
|
+
cipher = AES.new(encryption_key, AES.MODE_CTR, counter=ctr_e)
|
|
241
|
+
encrypted_blob:bytes = cipher.encrypt(self._blob)
|
|
242
|
+
_logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (unsigned, bytes)", encrypted_blob)
|
|
243
|
+
|
|
244
|
+
# create checksum key and checksum value of encrypted data.
|
|
245
|
+
checksum_key:bytes = hmac.new(base_key, b'checksum', 'sha1').digest()
|
|
246
|
+
_logsi.LogBinary(SILevel.Verbose, "Checksum Key: %s" % checksum_key.hex(), checksum_key)
|
|
247
|
+
checksum:bytes = hmac.new(checksum_key, encrypted_blob, 'sha1').digest()
|
|
248
|
+
_logsi.LogBinary(SILevel.Verbose, "Checksum Value (bytes)", checksum)
|
|
249
|
+
|
|
250
|
+
# create signed encrypted data buffer that contains the encrypted data and checksum.
|
|
251
|
+
self._EncryptedBlobSigned:bytearray = bytearray(iv)
|
|
252
|
+
self._EncryptedBlobSigned.extend(encrypted_blob)
|
|
253
|
+
self._EncryptedBlobSigned.extend(checksum)
|
|
254
|
+
_logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (signed with checksum, bytes)", self._EncryptedBlobSigned)
|
|
255
|
+
|
|
256
|
+
# convert encrypted bytes to a base64-encoded string.
|
|
257
|
+
b64result_bytes:bytes = b64encode(self._EncryptedBlobSigned)
|
|
258
|
+
self._EncryptedBlobSignedBase64String:str = b64result_bytes.decode('ascii')
|
|
259
|
+
_logsi.LogText(SILevel.Verbose, "Encrypted Blob (signed with checksum, base64-encoded string)", self._EncryptedBlobSignedBase64String)
|
|
260
|
+
|
|
261
|
+
finally:
|
|
262
|
+
|
|
263
|
+
# trace.
|
|
264
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# def _Decrypt(
|
|
268
|
+
# self,
|
|
269
|
+
# encrypted_blob_signed:str,
|
|
270
|
+
# ) -> None:
|
|
271
|
+
# """
|
|
272
|
+
# Decreypts the specified blob that was previously encrypted and signed by the
|
|
273
|
+
# `Encrypt` method.
|
|
274
|
+
|
|
275
|
+
# Args:
|
|
276
|
+
# encrypted_blob_signed (str):
|
|
277
|
+
# A base64 encoded string that represents a signed and encrypted data blob; the
|
|
278
|
+
# packet consists of the IV, data, and checksum blocks.
|
|
279
|
+
# """
|
|
280
|
+
|
|
281
|
+
# apiMethodName:str = 'BlobBuilder.Decrypt'
|
|
282
|
+
# apiMethodParms:SIMethodParmListContext = None
|
|
283
|
+
|
|
284
|
+
# try:
|
|
285
|
+
|
|
286
|
+
# # trace.
|
|
287
|
+
# apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
288
|
+
# apiMethodParms.AppendKeyValue("encrypted_blob_signed", encrypted_blob_signed)
|
|
289
|
+
# apiMethodParms.AppendKeyValue("RemotePublicKeyBase64String", self.RemotePublicKeyBase64String)
|
|
290
|
+
# _logsi.LogMethodParmList(SILevel.Verbose, "Decrypt Spotify Connect ZeroConf addUser Blob", apiMethodParms)
|
|
291
|
+
|
|
292
|
+
# if encrypted_blob_signed is not None:
|
|
293
|
+
# self._EncryptedBlobSignedBase64String = encrypted_blob_signed
|
|
294
|
+
|
|
295
|
+
# # convert base64-encoded string to bytes.
|
|
296
|
+
# _logsi.LogText(SILevel.Verbose, "Encrypted Blob (signed with checksum, base64-encoded string)", self._EncryptedBlobSignedBase64String)
|
|
297
|
+
# self._EncryptedBlobSigned:bytes = b64decode(self._EncryptedBlobSignedBase64String)
|
|
298
|
+
# _logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (signed with checksum, bytes)", self._EncryptedBlobSigned)
|
|
299
|
+
|
|
300
|
+
# # get the individual components of the signed blob packet.
|
|
301
|
+
# iv = self._EncryptedBlobSigned[:16]
|
|
302
|
+
# self._EncryptedBlob = self._EncryptedBlobSigned[16:len(self._EncryptedBlobSigned)-20]
|
|
303
|
+
# checksum = self._EncryptedBlobSigned[len(self._EncryptedBlobSigned)-20:]
|
|
304
|
+
# _logsi.LogBinary(SILevel.Verbose, "Checksum Value (bytes)", checksum)
|
|
305
|
+
# _logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (unsigned, bytes)", self._EncryptedBlob)
|
|
306
|
+
|
|
307
|
+
# clientKey:str = "7efznlz1HspR5tQftOUdz7cv/L6V5EuXWI596GyiQMist7piBWxfbdAjcpP9/o7WnCl5HdwgI7nY6cQ7ktz3Tsbs4kSG5mbi1QwnYi8IbJ9sdYdd8YxI3Vl6dfuG3JWP"
|
|
308
|
+
# result_b64decode = b64decode(clientKey)
|
|
309
|
+
# result_b64_to_int = b64_to_int(clientKey) # 114863 ...
|
|
310
|
+
# # result_string_to_int = string_to_int(clientKey) # 114863 ...
|
|
311
|
+
# # result_byte_list_to_int = byte_list_to_int(clientKey) # 114863 ...
|
|
312
|
+
# # result_dh_keys_Exchange = self.dh_keys.Exchange(clientKey) # 6243 ...
|
|
313
|
+
# #resultInt = self.dh_keys.Exchange(clientKey) # exception
|
|
314
|
+
# #resultInt = self.dh_keys.shared_secret(b64_to_int(resultBytes)) # exception
|
|
315
|
+
# #result_dh_keys_shared_secret = self.dh_keys.shared_secret(string_to_int(resultBytes)) # 438506 ...
|
|
316
|
+
# #resultInt = self.dh_keys.shared_secret(string_to_int(clientKey)) # exception ...
|
|
317
|
+
|
|
318
|
+
# remote_device_key:int = b64_to_int(clientKey)
|
|
319
|
+
# shared_key:int = 478549389067009439856765960469867849375843534534536997108028584308833390603433510828238916782187762759272369310194601566745030716193055009703822032238289108648174583850368649722797922715533666774901465941959787614689524297840942749
|
|
320
|
+
# shared_key_int_to_bytes = int_to_bytes(shared_key)
|
|
321
|
+
# #shared_key_b64encode = b64encode(shared_key)
|
|
322
|
+
# shared_key_b64encode_int_to_bytes = b64encode(int_to_bytes(shared_key))
|
|
323
|
+
# shared_key_b64encode_utf8 = b64encode(int_to_bytes(shared_key)).decode('utf-8')
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# #shared_key:int = self.dh_keys.shared_secret(remote_device_key)
|
|
327
|
+
# b_shared_key:bytes = int_to_bytes(shared_key)
|
|
328
|
+
# base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
|
|
329
|
+
# #tmpbytes = b64decode(clientKey)
|
|
330
|
+
# #shared_key:int = byte_list_to_int(tmpbytes)
|
|
331
|
+
# #shared_key:int = b64_to_int(clientKey)
|
|
332
|
+
# #shared_key = self.dh_keys.Exchange(clientKey)
|
|
333
|
+
# _logsi.LogValue(SILevel.Verbose, "Shared Key: %s" % shared_key, shared_key)
|
|
334
|
+
# #baseKey := func() []byte { sum := sha1.Sum(sharedSecret); return sum[:16] }()
|
|
335
|
+
# #b_shared_key:bytes = int_to_bytes(shared_key)
|
|
336
|
+
# #base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
|
|
337
|
+
# #encryptionKey := func() []byte { sum := mac.Sum(nil); return sum[:16] }()
|
|
338
|
+
# encryption_key:bytes = hmac.new(base_key, b'encryption', 'sha1').digest()
|
|
339
|
+
# _logsi.LogBinary(SILevel.Verbose, "Encryption Key", encryption_key)
|
|
340
|
+
|
|
341
|
+
# # # create remote device key.
|
|
342
|
+
# # remote_device_key:int = b64_to_int(self.RemotePublicKeyBase64String)
|
|
343
|
+
# # _logsi.LogValue(SILevel.Verbose, "Remote Device Key", remote_device_key)
|
|
344
|
+
|
|
345
|
+
# # # create shared key.
|
|
346
|
+
# # shared_key:int = self.dh_keys.shared_secret(remote_device_key)
|
|
347
|
+
# # _logsi.LogValue(SILevel.Verbose, "Shared Key: %s" % shared_key, shared_key)
|
|
348
|
+
|
|
349
|
+
# # b_shared_key:bytes = int_to_bytes(shared_key)
|
|
350
|
+
# # base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
|
|
351
|
+
|
|
352
|
+
# # # create encryption key.
|
|
353
|
+
# # encryption_key:bytes = hmac.new(base_key, b'encryption', 'sha1').digest()
|
|
354
|
+
# # _logsi.LogBinary(SILevel.Verbose, "Encryption Key", encryption_key)
|
|
355
|
+
|
|
356
|
+
# # create new AES cipher, and decrypt the data.
|
|
357
|
+
# ctr_e:dict = Counter.new(128, initial_value=byte_list_to_int(iv))
|
|
358
|
+
# cipher = AES.new(encryption_key[:16], AES.MODE_CTR, counter=ctr_e)
|
|
359
|
+
# self._decrypted_blob = cipher.decrypt(self._EncryptedBlob)
|
|
360
|
+
# _logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted base64-encoded bytes", self._decrypted_blob)
|
|
361
|
+
# self._blob = b64decode(self._decrypted_blob)
|
|
362
|
+
# _logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted Bytes", self._blob)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# # cipher.NewCTR(bc, iv).XORKeyStream(decrypted, encrypted)
|
|
366
|
+
|
|
367
|
+
# finally:
|
|
368
|
+
|
|
369
|
+
# # trace.
|
|
370
|
+
# _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# our package imports.
|
|
2
|
+
from .authenticationtypes import AuthenticationTypes
|
|
3
|
+
|
|
4
|
+
class Credentials:
|
|
5
|
+
"""
|
|
6
|
+
Credentials used by the Spotify SpConnectionLoginBlob method.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
username:str,
|
|
12
|
+
password:str,
|
|
13
|
+
authenticationType:AuthenticationTypes=AuthenticationTypes.USER_PASS,
|
|
14
|
+
):
|
|
15
|
+
"""
|
|
16
|
+
Initializes a new instance of the class.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
username (str):
|
|
20
|
+
Spotify Connect user name to login with.
|
|
21
|
+
password (str):
|
|
22
|
+
Spotify Connect user password to login with.
|
|
23
|
+
authenticationType (AuthenticationTypes):
|
|
24
|
+
Authentication type.
|
|
25
|
+
"""
|
|
26
|
+
# validations.
|
|
27
|
+
if (authenticationType is None) or (not isinstance(authenticationType, AuthenticationTypes)):
|
|
28
|
+
authenticationType = AuthenticationTypes.USER_PASS
|
|
29
|
+
|
|
30
|
+
# initialize storage.
|
|
31
|
+
self.username: bytes = bytes(username, 'ascii')
|
|
32
|
+
if (isinstance(password, bytes)):
|
|
33
|
+
self.password: bytes = password
|
|
34
|
+
else:
|
|
35
|
+
self.password: bytes = bytes(password, 'ascii')
|
|
36
|
+
self.auth_type: AuthenticationTypes = authenticationType
|
|
37
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
|
|
3
|
+
from .helpers import byte_list_to_int, int_to_bytes, b64_to_int, int_to_b64str
|
|
4
|
+
|
|
5
|
+
class CryptoDiffieHellman:
|
|
6
|
+
"""
|
|
7
|
+
Diffie Hellman cryptography helper class.
|
|
8
|
+
|
|
9
|
+
The following link does a pretty good job of explaining this process:
|
|
10
|
+
https://www.comparitech.com/blog/information-security/diffie-hellman-key-exchange/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
DH_GENERATOR = 2
|
|
14
|
+
DH_PRIME_MODULUS = byte_list_to_int([
|
|
15
|
+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2,
|
|
16
|
+
0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67,
|
|
17
|
+
0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e,
|
|
18
|
+
0x34, 0x04, 0xdd, 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d,
|
|
19
|
+
0xf2, 0x5f, 0x14, 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5,
|
|
20
|
+
0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff,
|
|
21
|
+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
"""
|
|
26
|
+
Initializes a new instance of the class.
|
|
27
|
+
"""
|
|
28
|
+
# generate our private key value.
|
|
29
|
+
self._PrivateKey = secrets.randbits(95 * 8)
|
|
30
|
+
|
|
31
|
+
# generate a public key that we can share with a partner.
|
|
32
|
+
self._PublicKey:int = pow(CryptoDiffieHellman.DH_GENERATOR, self._PrivateKey, CryptoDiffieHellman.DH_PRIME_MODULUS)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def PublicKey(self) -> int:
|
|
37
|
+
"""
|
|
38
|
+
Returns our public key in an integer format.
|
|
39
|
+
"""
|
|
40
|
+
return self._PublicKey
|
|
41
|
+
|
|
42
|
+
@PublicKey.setter
|
|
43
|
+
def PublicKey(self, value:int):
|
|
44
|
+
"""
|
|
45
|
+
Sets the PublicKey property value.
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(value, int):
|
|
48
|
+
self._PublicKey = value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def PublicKeyBase64String(self) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Returns our public key in a base64 encoded string format.
|
|
55
|
+
"""
|
|
56
|
+
return int_to_b64str(self._PublicKey)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def SharedSecret(self, remotePublicKey:int):
|
|
60
|
+
"""
|
|
61
|
+
Returns a shared secret value that is calculated based upon the specified `remotePublicKey`
|
|
62
|
+
value, our private key value, and a shared Modulus value.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
remotePublicKey (int):
|
|
66
|
+
The Remote clients PublicKey value.
|
|
67
|
+
|
|
68
|
+
The shared secret value can be calculated as:
|
|
69
|
+
SharedSecret = remotePublicKey(base,g) ** PrivateKey(secret,a) % DH_PRIME(modulus,p)
|
|
70
|
+
SharedSecret = g**a mod p
|
|
71
|
+
SharedSecret = base ** secret % modulus
|
|
72
|
+
|
|
73
|
+
- base = remotePublicKey
|
|
74
|
+
- secret = PrivateKey
|
|
75
|
+
- modulus = DH_PRIME
|
|
76
|
+
"""
|
|
77
|
+
# calculate the shared secret value based upon the remote's PublicKey, our PrivateKey, and the shared Modulus.
|
|
78
|
+
|
|
79
|
+
# another way to express this formula is:
|
|
80
|
+
# SharedSecret = g**a mod p
|
|
81
|
+
|
|
82
|
+
# another way to express this formula is:
|
|
83
|
+
# SharedSecret = base ** secret % modulus
|
|
84
|
+
|
|
85
|
+
# - base or `g` = remotePublicKey
|
|
86
|
+
# - secret or `a` = PrivateKey
|
|
87
|
+
# - modulus or `p` = DH_PRIME
|
|
88
|
+
return pow(remotePublicKey, self._PrivateKey, CryptoDiffieHellman.DH_PRIME_MODULUS)
|
|
89
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from base64 import b64decode, b64encode
|
|
2
|
+
import binascii
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def write_int(i: int, out: bytearray):
|
|
6
|
+
if i < 0x80:
|
|
7
|
+
out.append(i)
|
|
8
|
+
else:
|
|
9
|
+
out.append(0x80 | (i & 0x7f))
|
|
10
|
+
out.append((i >> 7))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def write_bytes(b: bytes, out: bytearray):
|
|
14
|
+
write_int(len(b), out)
|
|
15
|
+
out.extend(b)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def byte_list_to_int(byte_list) -> int:
|
|
19
|
+
"""
|
|
20
|
+
Converts a list of bytes to a big integer.
|
|
21
|
+
"""
|
|
22
|
+
return int.from_bytes(bytes(byte_list), 'big')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def int_to_bytes(value: int) -> bytes:
|
|
26
|
+
"""
|
|
27
|
+
Converts a big integer to bytes.
|
|
28
|
+
"""
|
|
29
|
+
return value.to_bytes((value.bit_length() + 7) // 8, byteorder='big') or b'\0'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def int_to_b64str(value: int) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Converts a big integer to a base64-encoded string value.
|
|
35
|
+
"""
|
|
36
|
+
return b64encode(int_to_bytes(value)).decode('ascii')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def b64_to_int(value: str):
|
|
40
|
+
"""
|
|
41
|
+
Converts a base64-encoded string or byte array to a big integer.
|
|
42
|
+
"""
|
|
43
|
+
return int.from_bytes(b64decode(value), 'big')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def b64str_to_bytes(value: str):
|
|
47
|
+
"""
|
|
48
|
+
Decodes a base64-encoded string to a byte array.
|
|
49
|
+
"""
|
|
50
|
+
return b64decode(value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def string_to_int(value):
|
|
54
|
+
"""
|
|
55
|
+
Converts a string to a big integer.
|
|
56
|
+
"""
|
|
57
|
+
return int(binascii.hexlify(value), 16)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def int_of_string(value):
|
|
61
|
+
"""
|
|
62
|
+
Converts a string to a big integer.
|
|
63
|
+
"""
|
|
64
|
+
return int(binascii.hexlify(value), 16)
|
|
@@ -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
|