class MmCall(Call): """Abstract base for Music Manager calls.""" static_method = 'POST' # remember that setting this in a subclass overrides, not merges # static + dynamic does merge, though static_headers = { 'User-agent': 'Music Manager (1, 0, 55, 7425 HTTPS - Windows)' } required_auth = authtypes(oauth=True) # this is a shared union class that has all specific upload types # nearly all of the proto calls return a message of this form res_msg_type = upload_pb2.UploadResponse @classmethod def parse_response(cls, response): """Parse the cls.res_msg_type proto msg.""" res_msg = cls.res_msg_type() try: res_msg.ParseFromString(response.content) except DecodeError as e: raise ParseException(str(e)) from e return res_msg @classmethod def filter_response(cls, msg): return Call._filter_proto(msg)
class WcCall(Call): """Abstract base for web client calls.""" required_auth = authtypes(xt=True, sso=True) #validictory schema for the response _res_schema = utils.NotImplementedField @classmethod def validate(cls, response, msg): """Use validictory and a static schema (stored in cls._res_schema).""" try: return validictory.validate(msg, cls._res_schema) except ValueError as e: trace = sys.exc_info()[2] raise ValidationException(str(e)), None, trace @classmethod def check_success(cls, response, msg): #Failed responses always have a success=False key. #Some successful responses do not have a success=True key, however. #TODO remove utils.call_succeeded if 'success' in msg and not msg['success']: raise CallFailure( "the server reported failure. This is usually" " caused by bad arguments, but can also happen if requests" " are made too quickly (eg creating a playlist then" " modifying it before the server has created it)", cls.__name__) @classmethod def parse_response(cls, response): return cls._parse_json(response.text)
class Init(Call): """Called one time per session, immediately after login. This performs one-time setup: it gathers the cookies we need (specifically `xt`), and Google uses it to create the webclient DOM. Note the use of the HEAD verb. Google uses GET, but we don't need the large response containing Google's webui. """ static_method = 'HEAD' static_url = base_url + 'listen' required_auth = authtypes(sso=True) #This call doesn't actually request/return anything useful aside from cookies. @staticmethod def parse_response(response): return response.text @classmethod def check_success(cls, response, msg): if response.status_code != 200: raise CallFailure(('status code %s != 200' % response.status_code), cls.__name__) if 'xt' not in response.cookies: raise CallFailure('did not receieve xt cookies', cls.__name__)
class GetStreamUrl(WcCall): """Used to request a streaming link of a track.""" static_method = 'GET' static_url = base_url + 'play' # note use of base_url, not service_url required_auth = authtypes(sso=True) # no xt required _res_schema = { "type": "object", "properties": { "url": { "type": "string" } }, "additionalProperties": False } @staticmethod def dynamic_params(song_id): return { 'u': 0, # select first user of logged in; probably shouldn't be hardcoded 'pt': 'e', # unknown 'songid': song_id, }
class McCall(Call): """Abstract base for mobile client calls.""" required_auth = authtypes(xt=False, sso=True) #validictory schema for the response _res_schema = utils.NotImplementedField @classmethod def validate(cls, response, msg): """Use validictory and a static schema (stored in cls._res_schema).""" try: return validictory.validate(msg, cls._res_schema) except ValueError as e: trace = sys.exc_info()[2] raise ValidationException(str(e)), None, trace @classmethod def check_success(cls, response, msg): #TODO not sure if this is still valid for mc pass #if 'success' in msg and not msg['success']: # raise CallFailure( # "the server reported failure. This is usually" # " caused by bad arguments, but can also happen if requests" # " are made too quickly (eg creating a playlist then" # " modifying it before the server has created it)", # cls.__name__) @classmethod def parse_response(cls, response): return cls._parse_json(response.text)
class GetStreamUrl(WcCall): """Used to request a streaming link of a track.""" static_method = 'GET' static_url = base_url + 'play' # note use of base_url, not service_url required_auth = authtypes(sso=True) # no xt required _res_schema = { "type": "object", "properties": { "url": { "type": "string", "required": False }, "urls": { "type": "array", "required": False }, 'now': { 'type': 'integer', 'required': False }, 'tier': { 'type': 'integer', 'required': False }, }, "additionalProperties": False } @staticmethod def dynamic_params(song_id): # https://github.com/simon-weber/Unofficial-Google-Music-API/issues/137 # there are three cases when streaming: # | track type | guid songid? | slt/sig needed? | # user-uploaded yes no # AA track in library yes yes # AA track not in library no yes # without the track['type'] field we can't tell between 1 and 2, but # include slt/sig anyway; the server ignores the extra params. key = '27f7313e-f75d-445a-ac99-56386a5fe879' salt = ''.join( random.choice(string.ascii_lowercase + string.digits) for x in range(12)) sig = base64.urlsafe_b64encode( hmac.new(key, (song_id + salt), sha1).digest())[:-1] params = {'u': 0, 'pt': 'e', 'slt': salt, 'sig': sig} # TODO match guid instead, should be more robust if song_id[0] == 'T': # all access params['mjck'] = song_id else: params['songid'] = song_id return params
def send_without_auth(): for s in create_sessions(): s.is_authenticated = True mock_session = Mock() mock_req_kwargs = {'fake': 'kwargs'} s.send(mock_req_kwargs, authtypes(), mock_session) # sending without auth should not use the normal session, # since that might have auth cookies automatically attached assert_false(s._rsession.called) mock_session.request.called_once_with(**mock_req_kwargs) mock_session.closed.called_once_with()
def send_without_auth(): for s in create_sessions(): s.is_authenticated = True mock_session = MagicMock() mock_req_kwargs = {"fake": "kwargs"} s.send(mock_req_kwargs, authtypes(), mock_session) # sending without auth should not use the normal session, # since that might have auth cookies automatically attached assert_false(s._rsession.called) mock_session.request.called_once_with(**mock_req_kwargs) mock_session.closed.called_once_with()
class Init(Call): """Called after login and once before any other webclient call. This gathers the cookies we need (specifically xt); it's the call that creates the webclient DOM.""" static_method = 'HEAD' static_url = base_url + 'listen' required_auth = authtypes(sso=True) #This call doesn't actually request/return anything useful aside from cookies. @staticmethod def parse_response(response): return response.text @classmethod def check_success(cls, response, msg): if response.status_code != 200: raise CallFailure(('status code %s != 200' % response.status_code), cls.__name__) if 'xt' not in response.cookies: raise CallFailure('did not receieve xt cookies', cls.__name__)
def request_invalid_site(self, client): req_kwargs = {'url': self.test_url, 'method': 'HEAD'} no_auth = authtypes() client.session.send(req_kwargs, no_auth)
def authtypes_factory_args(): auth = authtypes(oauth=True) assert_true(auth.oauth) assert_false(auth.sso) assert_false(auth.xt)
def authtypes_factory_defaults(): auth = authtypes() assert_false(auth.oauth) assert_false(auth.sso) assert_false(auth.xt)