Beispiel #1
0
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)
Beispiel #2
0
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)
Beispiel #3
0
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__)
Beispiel #4
0
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
Beispiel #7
0
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()
Beispiel #8
0
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()
Beispiel #9
0
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__)
Beispiel #10
0
    def request_invalid_site(self, client):
        req_kwargs = {'url': self.test_url,
                      'method': 'HEAD'}
        no_auth = authtypes()

        client.session.send(req_kwargs, no_auth)
Beispiel #11
0
    def request_invalid_site(self, client):
        req_kwargs = {'url': self.test_url, 'method': 'HEAD'}
        no_auth = authtypes()

        client.session.send(req_kwargs, no_auth)
Beispiel #12
0
def authtypes_factory_args():
    auth = authtypes(oauth=True)
    assert_true(auth.oauth)
    assert_false(auth.sso)
    assert_false(auth.xt)
Beispiel #13
0
def authtypes_factory_defaults():
    auth = authtypes()
    assert_false(auth.oauth)
    assert_false(auth.sso)
    assert_false(auth.xt)
Beispiel #14
0
def authtypes_factory_args():
    auth = authtypes(oauth=True)
    assert_true(auth.oauth)
    assert_false(auth.sso)
    assert_false(auth.xt)
Beispiel #15
0
def authtypes_factory_defaults():
    auth = authtypes()
    assert_false(auth.oauth)
    assert_false(auth.sso)
    assert_false(auth.xt)