fix: license + login5
This commit is contained in:
@@ -40,7 +40,6 @@ from librespot.crypto import CipherPair
|
||||
from librespot.crypto import DiffieHellman
|
||||
from librespot.crypto import Packet
|
||||
from librespot.mercury import MercuryClient
|
||||
from librespot.mercury import MercuryRequests
|
||||
from librespot.mercury import RawMercuryRequest
|
||||
from librespot.metadata import AlbumId
|
||||
from librespot.metadata import ArtistId
|
||||
@@ -56,6 +55,8 @@ from librespot.proto import Keyexchange_pb2 as Keyexchange
|
||||
from librespot.proto import Metadata_pb2 as Metadata
|
||||
from librespot.proto import Playlist4External_pb2 as Playlist4External
|
||||
from librespot.proto.ExplicitContentPubsub_pb2 import UserAttributesUpdate
|
||||
from librespot.proto.spotify.login5.v3 import Login5_pb2 as Login5
|
||||
from librespot.proto.spotify.login5.v3.credentials import Credentials_pb2 as Login5Credentials
|
||||
from librespot.structure import Closeable
|
||||
from librespot.structure import MessageListener
|
||||
from librespot.structure import RequestListener
|
||||
@@ -203,7 +204,7 @@ class ApiClient(Closeable):
|
||||
proto_req = ClientToken.ClientTokenRequest(
|
||||
request_type=ClientToken.ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST,
|
||||
client_data=ClientToken.ClientDataRequest(
|
||||
client_id=MercuryRequests.keymaster_client_id,
|
||||
client_id=Session.CLIENT_ID,
|
||||
client_version=Version.version_name,
|
||||
connectivity_sdk_data=Connectivity.ConnectivitySdkData(
|
||||
device_id=self.__session.device_id(),
|
||||
@@ -781,6 +782,7 @@ class MessageType(enum.Enum):
|
||||
|
||||
class Session(Closeable, MessageListener, SubListener):
|
||||
""" """
|
||||
CLIENT_ID = "65b708073fc0480ea92a077233ca87bd" # Spotify client ID for librespot
|
||||
cipher_pair: typing.Union[CipherPair, None]
|
||||
country_code: str = "EN"
|
||||
connection: typing.Union[ConnectionHolder, None]
|
||||
@@ -823,6 +825,8 @@ class Session(Closeable, MessageListener, SubListener):
|
||||
__stored_str: str = ""
|
||||
__token_provider: typing.Union[TokenProvider, None]
|
||||
__user_attributes = {}
|
||||
__login5_access_token: typing.Union[str, None] = None
|
||||
__login5_token_expiry: typing.Union[int, None] = None
|
||||
|
||||
def __init__(self, inner: Inner, address: str) -> None:
|
||||
self.__client = Session.create_client(inner.conf)
|
||||
@@ -862,6 +866,10 @@ class Session(Closeable, MessageListener, SubListener):
|
||||
|
||||
"""
|
||||
self.__authenticate_partial(credential, False)
|
||||
|
||||
# Try Login5 authentication for access token
|
||||
self.__authenticate_login5(credential)
|
||||
|
||||
with self.__auth_lock:
|
||||
self.__mercury_client = MercuryClient(self)
|
||||
self.__token_provider = TokenProvider(self)
|
||||
@@ -1279,6 +1287,66 @@ class Session(Closeable, MessageListener, SubListener):
|
||||
def __send_unchecked(self, cmd: bytes, payload: bytes) -> None:
|
||||
self.cipher_pair.send_encoded(self.connection, cmd, payload)
|
||||
|
||||
def __authenticate_login5(self, credential: Authentication.LoginCredentials) -> None:
|
||||
"""Authenticate using Login5 to get access token"""
|
||||
try:
|
||||
# Build Login5 request
|
||||
login5_request = Login5.LoginRequest()
|
||||
|
||||
# Set client info
|
||||
login5_request.client_info.client_id = Session.CLIENT_ID
|
||||
login5_request.client_info.device_id = self.__inner.device_id
|
||||
|
||||
# Set stored credential from APWelcome
|
||||
if hasattr(self, '_Session__ap_welcome') and self.__ap_welcome:
|
||||
stored_cred = Login5Credentials.StoredCredential()
|
||||
stored_cred.username = self.__ap_welcome.canonical_username
|
||||
stored_cred.data = self.__ap_welcome.reusable_auth_credentials
|
||||
login5_request.stored_credential.CopyFrom(stored_cred)
|
||||
|
||||
# Send Login5 request
|
||||
login5_url = "https://login5.spotify.com/v3/login"
|
||||
headers = {
|
||||
"Content-Type": "application/x-protobuf",
|
||||
"Accept": "application/x-protobuf"
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
login5_url,
|
||||
data=login5_request.SerializeToString(),
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
login5_response = Login5.LoginResponse()
|
||||
login5_response.ParseFromString(response.content)
|
||||
|
||||
if login5_response.HasField('ok'):
|
||||
self.__login5_access_token = login5_response.ok.access_token
|
||||
self.__login5_token_expiry = int(time.time()) + login5_response.ok.access_token_expires_in
|
||||
self.logger.info("Login5 authentication successful, got access token")
|
||||
else:
|
||||
error_msg = "Login5 authentication failed"
|
||||
if login5_response.HasField('error'):
|
||||
error_msg += f": {login5_response.error}"
|
||||
self.logger.error(error_msg)
|
||||
# Don't raise exception here, as the session can still work with some limitations
|
||||
else:
|
||||
self.logger.error("Login5 request failed with status: {}".format(response.status_code))
|
||||
# Don't raise exception here, as the session can still work with some limitations
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to authenticate with Login5: {}".format(e))
|
||||
# Don't raise exception here, as the session can still work with some limitations
|
||||
|
||||
def get_login5_token(self) -> typing.Union[str, None]:
|
||||
"""Get the Login5 access token if available and not expired"""
|
||||
if self.__login5_access_token and self.__login5_token_expiry:
|
||||
if int(time.time()) < self.__login5_token_expiry - 60: # 60 second buffer
|
||||
return self.__login5_access_token
|
||||
else:
|
||||
self.logger.debug("Login5 token expired, need to re-authenticate")
|
||||
return None
|
||||
|
||||
def __wait_auth_lock(self) -> None:
|
||||
if self.__closing and self.connection is None:
|
||||
self.logger.debug("Connection was broken while closing.")
|
||||
@@ -1921,7 +1989,7 @@ class Session(Closeable, MessageListener, SubListener):
|
||||
format(util.bytes_to_hex(packet.cmd),
|
||||
packet.payload))
|
||||
continue
|
||||
except (RuntimeError, ConnectionResetError) as ex:
|
||||
except (RuntimeError, ConnectionResetError, OSError) as ex:
|
||||
if self.__running:
|
||||
self.__session.logger.fatal(
|
||||
"Failed reading packet! {}".format(ex))
|
||||
@@ -2171,24 +2239,46 @@ class TokenProvider:
|
||||
scopes = list(scopes)
|
||||
if len(scopes) == 0:
|
||||
raise RuntimeError("The token doesn't have any scope")
|
||||
|
||||
# Use Login5 token
|
||||
login5_token = self._session.get_login5_token()
|
||||
if login5_token:
|
||||
# Create a StoredToken-compatible object using Login5 token
|
||||
login5_stored_token = TokenProvider.Login5StoredToken(login5_token, scopes)
|
||||
self.logger.debug("Using Login5 access token for scopes: {}".format(scopes))
|
||||
return login5_stored_token
|
||||
|
||||
# Check existing tokens
|
||||
token = self.find_token_with_all_scopes(scopes)
|
||||
if token is not None:
|
||||
if token.expired():
|
||||
self.__tokens.remove(token)
|
||||
else:
|
||||
return token
|
||||
self.logger.debug(
|
||||
"Token expired or not suitable, requesting again. scopes: {}, old_token: {}"
|
||||
.format(scopes, token))
|
||||
response = self._session.mercury().send_sync_json(
|
||||
MercuryRequests.request_token(self._session.device_id(),
|
||||
",".join(scopes)))
|
||||
token = TokenProvider.StoredToken(response)
|
||||
self.logger.debug(
|
||||
"Updated token successfully! scopes: {}, new_token: {}".format(
|
||||
scopes, token))
|
||||
self.__tokens.append(token)
|
||||
return token
|
||||
|
||||
# No valid token available
|
||||
raise RuntimeError("Unable to obtain access token. Login5 authentication may have failed.")
|
||||
|
||||
class Login5StoredToken:
|
||||
"""StoredToken-compatible wrapper for Login5 access tokens"""
|
||||
access_token: str
|
||||
scopes: typing.List[str]
|
||||
|
||||
def __init__(self, access_token: str, scopes: typing.List[str]):
|
||||
self.access_token = access_token
|
||||
self.scopes = scopes
|
||||
|
||||
def expired(self) -> bool:
|
||||
"""Login5 tokens are managed by Session, so delegate expiry check"""
|
||||
return False # Session handles expiry
|
||||
|
||||
def has_scope(self, scope: str) -> bool:
|
||||
"""Login5 tokens are general-purpose, assume they have all scopes"""
|
||||
return True
|
||||
|
||||
def has_scopes(self, sc: typing.List[str]) -> bool:
|
||||
"""Login5 tokens are general-purpose, assume they have all scopes"""
|
||||
return True
|
||||
|
||||
class StoredToken:
|
||||
""" """
|
||||
|
||||
@@ -299,13 +299,6 @@ class MercuryRequests:
|
||||
@TODO implement function
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def request_token(device_id, scope):
|
||||
return JsonMercuryRequest(
|
||||
RawMercuryRequest.get(
|
||||
"hm://keymaster/token/authenticated?scope={}&client_id={}&device_id={}"
|
||||
.format(scope, MercuryRequests.keymaster_client_id,
|
||||
device_id)))
|
||||
|
||||
|
||||
class RawMercuryRequest:
|
||||
|
||||
@@ -7,19 +7,19 @@ from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import enum_type_wrapper
|
||||
from spotify.login5.v3 import \
|
||||
client_info_pb2 as spotify_dot_login5_dot_v3_dot_client__info__pb2
|
||||
from spotify.login5.v3 import \
|
||||
user_info_pb2 as spotify_dot_login5_dot_v3_dot_user__info__pb2
|
||||
from spotify.login5.v3.challenges import \
|
||||
code_pb2 as spotify_dot_login5_dot_v3_dot_challenges_dot_code__pb2
|
||||
from spotify.login5.v3.challenges import \
|
||||
hashcash_pb2 as spotify_dot_login5_dot_v3_dot_challenges_dot_hashcash__pb2
|
||||
from spotify.login5.v3.credentials import \
|
||||
credentials_pb2 as \
|
||||
from librespot.proto.spotify.login5.v3 import \
|
||||
ClientInfo_pb2 as spotify_dot_login5_dot_v3_dot_client__info__pb2
|
||||
from librespot.proto.spotify.login5.v3 import \
|
||||
UserInfo_pb2 as spotify_dot_login5_dot_v3_dot_user__info__pb2
|
||||
from librespot.proto.spotify.login5.v3.challenges import \
|
||||
Code_pb2 as spotify_dot_login5_dot_v3_dot_challenges_dot_code__pb2
|
||||
from librespot.proto.spotify.login5.v3.challenges import \
|
||||
Hashcash_pb2 as spotify_dot_login5_dot_v3_dot_challenges_dot_hashcash__pb2
|
||||
from librespot.proto.spotify.login5.v3.credentials import \
|
||||
Credentials_pb2 as \
|
||||
spotify_dot_login5_dot_v3_dot_credentials_dot_credentials__pb2
|
||||
from spotify.login5.v3.identifiers import \
|
||||
identifiers_pb2 as \
|
||||
from librespot.proto.spotify.login5.v3.identifiers import \
|
||||
Identifiers as \
|
||||
spotify_dot_login5_dot_v3_dot_identifiers_dot_identifiers__pb2
|
||||
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
Reference in New Issue
Block a user