From b28e684e97d61125278f98e12d779bcc92e4875d Mon Sep 17 00:00:00 2001 From: kokarare1212 Date: Tue, 28 Jun 2022 20:55:10 +0900 Subject: [PATCH] #135 Add ClientToken support --- librespot/__init__.py | 2 +- librespot/core.py | 47 ++++++++++- librespot/proto/ClientToken_pb2.py | 63 ++++++++++++++ librespot/proto/Connectivity_pb2.py | 36 ++++++++ proto/client_token.proto | 125 ++++++++++++++++++++++++++++ proto/connectivity.proto | 51 ++++++++++++ setup.py | 2 +- 7 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 librespot/proto/ClientToken_pb2.py create mode 100644 librespot/proto/Connectivity_pb2.py create mode 100644 proto/client_token.proto create mode 100644 proto/connectivity.proto diff --git a/librespot/__init__.py b/librespot/__init__.py index b210b15..1b8a908 100644 --- a/librespot/__init__.py +++ b/librespot/__init__.py @@ -6,7 +6,7 @@ import platform class Version: - version_name = "0.0.1" + version_name = "0.0.4" @staticmethod def platform() -> Platform: diff --git a/librespot/core.py b/librespot/core.py index 9b6fcf2..7347f50 100644 --- a/librespot/core.py +++ b/librespot/core.py @@ -12,7 +12,7 @@ from librespot.cache import CacheManager from librespot.crypto import CipherPair, DiffieHellman, Packet from librespot.mercury import MercuryClient, MercuryRequests, RawMercuryRequest from librespot.metadata import AlbumId, ArtistId, EpisodeId, ShowId, TrackId -from librespot.proto import Authentication_pb2 as Authentication, Connect_pb2 as Connect, Keyexchange_pb2 as Keyexchange, Metadata_pb2 as Metadata +from librespot.proto import Authentication_pb2 as Authentication, ClientToken_pb2 as ClientToken, Connect_pb2 as Connect, Connectivity_pb2 as Connectivity, Keyexchange_pb2 as Keyexchange, Metadata_pb2 as Metadata from librespot.proto.ExplicitContentPubsub_pb2 import UserAttributesUpdate from librespot.structure import Closeable, MessageListener, RequestListener, SubListener import base64 @@ -39,6 +39,7 @@ import websocket class ApiClient(Closeable): logger = logging.getLogger("Librespot:ApiClient") __base_url: str + __client_token_str: str = None __session: Session def __init__(self, session: Session): @@ -49,6 +50,11 @@ class ApiClient(Closeable): self, method: str, suffix: str, headers: typing.Union[None, typing.Dict[str, str]], body: typing.Union[None, bytes]) -> requests.PreparedRequest: + if self.__client_token_str is None: + resp = self.__client_token() + self.__client_token_str = resp.granted_token.token + self.logger.debug("Updated client token: {}".format(self.__client_token_str)) + request = requests.PreparedRequest() request.method = method request.data = body @@ -147,6 +153,45 @@ class ApiClient(Closeable): proto.ParseFromString(body) return proto + def set_client_token(self, client_token): + self.__client_token_str = client_token + + def __client_token(self): + proto_req = ClientToken.ClientTokenRequest( + request_type=ClientToken.ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST, + client_data=ClientToken.ClientDataRequest( + client_id=MercuryRequests.keymaster_client_id, + client_version=Version.version_name, + connectivity_sdk_data=Connectivity.ConnectivitySdkData( + device_id=self.__session.device_id(), + platform_specific_data=Connectivity.PlatformSpecificData( + windows=Connectivity.NativeWindowsData( + something1=10, + something3=21370, + something4=2, + something6=9, + something7=332, + something8=33404, + something10=True, + ), + ), + ), + ), + ) + + resp = requests.post("https://clienttoken.spotify.com/v1/clienttoken", + proto_req.SerializeToString(), + headers={ + "Accept": "application/x-protobuf", + "Content-Encoding": "", + }) + + ApiClient.StatusCodeException.check_status(resp) + + proto_resp = ClientToken.ClientTokenResponse() + proto_resp.ParseFromString(resp.content) + return proto_resp + class StatusCodeException(IOError): code: int diff --git a/librespot/proto/ClientToken_pb2.py b/librespot/proto/ClientToken_pb2.py new file mode 100644 index 0000000..2b7e954 --- /dev/null +++ b/librespot/proto/ClientToken_pb2.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: client_token.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from librespot.proto import Connectivity_pb2 as connectivity__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12\x63lient_token.proto\x12\x1bspotify.clienttoken.http.v0\x1a\x12\x63onnectivity.proto\"\x84\x02\n\x12\x43lientTokenRequest\x12I\n\x0crequest_type\x18\x01 \x01(\x0e\x32\x33.spotify.clienttoken.http.v0.ClientTokenRequestType\x12\x45\n\x0b\x63lient_data\x18\x02 \x01(\x0b\x32..spotify.clienttoken.http.v0.ClientDataRequestH\x00\x12Q\n\x11\x63hallenge_answers\x18\x03 \x01(\x0b\x32\x34.spotify.clienttoken.http.v0.ChallengeAnswersRequestH\x00\x42\t\n\x07request\"\x99\x01\n\x11\x43lientDataRequest\x12\x16\n\x0e\x63lient_version\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12Q\n\x15\x63onnectivity_sdk_data\x18\x03 \x01(\x0b\x32\x30.spotify.clienttoken.data.v0.ConnectivitySdkDataH\x00\x42\x06\n\x04\x64\x61ta\"g\n\x17\x43hallengeAnswersRequest\x12\r\n\x05state\x18\x01 \x01(\t\x12=\n\x07\x61nswers\x18\x02 \x03(\x0b\x32,.spotify.clienttoken.http.v0.ChallengeAnswer\"\x81\x02\n\x13\x43lientTokenResponse\x12K\n\rresponse_type\x18\x01 \x01(\x0e\x32\x34.spotify.clienttoken.http.v0.ClientTokenResponseType\x12J\n\rgranted_token\x18\x02 \x01(\x0b\x32\x31.spotify.clienttoken.http.v0.GrantedTokenResponseH\x00\x12\x45\n\nchallenges\x18\x03 \x01(\x0b\x32/.spotify.clienttoken.http.v0.ChallengesResponseH\x00\x42\n\n\x08response\"\x1d\n\x0bTokenDomain\x12\x0e\n\x06\x64omain\x18\x01 \x01(\t\"\x9e\x01\n\x14GrantedTokenResponse\x12\r\n\x05token\x18\x01 \x01(\t\x12\x1d\n\x15\x65xpires_after_seconds\x18\x02 \x01(\x05\x12\x1d\n\x15refresh_after_seconds\x18\x03 \x01(\x05\x12\x39\n\x07\x64omains\x18\x04 \x03(\x0b\x32(.spotify.clienttoken.http.v0.TokenDomain\"_\n\x12\x43hallengesResponse\x12\r\n\x05state\x18\x01 \x01(\t\x12:\n\nchallenges\x18\x02 \x03(\x0b\x32&.spotify.clienttoken.http.v0.Challenge\"&\n\x16\x43lientSecretParameters\x12\x0c\n\x04salt\x18\x01 \x01(\t\"7\n\x14\x45valuateJSParameters\x12\x0c\n\x04\x63ode\x18\x01 \x01(\t\x12\x11\n\tlibraries\x18\x02 \x03(\t\"4\n\x12HashCashParameters\x12\x0e\n\x06length\x18\x01 \x01(\x05\x12\x0e\n\x06prefix\x18\x02 \x01(\t\"\xda\x02\n\tChallenge\x12\x38\n\x04type\x18\x01 \x01(\x0e\x32*.spotify.clienttoken.http.v0.ChallengeType\x12W\n\x18\x63lient_secret_parameters\x18\x02 \x01(\x0b\x32\x33.spotify.clienttoken.http.v0.ClientSecretParametersH\x00\x12S\n\x16\x65valuate_js_parameters\x18\x03 \x01(\x0b\x32\x31.spotify.clienttoken.http.v0.EvaluateJSParametersH\x00\x12W\n\x1c\x65valuate_hashcash_parameters\x18\x04 \x01(\x0b\x32/.spotify.clienttoken.http.v0.HashCashParametersH\x00\x42\x0c\n\nparameters\"&\n\x16\x43lientSecretHMACAnswer\x12\x0c\n\x04hmac\x18\x01 \x01(\t\"\"\n\x10\x45valuateJSAnswer\x12\x0e\n\x06result\x18\x01 \x01(\t\" \n\x0eHashCashAnswer\x12\x0e\n\x06suffix\x18\x01 \x01(\t\"\xb4\x02\n\x0f\x43hallengeAnswer\x12\x41\n\rChallengeType\x18\x01 \x01(\x0e\x32*.spotify.clienttoken.http.v0.ChallengeType\x12L\n\rclient_secret\x18\x02 \x01(\x0b\x32\x33.spotify.clienttoken.http.v0.ClientSecretHMACAnswerH\x00\x12\x44\n\x0b\x65valuate_js\x18\x03 \x01(\x0b\x32-.spotify.clienttoken.http.v0.EvaluateJSAnswerH\x00\x12@\n\thash_cash\x18\x04 \x01(\x0b\x32+.spotify.clienttoken.http.v0.HashCashAnswerH\x00\x42\x08\n\x06\x61nswer\"(\n\x15\x43lientTokenBadRequest\x12\x0f\n\x07message\x18\x01 \x01(\t*u\n\x16\x43lientTokenRequestType\x12\x13\n\x0fREQUEST_UNKNOWN\x10\x00\x12\x1f\n\x1bREQUEST_CLIENT_DATA_REQUEST\x10\x01\x12%\n!REQUEST_CHALLENGE_ANSWERS_REQUEST\x10\x02*v\n\x17\x43lientTokenResponseType\x12\x14\n\x10RESPONSE_UNKNOWN\x10\x00\x12#\n\x1fRESPONSE_GRANTED_TOKEN_RESPONSE\x10\x01\x12 \n\x1cRESPONSE_CHALLENGES_RESPONSE\x10\x02*|\n\rChallengeType\x12\x15\n\x11\x43HALLENGE_UNKNOWN\x10\x00\x12 \n\x1c\x43HALLENGE_CLIENT_SECRET_HMAC\x10\x01\x12\x19\n\x15\x43HALLENGE_EVALUATE_JS\x10\x02\x12\x17\n\x13\x43HALLENGE_HASH_CASH\x10\x03\x42#\n\x1f\x63om.spotify.clienttoken.http.v0H\x02\x62\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'client_token_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\037com.spotify.clienttoken.http.v0H\002' + _CLIENTTOKENREQUESTTYPE._serialized_start=2107 + _CLIENTTOKENREQUESTTYPE._serialized_end=2224 + _CLIENTTOKENRESPONSETYPE._serialized_start=2226 + _CLIENTTOKENRESPONSETYPE._serialized_end=2344 + _CHALLENGETYPE._serialized_start=2346 + _CHALLENGETYPE._serialized_end=2470 + _CLIENTTOKENREQUEST._serialized_start=72 + _CLIENTTOKENREQUEST._serialized_end=332 + _CLIENTDATAREQUEST._serialized_start=335 + _CLIENTDATAREQUEST._serialized_end=488 + _CHALLENGEANSWERSREQUEST._serialized_start=490 + _CHALLENGEANSWERSREQUEST._serialized_end=593 + _CLIENTTOKENRESPONSE._serialized_start=596 + _CLIENTTOKENRESPONSE._serialized_end=853 + _TOKENDOMAIN._serialized_start=855 + _TOKENDOMAIN._serialized_end=884 + _GRANTEDTOKENRESPONSE._serialized_start=887 + _GRANTEDTOKENRESPONSE._serialized_end=1045 + _CHALLENGESRESPONSE._serialized_start=1047 + _CHALLENGESRESPONSE._serialized_end=1142 + _CLIENTSECRETPARAMETERS._serialized_start=1144 + _CLIENTSECRETPARAMETERS._serialized_end=1182 + _EVALUATEJSPARAMETERS._serialized_start=1184 + _EVALUATEJSPARAMETERS._serialized_end=1239 + _HASHCASHPARAMETERS._serialized_start=1241 + _HASHCASHPARAMETERS._serialized_end=1293 + _CHALLENGE._serialized_start=1296 + _CHALLENGE._serialized_end=1642 + _CLIENTSECRETHMACANSWER._serialized_start=1644 + _CLIENTSECRETHMACANSWER._serialized_end=1682 + _EVALUATEJSANSWER._serialized_start=1684 + _EVALUATEJSANSWER._serialized_end=1718 + _HASHCASHANSWER._serialized_start=1720 + _HASHCASHANSWER._serialized_end=1752 + _CHALLENGEANSWER._serialized_start=1755 + _CHALLENGEANSWER._serialized_end=2063 + _CLIENTTOKENBADREQUEST._serialized_start=2065 + _CLIENTTOKENBADREQUEST._serialized_end=2105 +# @@protoc_insertion_point(module_scope) diff --git a/librespot/proto/Connectivity_pb2.py b/librespot/proto/Connectivity_pb2.py new file mode 100644 index 0000000..bf17a91 --- /dev/null +++ b/librespot/proto/Connectivity_pb2.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: connectivity.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12\x63onnectivity.proto\x12\x1bspotify.clienttoken.data.v0\"{\n\x13\x43onnectivitySdkData\x12Q\n\x16platform_specific_data\x18\x01 \x01(\x0b\x32\x31.spotify.clienttoken.data.v0.PlatformSpecificData\x12\x11\n\tdevice_id\x18\x02 \x01(\t\"\xdf\x01\n\x14PlatformSpecificData\x12\x41\n\x07\x61ndroid\x18\x01 \x01(\x0b\x32..spotify.clienttoken.data.v0.NativeAndroidDataH\x00\x12\x39\n\x03ios\x18\x02 \x01(\x0b\x32*.spotify.clienttoken.data.v0.NativeIOSDataH\x00\x12\x41\n\x07windows\x18\x04 \x01(\x0b\x32..spotify.clienttoken.data.v0.NativeWindowsDataH\x00\x42\x06\n\x04\x64\x61ta\"\xb9\x01\n\x11NativeAndroidData\x12\x19\n\x11major_sdk_version\x18\x01 \x01(\x05\x12\x19\n\x11minor_sdk_version\x18\x02 \x01(\x05\x12\x19\n\x11patch_sdk_version\x18\x03 \x01(\x05\x12\x13\n\x0b\x61pi_version\x18\x04 \x01(\r\x12>\n\x11screen_dimensions\x18\x05 \x01(\x0b\x32#.spotify.clienttoken.data.v0.Screen\"\x9e\x01\n\rNativeIOSData\x12\x1c\n\x14user_interface_idiom\x18\x01 \x01(\x05\x12\x1f\n\x17target_iphone_simulator\x18\x02 \x01(\x08\x12\x12\n\nhw_machine\x18\x03 \x01(\t\x12\x16\n\x0esystem_version\x18\x04 \x01(\t\x12\"\n\x1asimulator_model_identifier\x18\x05 \x01(\t\"\xa0\x01\n\x11NativeWindowsData\x12\x12\n\nsomething1\x18\x01 \x01(\x05\x12\x12\n\nsomething3\x18\x03 \x01(\x05\x12\x12\n\nsomething4\x18\x04 \x01(\x05\x12\x12\n\nsomething6\x18\x06 \x01(\x05\x12\x12\n\nsomething7\x18\x07 \x01(\x05\x12\x12\n\nsomething8\x18\x08 \x01(\x05\x12\x13\n\x0bsomething10\x18\n \x01(\x08\"8\n\x06Screen\x12\r\n\x05width\x18\x01 \x01(\x05\x12\x0e\n\x06height\x18\x02 \x01(\x05\x12\x0f\n\x07\x64\x65nsity\x18\x03 \x01(\x05\x42#\n\x1f\x63om.spotify.clienttoken.data.v0H\x02\x62\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'connectivity_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\037com.spotify.clienttoken.data.v0H\002' + _CONNECTIVITYSDKDATA._serialized_start=51 + _CONNECTIVITYSDKDATA._serialized_end=174 + _PLATFORMSPECIFICDATA._serialized_start=177 + _PLATFORMSPECIFICDATA._serialized_end=400 + _NATIVEANDROIDDATA._serialized_start=403 + _NATIVEANDROIDDATA._serialized_end=588 + _NATIVEIOSDATA._serialized_start=591 + _NATIVEIOSDATA._serialized_end=749 + _NATIVEWINDOWSDATA._serialized_start=752 + _NATIVEWINDOWSDATA._serialized_end=912 + _SCREEN._serialized_start=914 + _SCREEN._serialized_end=970 +# @@protoc_insertion_point(module_scope) diff --git a/proto/client_token.proto b/proto/client_token.proto new file mode 100644 index 0000000..049380a --- /dev/null +++ b/proto/client_token.proto @@ -0,0 +1,125 @@ +syntax = "proto3"; + +package spotify.clienttoken.http.v0; + +import "connectivity.proto"; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.clienttoken.http.v0"; + +message ClientTokenRequest { + ClientTokenRequestType request_type = 1; + + oneof request { + ClientDataRequest client_data = 2; + ChallengeAnswersRequest challenge_answers = 3; + } +} + +message ClientDataRequest { + string client_version = 1; + string client_id = 2; + + oneof data { + data.v0.ConnectivitySdkData connectivity_sdk_data = 3; + } +} + +message ChallengeAnswersRequest { + string state = 1; + repeated ChallengeAnswer answers = 2; +} + +message ClientTokenResponse { + ClientTokenResponseType response_type = 1; + + oneof response { + GrantedTokenResponse granted_token = 2; + ChallengesResponse challenges = 3; + } +} + +message TokenDomain { + string domain = 1; +} + +message GrantedTokenResponse { + string token = 1; + int32 expires_after_seconds = 2; + int32 refresh_after_seconds = 3; + repeated TokenDomain domains = 4; +} + +message ChallengesResponse { + string state = 1; + repeated Challenge challenges = 2; +} + +message ClientSecretParameters { + string salt = 1; +} + +message EvaluateJSParameters { + string code = 1; + repeated string libraries = 2; +} + +message HashCashParameters { + int32 length = 1; + string prefix = 2; +} + +message Challenge { + ChallengeType type = 1; + + oneof parameters { + ClientSecretParameters client_secret_parameters = 2; + EvaluateJSParameters evaluate_js_parameters = 3; + HashCashParameters evaluate_hashcash_parameters = 4; + } +} + +message ClientSecretHMACAnswer { + string hmac = 1; +} + +message EvaluateJSAnswer { + string result = 1; +} + +message HashCashAnswer { + string suffix = 1; +} + +message ChallengeAnswer { + ChallengeType ChallengeType = 1; + + oneof answer { + ClientSecretHMACAnswer client_secret = 2; + EvaluateJSAnswer evaluate_js = 3; + HashCashAnswer hash_cash = 4; + } +} + +message ClientTokenBadRequest { + string message = 1; +} + +enum ClientTokenRequestType { + REQUEST_UNKNOWN = 0; + REQUEST_CLIENT_DATA_REQUEST = 1; + REQUEST_CHALLENGE_ANSWERS_REQUEST = 2; +} + +enum ClientTokenResponseType { + RESPONSE_UNKNOWN = 0; + RESPONSE_GRANTED_TOKEN_RESPONSE = 1; + RESPONSE_CHALLENGES_RESPONSE = 2; +} + +enum ChallengeType { + CHALLENGE_UNKNOWN = 0; + CHALLENGE_CLIENT_SECRET_HMAC = 1; + CHALLENGE_EVALUATE_JS = 2; + CHALLENGE_HASH_CASH = 3; +} \ No newline at end of file diff --git a/proto/connectivity.proto b/proto/connectivity.proto new file mode 100644 index 0000000..e26c2e4 --- /dev/null +++ b/proto/connectivity.proto @@ -0,0 +1,51 @@ +syntax = "proto3"; + +package spotify.clienttoken.data.v0; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.clienttoken.data.v0"; + +message ConnectivitySdkData { + PlatformSpecificData platform_specific_data = 1; + string device_id = 2; +} + +message PlatformSpecificData { + oneof data { + NativeAndroidData android = 1; + NativeIOSData ios = 2; + NativeWindowsData windows = 4; + } +} + +message NativeAndroidData { + int32 major_sdk_version = 1; + int32 minor_sdk_version = 2; + int32 patch_sdk_version = 3; + uint32 api_version = 4; + Screen screen_dimensions = 5; +} + +message NativeIOSData { + int32 user_interface_idiom = 1; + bool target_iphone_simulator = 2; + string hw_machine = 3; + string system_version = 4; + string simulator_model_identifier = 5; +} + +message NativeWindowsData { + int32 something1 = 1; + int32 something3 = 3; + int32 something4 = 4; + int32 something6 = 6; + int32 something7 = 7; + int32 something8 = 8; + bool something10 = 10; +} + +message Screen { + int32 width = 1; + int32 height = 2; + int32 density = 3; +} \ No newline at end of file diff --git a/setup.py b/setup.py index ee61cd5..090fa44 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ import setuptools setuptools.setup(name="librespot", - version="0.0.3", + version="0.0.4", description="Open Source Spotify Client", long_description=open("README.md").read(), long_description_content_type="text/markdown",