spotifywebapiPython 1.0.76__py3-none-any.whl → 1.0.77__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.76.dist-info → spotifywebapiPython-1.0.77.dist-info}/METADATA +1 -1
- {spotifywebapiPython-1.0.76.dist-info → spotifywebapiPython-1.0.77.dist-info}/RECORD +23 -23
- spotifywebapipython/const.py +1 -1
- spotifywebapipython/docs/__init__.html +13 -2
- spotifywebapipython/docs/index.html +2 -2
- spotifywebapipython/docs/search.js +1 -1
- spotifywebapipython/docs/spotifywebapiPython/const.html +3 -3
- spotifywebapipython/docs/spotifywebapiPython/saappmessages.html +2 -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 +109 -53
- spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapierror.html +2 -2
- spotifywebapipython/oauthcli/authclient.py +10 -11
- spotifywebapipython/spotifyclient.py +282 -150
- spotifywebapipython/zeroconfapi/blobbuilder.py +7 -12
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +244 -43
- {spotifywebapiPython-1.0.76.dist-info → spotifywebapiPython-1.0.77.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.76.dist-info → spotifywebapiPython-1.0.77.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.76.dist-info → spotifywebapiPython-1.0.77.dist-info}/top_level.txt +0 -0
|
@@ -131,19 +131,14 @@ class BlobBuilder:
|
|
|
131
131
|
|
|
132
132
|
# create the blob data that will be sent to the device.
|
|
133
133
|
blob = bytearray()
|
|
134
|
-
# 'I'
|
|
135
|
-
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
# '
|
|
139
|
-
|
|
140
|
-
# auth_type
|
|
141
|
-
write_int(self.credentials.auth_type, blob)
|
|
142
|
-
# 'Q'
|
|
143
|
-
write_int(0x51, blob)
|
|
144
|
-
# password
|
|
145
|
-
write_bytes(self.credentials.password, blob)
|
|
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)
|
|
146
140
|
_logsi.LogBinary(SILevel.Verbose, "Blob Data, Unencrypted Bytes", blob)
|
|
141
|
+
|
|
147
142
|
# padding
|
|
148
143
|
n_zeros = BlobBuilder.AES_BLOCK_SIZE - (len(blob) % BlobBuilder.AES_BLOCK_SIZE) - 1
|
|
149
144
|
blob.extend([0] * n_zeros)
|
|
@@ -13,9 +13,14 @@ from .zeroconfresponse import ZeroconfResponse
|
|
|
13
13
|
from .zeroconfgetinfo import ZeroconfGetInfo
|
|
14
14
|
from ..saappmessages import SAAppMessages
|
|
15
15
|
from ..sautils import export, validateDelay
|
|
16
|
+
from ..spotifyauthtoken import SpotifyAuthToken
|
|
16
17
|
from ..spotifyapierror import SpotifyApiError
|
|
17
18
|
from ..spotifywebapierror import SpotifyWebApiError
|
|
19
|
+
from ..oauthcli.authclient import AuthClient
|
|
18
20
|
from ..const import (
|
|
21
|
+
SPOTIFY_API_AUTHORIZE_URL,
|
|
22
|
+
SPOTIFY_API_TOKEN_URL,
|
|
23
|
+
TRACE_METHOD_RESULT,
|
|
19
24
|
TRACE_MSG_DELAY_DEVICE,
|
|
20
25
|
)
|
|
21
26
|
from ..sautils import passwordMaskString
|
|
@@ -145,7 +150,7 @@ class ZeroconfConnect:
|
|
|
145
150
|
return self._Version or ''
|
|
146
151
|
|
|
147
152
|
|
|
148
|
-
def _CheckResponseForErrors(self, response:Response, methodName:str, endpoint:str) -> None:
|
|
153
|
+
def _CheckResponseForErrors(self, response:Response, methodName:str, endpoint:str, tracePrefix:str='ZeroconfConnect') -> None:
|
|
149
154
|
"""
|
|
150
155
|
Checks the Spotify Zeroconf API response for errors.
|
|
151
156
|
|
|
@@ -162,6 +167,9 @@ class ZeroconfConnect:
|
|
|
162
167
|
method name that made the request (for trace purposes).
|
|
163
168
|
endpoint (str):
|
|
164
169
|
Endpoint that was requested (for trace purposes).
|
|
170
|
+
tracePrefix (str):
|
|
171
|
+
Prefix text used in trace log messages.
|
|
172
|
+
Default is "ZeroconfConnect".
|
|
165
173
|
|
|
166
174
|
Raises:
|
|
167
175
|
SpotifyZeroconfApiError:
|
|
@@ -174,13 +182,13 @@ class ZeroconfConnect:
|
|
|
174
182
|
|
|
175
183
|
# trace.
|
|
176
184
|
if _logsi.IsOn(SILevel.Debug):
|
|
177
|
-
_logsi.LogObject(SILevel.Debug, '
|
|
185
|
+
_logsi.LogObject(SILevel.Debug, '%s http response object - type="%s", module="%s"' % (tracePrefix, type(response).__name__, type(response).__module__), response)
|
|
178
186
|
|
|
179
187
|
# trace.
|
|
180
188
|
if _logsi.IsOn(SILevel.Debug):
|
|
181
|
-
_logsi.LogObject(SILevel.Debug, "
|
|
189
|
+
_logsi.LogObject(SILevel.Debug, "%s http response [%s-%s]: '%s' (response)" % (tracePrefix, response.status_code, response.reason, endpoint), response)
|
|
182
190
|
if (response.headers):
|
|
183
|
-
_logsi.LogCollection(SILevel.Debug, "
|
|
191
|
+
_logsi.LogCollection(SILevel.Debug, "%s http response [%s-%s]: '%s' (headers)" % (tracePrefix, response.status_code, response.reason, endpoint), response.headers.items())
|
|
184
192
|
|
|
185
193
|
# do we have response data?
|
|
186
194
|
if (response.content is not None) and (len(response.content) > 0):
|
|
@@ -193,11 +201,11 @@ class ZeroconfConnect:
|
|
|
193
201
|
|
|
194
202
|
if _logsi.IsOn(SILevel.Verbose):
|
|
195
203
|
if isinstance(responseData, dict):
|
|
196
|
-
_logsi.LogDictionary(SILevel.Verbose, "
|
|
204
|
+
_logsi.LogDictionary(SILevel.Verbose, "%s http response [%s-%s]: '%s' (json dict)" % (tracePrefix, response.status_code, response.reason, endpoint), responseData, prettyPrint=True)
|
|
197
205
|
elif isinstance(responseData, list):
|
|
198
|
-
_logsi.LogArray(SILevel.Verbose, "
|
|
206
|
+
_logsi.LogArray(SILevel.Verbose, "%s http response [%s-%s]: '%s' (json array)" % (tracePrefix, response.status_code, response.reason, endpoint), responseData)
|
|
199
207
|
else:
|
|
200
|
-
_logsi.LogObject(SILevel.Verbose, "
|
|
208
|
+
_logsi.LogObject(SILevel.Verbose, "%s http response [%s-%s]: '%s' (json object)" % (tracePrefix, response.status_code, response.reason, endpoint), responseData)
|
|
201
209
|
|
|
202
210
|
else:
|
|
203
211
|
|
|
@@ -209,12 +217,12 @@ class ZeroconfConnect:
|
|
|
209
217
|
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
210
218
|
except Exception as ex:
|
|
211
219
|
|
|
212
|
-
_logsi.LogException("
|
|
220
|
+
_logsi.LogException("{tracePrefix} http response could not be converted to JSON and will be converted to utf-8.\nConversion exception returned was:\n{ex}".format(tracePrefix=tracePrefix, ex=str(ex)), ex, logToSystemLogger=False)
|
|
213
221
|
|
|
214
222
|
# if json conversion failed, then convert to utf-8 response.
|
|
215
223
|
if response.content is not None:
|
|
216
224
|
responseUTF8 = response.content.decode('utf-8')
|
|
217
|
-
_logsi.LogText(SILevel.Error, "
|
|
225
|
+
_logsi.LogText(SILevel.Error, "%s http response [%s-%s]: '%s' (utf-8)" % (tracePrefix, response.status_code, response.reason, endpoint), responseUTF8)
|
|
218
226
|
|
|
219
227
|
# at this point we don't know what Spotify Web Api returned!
|
|
220
228
|
# it's been found that some devices (Sonos, etc) do not return a proper JSON response.
|
|
@@ -465,29 +473,29 @@ class ZeroconfConnect:
|
|
|
465
473
|
apiMethodParms.AppendKeyValue("info.PublicKey", info.PublicKey)
|
|
466
474
|
apiMethodParms.AppendKeyValue("info.DeviceId", info.DeviceId)
|
|
467
475
|
apiMethodParms.AppendKeyValue("info.RemoteName", info.RemoteName)
|
|
476
|
+
apiMethodParms.AppendKeyValue("info.TokenType", info.TokenType)
|
|
468
477
|
_logsi.LogMethodParmList(SILevel.Verbose, "Issuing Spotify Connect Zeroconf addUser request (ip=%s)" % self._HostIpAddress, apiMethodParms)
|
|
469
478
|
|
|
470
|
-
# formulate the blob.
|
|
471
|
-
credentials:Credentials = Credentials(username, password, AuthenticationTypes.USER_PASS)
|
|
472
|
-
builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
|
|
473
|
-
blob = builder.build()
|
|
474
|
-
|
|
475
479
|
# set request endpoint.
|
|
476
480
|
endpoint:str = self.GetEndpoint('addUser')
|
|
477
481
|
|
|
478
482
|
# set request headers.
|
|
479
483
|
reqHeaders:dict = {
|
|
480
484
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
481
|
-
'Connection': 'close'
|
|
485
|
+
#'Connection': 'close',
|
|
486
|
+
'Connection': 'keep-alive',
|
|
487
|
+
'Keep-Alive': '0',
|
|
488
|
+
'Accept-Encoding': 'gzip',
|
|
482
489
|
}
|
|
483
|
-
|
|
490
|
+
|
|
491
|
+
# create credentials and builder objects.
|
|
492
|
+
credentials:Credentials = Credentials(username, password, AuthenticationTypes.USER_PASS)
|
|
493
|
+
builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
|
|
484
494
|
|
|
485
495
|
# set defaults used by most manufacturers - we will tailor these in the
|
|
486
|
-
# logic below for specific manufacturers / devices as required
|
|
487
|
-
# - clientKey should be the getInfo response PublicKey (base64 encoded).
|
|
496
|
+
# logic below for specific manufacturers / devices as required.
|
|
488
497
|
# - exclude origin device information from the addUser request.
|
|
489
498
|
# - tokenType should be set to 'default'.
|
|
490
|
-
clientKey:str = builder.dh_keys.PublicKeyBase64String
|
|
491
499
|
includeOriginDeviceInfo:bool = False
|
|
492
500
|
tokenType:str = 'default'
|
|
493
501
|
|
|
@@ -496,33 +504,32 @@ class ZeroconfConnect:
|
|
|
496
504
|
includeOriginDeviceInfo = True
|
|
497
505
|
|
|
498
506
|
# special processing for tokenType "authorization_code":
|
|
499
|
-
# - include origin device information in the addUser request.
|
|
500
|
-
# - set the tokenType to 'authorization_code'.
|
|
501
|
-
# - set the clientKey value to null, as it is contained in the token.
|
|
502
507
|
if info.TokenType == 'authorization_code':
|
|
508
|
+
|
|
509
|
+
# set defaults used by token type "authorization_code".
|
|
510
|
+
# - set the clientKey value to null, as it is not used to encrypt / decrypt anything.
|
|
511
|
+
# - include origin device information in the addUser request.
|
|
512
|
+
# - set the tokenType to 'authorization_code'.
|
|
503
513
|
clientKey = ''
|
|
504
514
|
includeOriginDeviceInfo = True
|
|
505
515
|
tokenType = info.TokenType
|
|
516
|
+
|
|
517
|
+
# get authorization_code access token.
|
|
518
|
+
blob:str = self._GetSpotifyConnectAuthorizationCodeToken(info)
|
|
506
519
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
#
|
|
510
|
-
|
|
511
|
-
#
|
|
512
|
-
#
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
#
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
#builder._OriginDeviceName = 'THLUCASI9'
|
|
521
|
-
#builder._OriginDeviceId = '80da4987232671a83397682373f7ce21ea92f2e4'
|
|
522
|
-
#loginId = 'bc5ac8ea51ec48477ff2303d3934db9f'
|
|
523
|
-
# the following is a refresh_token value:
|
|
524
|
-
#blob = 'AQAOzwQB9mAQZcHiqaJgXuaDF88hW3Nt9UDXldqpQfZ6GJPmoSZ2eSN5HC3TQg08r9kZlNch6cJg4kQYysyRzs_S3TTMWHBohpvBLEnoR6_UePhzJVKbjP9exb8MAvUDdjE'
|
|
525
|
-
|
|
520
|
+
else:
|
|
521
|
+
|
|
522
|
+
# set defaults used by this token type:
|
|
523
|
+
# - clientKey should be the getInfo response PublicKey (base64 encoded).
|
|
524
|
+
# - exclude origin device information from the addUser request.
|
|
525
|
+
# - tokenType should be set to 'default'.
|
|
526
|
+
clientKey:str = builder.dh_keys.PublicKeyBase64String
|
|
527
|
+
includeOriginDeviceInfo = False
|
|
528
|
+
tokenType = 'default'
|
|
529
|
+
|
|
530
|
+
# formulate the blob.
|
|
531
|
+
blob = builder.build()
|
|
532
|
+
|
|
526
533
|
# set request parameters.
|
|
527
534
|
reqData={
|
|
528
535
|
'action': 'addUser',
|
|
@@ -530,10 +537,10 @@ class ZeroconfConnect:
|
|
|
530
537
|
'tokenType': tokenType,
|
|
531
538
|
'clientKey': clientKey,
|
|
532
539
|
'loginId': loginId or '', # canonical login id (e.g. "31l77fd87g8h9j00k89f07jf87ge")
|
|
533
|
-
'userName': credentials.username.decode('ascii'), # user name (e.g. "
|
|
540
|
+
'userName': credentials.username.decode('ascii'), # canonical user name (e.g. "31l77fd87g8h9j00k89f07jf87ge")
|
|
534
541
|
'blob': blob,
|
|
535
542
|
}
|
|
536
|
-
|
|
543
|
+
|
|
537
544
|
# are we including origin device information (e.g. deviceName, deviceId)?
|
|
538
545
|
# the `deviceName` and `deviceId` keys are specified by some manufacturers for the
|
|
539
546
|
# `addUser` action. I am not sure if they are required or not, as they are not part
|
|
@@ -552,6 +559,7 @@ class ZeroconfConnect:
|
|
|
552
559
|
reqData['clientKey'] = ''
|
|
553
560
|
|
|
554
561
|
# trace.
|
|
562
|
+
_logsi.LogDictionary(SILevel.Debug, "ZeroconfConnect http request: '%s' (headers)" % (endpoint), reqHeaders)
|
|
555
563
|
_logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (data)" % (endpoint), reqData)
|
|
556
564
|
|
|
557
565
|
# execute spotify zeroconf api request.
|
|
@@ -572,6 +580,199 @@ class ZeroconfConnect:
|
|
|
572
580
|
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
573
581
|
|
|
574
582
|
|
|
583
|
+
def _GetSpotifyConnectAuthorizationCodeToken(
|
|
584
|
+
self,
|
|
585
|
+
info:ZeroconfGetInfo,
|
|
586
|
+
) -> str:
|
|
587
|
+
"""
|
|
588
|
+
Exchange a Spotify access token for an authorization_code access token.
|
|
589
|
+
The resulting token will be used as the `blob` argument for the Spotify Connect Zeroconf `addUser` request.
|
|
590
|
+
"""
|
|
591
|
+
apiMethodName:str = '_GetSpotifyConnectAuthorizationCodeToken'
|
|
592
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
593
|
+
tracePrefix:str = 'SpotifyConnectAuthorizationCodeToken exchange'
|
|
594
|
+
|
|
595
|
+
# based upon Fiddler trace, it the tokenType "authorization_code" flow exchanges an
|
|
596
|
+
# access-token for an "authorization_code" token. it does this by calling
|
|
597
|
+
# "accounts.spotify.com/api/token" to exchange the spotify "login5" "client-token" value
|
|
598
|
+
# to an "authorization_code" token value, which is then passed on the Spotify Connect
|
|
599
|
+
# Zeroconf "adduser" call as the "blob" parameter.
|
|
600
|
+
|
|
601
|
+
try:
|
|
602
|
+
|
|
603
|
+
# trace.
|
|
604
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
605
|
+
apiMethodParms.AppendKeyValue("info.DeviceId", info.DeviceId)
|
|
606
|
+
apiMethodParms.AppendKeyValue("info.RemoteName", info.RemoteName)
|
|
607
|
+
apiMethodParms.AppendKeyValue("info.BrandDisplayName", info.BrandDisplayName)
|
|
608
|
+
apiMethodParms.AppendKeyValue("info.ModelDisplayName", info.ModelDisplayName)
|
|
609
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Preparing to generate a new authorization_code access token for Spotify Connect device: '%s' (%s)" % (info.RemoteName, info.DeviceId), apiMethodParms)
|
|
610
|
+
|
|
611
|
+
# set variables for spotify authorization_code token generation.
|
|
612
|
+
# the redirecy uri variables were obtained via Fiddler trace using Spotify Desktop App to initiate Spotify Connect to device.
|
|
613
|
+
redirectUriHost:str = '127.0.0.1'
|
|
614
|
+
redirectUriPort:str = 4381
|
|
615
|
+
redirectUriPath:str = '/login'
|
|
616
|
+
authorizationType:str = 'Authorization Code PKCE'
|
|
617
|
+
tokenStorageDir:str = './test/testdata'
|
|
618
|
+
tokenProfileId:str = 'SpotifyDesktopApp_Scopes_Streaming' # 'SpotifyDesktopApp_Scopes_Streaming', 'SpotifyDesktopApp_Scopes_All', 'SpotifyDesktopApp_Scopes_NoExtras'
|
|
619
|
+
SPOTIFY_DESKTOP_APP_CLIENT_ID:str = '65b708073fc0480ea92a077233ca87bd' # Spotify Desktop App client id
|
|
620
|
+
|
|
621
|
+
# Spotify Desktop App scopes requested for Spotify Connect
|
|
622
|
+
SPOTIFY_SCOPES:list = \
|
|
623
|
+
[
|
|
624
|
+
# 'playlist-modify-private',
|
|
625
|
+
# 'playlist-modify-public',
|
|
626
|
+
# 'playlist-read-collaborative',
|
|
627
|
+
# 'playlist-read-private',
|
|
628
|
+
# 'ugc-image-upload',
|
|
629
|
+
# 'user-follow-modify',
|
|
630
|
+
# 'user-follow-read',
|
|
631
|
+
# 'user-library-modify',
|
|
632
|
+
# 'user-library-read',
|
|
633
|
+
# 'user-modify-playback-state',
|
|
634
|
+
# 'user-read-currently-playing',
|
|
635
|
+
# 'user-read-email',
|
|
636
|
+
# 'user-read-playback-position',
|
|
637
|
+
# 'user-read-playback-state',
|
|
638
|
+
# 'user-read-private',
|
|
639
|
+
# 'user-read-recently-played',
|
|
640
|
+
# 'user-top-read',
|
|
641
|
+
# *** streaming scope
|
|
642
|
+
'streaming',
|
|
643
|
+
# *** extra scopes used by spotify desktop player
|
|
644
|
+
# 'app-remote-control',
|
|
645
|
+
# 'playlist-modify',
|
|
646
|
+
# 'playlist-read',
|
|
647
|
+
# 'user-modify',
|
|
648
|
+
# 'user-modify-private',
|
|
649
|
+
# 'user-personalized',
|
|
650
|
+
# 'user-read-birthdate',
|
|
651
|
+
# 'user-read-play-history',
|
|
652
|
+
]
|
|
653
|
+
|
|
654
|
+
# create oauth provider for spotify authentication code with pkce.
|
|
655
|
+
_logsi.LogVerbose('creating OAuth2 provider for Spotify Authentication Code with PKCE')
|
|
656
|
+
authClient:AuthClient = AuthClient(
|
|
657
|
+
authorizationType=authorizationType,
|
|
658
|
+
authorizationUrl=SPOTIFY_API_AUTHORIZE_URL,
|
|
659
|
+
tokenUrl=SPOTIFY_API_TOKEN_URL,
|
|
660
|
+
scope=SPOTIFY_SCOPES,
|
|
661
|
+
clientId=SPOTIFY_DESKTOP_APP_CLIENT_ID,
|
|
662
|
+
tokenStorageDir=tokenStorageDir,
|
|
663
|
+
tokenProviderId='SpotifyWebApiAuthCodePkce',
|
|
664
|
+
tokenProfileId=tokenProfileId,
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
# force the user to logon to spotify to authorize the application access if we
|
|
668
|
+
# do not have an authorized access token, or if the calling application requested
|
|
669
|
+
# us (by force) to re-authorize, or if the scope has changed.
|
|
670
|
+
isAuthorized = authClient.IsAuthorized
|
|
671
|
+
_logsi.LogVerbose('Checking OAuth2 authorization status: IsAuthorized=%s' % isAuthorized)
|
|
672
|
+
|
|
673
|
+
if (isAuthorized == False):
|
|
674
|
+
|
|
675
|
+
# at this point, we need a new authorization token.
|
|
676
|
+
# the user has up to 2 minutes to respond to the request by copying / pasting the
|
|
677
|
+
# message / auth approval url from the system log into a browser window that is
|
|
678
|
+
# running on the local machine.
|
|
679
|
+
_logsi.LogVerbose('Preparing to retrieve a new OAuth2 authorization access token')
|
|
680
|
+
authClient.AuthorizeWithServer(
|
|
681
|
+
host=redirectUriHost,
|
|
682
|
+
port=redirectUriPort,
|
|
683
|
+
redirect_uri_path=redirectUriPath,
|
|
684
|
+
open_browser=False,
|
|
685
|
+
timeout_seconds=120
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
else:
|
|
689
|
+
|
|
690
|
+
_logsi.LogVerbose('OAuth2 authorization token has previously been authorized')
|
|
691
|
+
|
|
692
|
+
# process results.
|
|
693
|
+
oauth2token:dict = authClient.Session.token
|
|
694
|
+
authToken = SpotifyAuthToken(authClient.AuthorizationType, authClient.TokenProfileId, root=oauth2token)
|
|
695
|
+
|
|
696
|
+
# does token need to be refreshed?
|
|
697
|
+
if authToken.IsExpired:
|
|
698
|
+
|
|
699
|
+
# refresh the token.
|
|
700
|
+
# this will also store the refreshed token to disk to be used later if required.
|
|
701
|
+
_logsi.LogVerbose('OAuth2 authorization token has expired, or is about to; token will be refreshed')
|
|
702
|
+
oauth2token:dict = authClient.RefreshToken()
|
|
703
|
+
authToken = SpotifyAuthToken(authClient.AuthorizationType, authClient.TokenProfileId, root=oauth2token)
|
|
704
|
+
_logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT % apiMethodName, authToken, excludeNonPublic=True)
|
|
705
|
+
|
|
706
|
+
else:
|
|
707
|
+
|
|
708
|
+
_logsi.LogVerbose('OAuth2 authorization token has not expired')
|
|
709
|
+
|
|
710
|
+
# set spotify token request headers.
|
|
711
|
+
tokHeaders:dict = {}
|
|
712
|
+
tokHeaders['User-Agent'] = 'Spotify/124300420 Win32_x86_64/0 (PC desktop)'
|
|
713
|
+
tokHeaders['Accept-Language'] = 'en-Latn-US,en-US;q=0.9,en-Latn;q=0.8,en;q=0.7'
|
|
714
|
+
tokHeaders['Accept-Encoding'] = 'gzip, deflate, br, zstd'
|
|
715
|
+
|
|
716
|
+
# TEST TODO set client-token (for testing purposes).
|
|
717
|
+
#tokHeaders['client-token'] = 'AADEtsyn61 ... redacted ...'
|
|
718
|
+
|
|
719
|
+
# TEST TODO set authorization token (for testing purposes).
|
|
720
|
+
# tokHeaders['authorization'] = 'Bearer BQCYWYv266 ... redacted ...'
|
|
721
|
+
# tokHeaders['authorization'] = authToken.HeaderValue
|
|
722
|
+
# tokHeaders['client-token'] = ''
|
|
723
|
+
|
|
724
|
+
# set audience based upon the device info.
|
|
725
|
+
audience:str = info.ClientId
|
|
726
|
+
|
|
727
|
+
# reset audience based upon specific device brand / model info.
|
|
728
|
+
if info.IsBrandSonos:
|
|
729
|
+
audience = '9b377073ea334637b1406f329ce005de', # Sonos Spotify application id
|
|
730
|
+
|
|
731
|
+
# set spotify token request parameters.
|
|
732
|
+
tokData={
|
|
733
|
+
'audience': audience, # Brand-specific client id that can use the generated token
|
|
734
|
+
'client_id': SPOTIFY_DESKTOP_APP_CLIENT_ID, # Spotify desktop app client id
|
|
735
|
+
'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
|
|
736
|
+
'requested_token_type': 'urn:spotify:params:oauth:authorization_code',
|
|
737
|
+
'resource': 'urn:spotify:resources:connect',
|
|
738
|
+
'scope': 'streaming',
|
|
739
|
+
'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
|
|
740
|
+
'subject_token': authToken.AccessToken,
|
|
741
|
+
# TEST TODO set subject token (for testing purposes).
|
|
742
|
+
#'subject_token': 'BQBBc-0WrN ... redacted ...',
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
# trace.
|
|
746
|
+
_logsi.LogVerbose('Exchanging OAuth2 authorization access token for an authorization_code access token')
|
|
747
|
+
_logsi.LogDictionary(SILevel.Debug, "%s http request: '%s' (headers)" % (tracePrefix, SPOTIFY_API_TOKEN_URL), tokHeaders)
|
|
748
|
+
_logsi.LogDictionary(SILevel.Verbose, "%s http request: '%s' (data)" % (tracePrefix, SPOTIFY_API_TOKEN_URL), tokData)
|
|
749
|
+
|
|
750
|
+
# execute spotify access token request.
|
|
751
|
+
response = requests.post(
|
|
752
|
+
SPOTIFY_API_TOKEN_URL,
|
|
753
|
+
timeout=10,
|
|
754
|
+
headers=tokHeaders,
|
|
755
|
+
data=tokData
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
# check response for initial errors, and return json response.
|
|
759
|
+
responseData:dict = self._CheckResponseForErrors(response, apiMethodName, SPOTIFY_API_TOKEN_URL, tracePrefix)
|
|
760
|
+
result = responseData.get('access_token', None)
|
|
761
|
+
errorMsg = responseData.get('error', None)
|
|
762
|
+
|
|
763
|
+
# check for request errors.
|
|
764
|
+
if response.status_code != 200:
|
|
765
|
+
raise SpotifyZeroconfApiError(response.status_code, 'Spotify OAuth2 Token Exchange failed: %s' % (errorMsg or responseData), apiMethodName, response.reason, _logsi)
|
|
766
|
+
|
|
767
|
+
# return authorization_code access token to caller.
|
|
768
|
+
return result
|
|
769
|
+
|
|
770
|
+
finally:
|
|
771
|
+
|
|
772
|
+
# trace.
|
|
773
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
774
|
+
|
|
775
|
+
|
|
575
776
|
def Disconnect(
|
|
576
777
|
self,
|
|
577
778
|
delay:float=0.50,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|