|
29 | 29 | from Cryptodome.PublicKey import RSA |
30 | 30 | from Cryptodome.Signature import PKCS1_v1_5 |
31 | 31 |
|
32 | | -from librespot import util, oauth |
| 32 | +from librespot import util |
33 | 33 | from librespot import Version |
34 | 34 | from librespot.audio import AudioKeyManager |
35 | 35 | from librespot.audio import CdnManager |
|
57 | 57 | from librespot.proto import Metadata_pb2 as Metadata |
58 | 58 | from librespot.proto import Playlist4External_pb2 as Playlist4External |
59 | 59 | from librespot.proto.ExplicitContentPubsub_pb2 import UserAttributesUpdate |
| 60 | +from librespot.proto.spotify.login5.v3 import Login5_pb2 as Login5 |
| 61 | +from librespot.proto.spotify.login5.v3.credentials import Credentials_pb2 as Login5Credentials |
60 | 62 | from librespot.structure import Closeable |
61 | 63 | from librespot.structure import MessageListener |
62 | 64 | from librespot.structure import RequestListener |
@@ -908,6 +910,8 @@ class Session(Closeable, MessageListener, SubListener): |
908 | 910 | __dealer_client: typing.Union[DealerClient, None] = None |
909 | 911 | __event_service: typing.Union[EventService, None] = None |
910 | 912 | __keys: DiffieHellman |
| 913 | + __login5_access_token: typing.Union[str, None] = None |
| 914 | + __login5_token_expiry: typing.Union[int, None] = None |
911 | 915 | __mercury_client: MercuryClient |
912 | 916 | __receiver: typing.Union[Receiver, None] = None |
913 | 917 | __search: typing.Union[SearchManager, None] |
@@ -968,6 +972,7 @@ def authenticate(self, |
968 | 972 |
|
969 | 973 | """ |
970 | 974 | self.__authenticate_partial(credential, False) |
| 975 | + self.__authenticate_login5() |
971 | 976 | with self.__auth_lock: |
972 | 977 | self.__mercury_client = MercuryClient(self) |
973 | 978 | self.__token_provider = TokenProvider(self) |
@@ -1203,6 +1208,13 @@ def is_valid(self) -> bool: |
1203 | 1208 | self.__wait_auth_lock() |
1204 | 1209 | return self.__ap_welcome is not None and self.connection is not None |
1205 | 1210 |
|
| 1211 | + def login5(self) -> tuple[str, int]: |
| 1212 | + """ """ |
| 1213 | + self.__wait_auth_lock() |
| 1214 | + if self.__login5_access_token is None or self.__login5_token_expiry is None: |
| 1215 | + raise RuntimeError("Session isn't authenticated!") |
| 1216 | + return self.__login5_access_token, self.__login5_token_expiry |
| 1217 | + |
1206 | 1218 | def mercury(self) -> MercuryClient: |
1207 | 1219 | """ """ |
1208 | 1220 | self.__wait_auth_lock() |
@@ -1382,6 +1394,43 @@ def __authenticate_partial(self, |
1382 | 1394 | else: |
1383 | 1395 | raise RuntimeError("Unknown CMD 0x" + packet.cmd.hex()) |
1384 | 1396 |
|
| 1397 | + def __authenticate_login5(self) -> None: |
| 1398 | + """Authenticate using Login5 to get access token""" |
| 1399 | + login5_request = Login5.LoginRequest() |
| 1400 | + login5_request.client_info.client_id = MercuryRequests.keymaster_client_id |
| 1401 | + login5_request.client_info.device_id = self.__inner.device_id |
| 1402 | + |
| 1403 | + # Set stored credential from APWelcome |
| 1404 | + if hasattr(self, '_Session__ap_welcome') and self.__ap_welcome: |
| 1405 | + stored_cred = Login5Credentials.StoredCredential() |
| 1406 | + stored_cred.username = self.__ap_welcome.canonical_username |
| 1407 | + stored_cred.data = self.__ap_welcome.reusable_auth_credentials |
| 1408 | + login5_request.stored_credential.CopyFrom(stored_cred) |
| 1409 | + |
| 1410 | + response = requests.post( |
| 1411 | + "https://login5.spotify.com/v3/login", |
| 1412 | + data=login5_request.SerializeToString(), |
| 1413 | + headers={ |
| 1414 | + "Content-Type": "application/x-protobuf", |
| 1415 | + "Accept": "application/x-protobuf" |
| 1416 | + } |
| 1417 | + ) |
| 1418 | + |
| 1419 | + if response.status_code == 200: |
| 1420 | + login5_response = Login5.LoginResponse() |
| 1421 | + login5_response.ParseFromString(response.content) |
| 1422 | + |
| 1423 | + if login5_response.HasField('ok'): |
| 1424 | + self.__login5_access_token = login5_response.ok.access_token |
| 1425 | + self.__login5_token_expiry = int(time.time()) + login5_response.ok.access_token_expires_in |
| 1426 | + self.logger.info("Login5 authentication successful, got access token") |
| 1427 | + else: |
| 1428 | + self.logger.warning("Login5 authentication failed: {}".format(login5_response.error)) |
| 1429 | + else: |
| 1430 | + self.logger.warning("Login5 request failed with status: {}".format(response.status_code)) |
| 1431 | + else: |
| 1432 | + self.logger.error("Login5 authentication failed: No APWelcome found") |
| 1433 | + |
1385 | 1434 | def __send_unchecked(self, cmd: bytes, payload: bytes) -> None: |
1386 | 1435 | self.cipher_pair.send_encoded(self.connection, cmd, payload) |
1387 | 1436 |
|
@@ -2258,7 +2307,7 @@ class TokenProvider: |
2258 | 2307 | __tokens: typing.List[StoredToken] = [] |
2259 | 2308 |
|
2260 | 2309 | def __init__(self, session: Session): |
2261 | | - self._session = session |
| 2310 | + self.__session = session |
2262 | 2311 |
|
2263 | 2312 | def find_token_with_all_scopes( |
2264 | 2313 | self, scopes: typing.List[str]) -> typing.Union[StoredToken, None]: |
@@ -2289,24 +2338,27 @@ def get_token(self, *scopes) -> StoredToken: |
2289 | 2338 | scopes = list(scopes) |
2290 | 2339 | if len(scopes) == 0: |
2291 | 2340 | raise RuntimeError("The token doesn't have any scope") |
| 2341 | + |
2292 | 2342 | token = self.find_token_with_all_scopes(scopes) |
2293 | 2343 | if token is not None: |
2294 | 2344 | if token.expired(): |
2295 | 2345 | self.__tokens.remove(token) |
2296 | 2346 | else: |
2297 | 2347 | return token |
2298 | | - self.logger.debug( |
2299 | | - "Token expired or not suitable, requesting again. scopes: {}, old_token: {}" |
2300 | | - .format(scopes, token)) |
2301 | | - response = self._session.mercury().send_sync_json( |
2302 | | - MercuryRequests.request_token(self._session.device_id(), |
2303 | | - ",".join(scopes))) |
2304 | | - token = TokenProvider.StoredToken(response) |
2305 | | - self.logger.debug( |
2306 | | - "Updated token successfully! scopes: {}, new_token: {}".format( |
2307 | | - scopes, token)) |
2308 | | - self.__tokens.append(token) |
2309 | | - return token |
| 2348 | + |
| 2349 | + login5_token = None |
| 2350 | + login5_access_token, login5_token_expiry = self.__session.login5() |
| 2351 | + if int(time.time()) < login5_token_expiry - 60: # 60 second buffer |
| 2352 | + login5_token = TokenProvider.StoredToken({ |
| 2353 | + "expiresIn": login5_token_expiry - int(time.time()), |
| 2354 | + "accessToken": login5_access_token, |
| 2355 | + "scope": scopes |
| 2356 | + }) |
| 2357 | + self.__tokens.append(login5_token) |
| 2358 | + self.logger.debug("Using Login5 access token for scopes: {}".format(scopes)) |
| 2359 | + else: |
| 2360 | + self.logger.debug("Login5 token expired, need to re-authenticate") |
| 2361 | + return login5_token |
2310 | 2362 |
|
2311 | 2363 | class StoredToken: |
2312 | 2364 | """ """ |
|
0 commit comments