Пример #1
0
    def check_success(cls, response, msg):
        if response.status_code != 200:
            if msg == {'Error': 'BadAuthentication'}:
                raise CallFailure('Invalid login credentials', cls.__name__)

            log.warning(
                "Received strange login response: %r"
                "\n\nYou may need to enable less secure logins:\n"
                "  https://www.google.com/settings/security/lesssecureapps",
                msg)

            raise CallFailure(
                "status code %s != 200, full response: %r " %
                (response.status_code, msg), cls.__name__)
    def check_success(cls, response, msg):
        if msg.status != download_pb2.GetTracksToExportResponse.OK:
            enum_desc = download_pb2._GETTRACKSTOEXPORTRESPONSE.enum_types[0]
            res_name = enum_desc.values_by_number[msg.status].name

            raise CallFailure(
                "Track export (list) error code %s: %s." %
                (msg.status, res_name), cls.__name__)
    def perform(cls, session, *args, **kwargs):
        """Send, parse, validate and check success of this call.
        *args and **kwargs are passed to protocol.build_transaction.

        :param session: a PlaySession used to send this request.
        """
        #TODO link up these docs

        call_name = cls.__name__

        if cls.gets_logged:
            log.debug(
                "%s(args=%s, kwargs=%s)", call_name,
                [utils.truncate(a) for a in args],
                dict((k, utils.truncate(v)) for (k, v) in kwargs.items()))
        else:
            log.debug("%s(<does not get logged>)", call_name)

        req_kwargs = cls.build_request(*args, **kwargs)

        response = session.send(req_kwargs, cls.required_auth)

        #TODO check return code

        try:
            msg = cls.parse_response(response)
        except ParseException:
            if cls.gets_logged:
                log.exception("couldn't parse %s response: %r", call_name,
                              response.content)
            raise CallFailure(
                "the server's response could not be understood."
                " The call may still have succeeded, but it's unlikely.",
                call_name)

        if cls.gets_logged:
            log.debug(cls.filter_response(msg))

        try:
            #order is important; validate only has a schema for a successful response
            cls.check_success(response, msg)
            cls.validate(response, msg)
        except CallFailure:
            raise
        except ValidationException:
            #TODO trim the response if it's huge
            if cls.gets_logged:
                msg_fmt = (
                    "the following response format for %s was not recognized."
                    "\nIf there has not been a compatibility update reported"
                    "[here](http://goo.gl/jTKNb),"
                    " please [create an issue](http://goo.gl/qbAW8) that includes"
                    " the following raw response:\n%r\n"
                    "\nA traceback follows:\n")
                log.exception(msg_fmt, call_name, msg)

        return msg
    def check_success(cls, response, msg):
        if msg.HasField('auth_status'
                        ) and msg.auth_status != upload_pb2.UploadResponse.OK:
            enum_desc = upload_pb2._UPLOADRESPONSE.enum_types[1]
            res_name = enum_desc.values_by_number[msg.auth_status].name

            raise CallFailure(
                "Upload auth error code %s: %s."
                " See http://goo.gl/O6xe7 for more information. " %
                (msg.auth_status, res_name), cls.__name__)
Пример #5
0
 def check_success(cls, response, msg):
     if ('error' in msg or
         not all([d.get('response_code', None) in ('OK', 'CONFLICT')
                  for d in msg.get('mutate_response', [])])):
         raise CallFailure('The server reported failure while'
                           ' changing the requested resource.'
                           " If this wasn't caused by invalid arguments"
                           ' or server flakiness,'
                           ' please open an issue.',
                           cls.__name__)
Пример #6
0
    def check_success(cls, res):
        #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 res and not res['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__)
Пример #7
0
    def get_stream_url(self, id):
        try:
            stream_url = self._mobileclient.get_stream_url(id, self._device)
        except NotLoggedIn:
            if self.authenticate():
                stream_url = self._mobileclient.get_stream_url(id, self._device)
            else:
                return ''
        except CallFailure:
            raise CallFailure('Could not play song with id: ' + id, 'get_stream_url')

        return stream_url
Пример #8
0
    def _make_call(self, protocol, *args, **kwargs):
        """Returns the response of a protocol.Call.
        Additional kw/args are passed to protocol.build_transaction."""
        #TODO link up these docs

        call_name = protocol.__name__

        self.log.debug("%s(args=%s, kwargs=%s)", call_name,
                       [utils.truncate(a) for a in args],
                       {k: utils.truncate(v)
                        for (k, v) in kwargs.items()})

        request = protocol.build_request(*args, **kwargs)

        response = self.session.send(request, protocol.get_auth(),
                                     protocol.session_options)

        #TODO check return code

        try:
            msg = protocol.parse_response(response)
        except ParseException:
            self.log.exception("couldn't parse %s response: %r", call_name,
                               response.content)
            raise CallFailure(
                "the server's response could not be understood."
                " The call may still have succeeded, but it's unlikely.",
                call_name)

        self.log.debug(protocol.filter_response(msg))

        try:
            #order is important; validate only has a schema for a successful response
            protocol.check_success(msg)
            protocol.validate(msg)
        except CallFailure:
            raise
        except ValidationException:
            #TODO link to some protocol for reporting this
            self.log.exception(
                "please report the following unknown response format for %s: %r",
                call_name, msg)

        return msg
 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__)
Пример #10
0
    def perform(cls, session, validate, *args, **kwargs):
        """Send, parse, validate and check success of this call.
        *args and **kwargs are passed to protocol.build_transaction.

        :param session: a PlaySession used to send this request.
        :param validate: if False, do not validate
        """
        # TODO link up these docs

        call_name = cls.__name__

        if cls.gets_logged:
            log.debug("%s(args=%s, kwargs=%s)",
                      call_name,
                      [utils.truncate(a) for a in args],
                      dict((k, utils.truncate(v)) for (k, v) in kwargs.items())
                      )
        else:
            log.debug("%s(<omitted>)", call_name)

        req_kwargs = cls.build_request(*args, **kwargs)

        response = session.send(req_kwargs, cls.required_auth)
        # TODO trim the logged response if it's huge?

        safe_req_kwargs = req_kwargs.copy()
        if safe_req_kwargs.get('headers', {}).get('Authorization', None) is not None:
            safe_req_kwargs['headers']['Authorization'] = '<omitted>'

        if cls.fail_on_non_200:
            try:
                response.raise_for_status()
            except requests.HTTPError as e:
                err_msg = str(e)

                if cls.gets_logged:
                    err_msg += "\n(requests kwargs: %r)" % (safe_req_kwargs)
                    err_msg += "\n(response was: %r)" % response.text

                raise CallFailure(err_msg, call_name)

        try:
            parsed_response = cls.parse_response(response)
        except ParseException:
            err_msg = ("the server's response could not be understood."
                       " The call may still have succeeded, but it's unlikely.")
            if cls.gets_logged:
                err_msg += "\n(requests kwargs: %r)" % (safe_req_kwargs)
                err_msg += "\n(response was: %r)" % response.text
                log.exception("could not parse %s response: %r", call_name, response.text)
            else:
                log.exception("could not parse %s response: (omitted)", call_name)

            raise CallFailure(err_msg, call_name)

        if cls.gets_logged:
            log.debug(cls.filter_response(parsed_response))

        try:
            # order is important; validate only has a schema for a successful response
            cls.check_success(response, parsed_response)
            if validate:
                cls.validate(response, parsed_response)
        except CallFailure as e:
            if not cls.gets_logged:
                raise

            # otherwise, reraise a new exception with our req/res context
            err_msg = ("{e_message}\n"
                       "(requests kwargs: {req_kwargs!r})\n"
                       "(response was: {content!r})").format(
                           e_message=str(e),
                           req_kwargs=safe_req_kwargs,
                           content=response.text)
            raise_from(CallFailure(err_msg, e.callname), e)

        except ValidationException as e:
            # TODO shouldn't be using formatting
            err_msg = "the response format for %s was not recognized." % call_name
            err_msg += "\n\n%s\n" % e

            if cls.gets_logged:
                raw_response = response.text

                if len(raw_response) > 10000:
                    raw_response = raw_response[:10000] + '...'

                err_msg += ("\nFirst, try the develop branch."
                            " If you can recreate this error with the most recent code"
                            " please [create an issue](http://goo.gl/qbAW8) that includes"
                            " the above ValidationException"
                            " and the following request/response:\n%r\n\n%r\n"
                            "\nA traceback follows:\n") % (safe_req_kwargs, raw_response)

            log.exception(err_msg)

        return parsed_response
 def check_succes(cls, response, msg):
     if response.status_code == 200:
         raise CallFailure("status code %s != 200" % response.status_code, cls.__name__)
 def check_success(cls, response, msg):
     if msg.HasField('getjobs_response'
                     ) and not msg.getjobs_response.get_tracks_success:
         raise CallFailure('get_tracks_success == False', cls.__name__)
Пример #13
0
 def check_success(cls, res):
     if res.HasField('auth_status'
                     ) and res.auth_status != upload_pb2.UploadResponse.OK:
         raise CallFailure(
             "Too many uploader ids/machines are registered."
             " See http://goo.gl/O6xe7 for more information.", cls.__name__)
Пример #14
0
    def upload(self, filepaths, transcode_quality=3, enable_matching=False):
        """Uploads the given filepaths.
        Any non-mp3 files will be `transcoded with avconv
        <https://github.com/simon-weber/Unofficial-Google-Music-API/
        blob/develop/gmusicapi/utils/utils.py#L18>`__ before being uploaded.

        Return a 3-tuple ``(uploaded, matched, not_uploaded)`` of dictionaries, eg::

            (
                {'<filepath>': '<new server id>'},               # uploaded
                {'<filepath>': '<new server id>'},               # matched
                {'<filepath>': '<reason, eg ALREADY_UPLOADED>'}  # not uploaded
            )

        :param filepaths: a list of filepaths, or a single filepath.
        :param transcode_quality: if int, pass to avconv ``-qscale`` for libmp3lame
          (lower-better int, roughly corresponding to `hydrogenaudio -vX settings
          <http://wiki.hydrogenaudio.org/index.php?title=LAME#Recommended_encoder_settings>`__).
          If string, pass to avconv ``-ab`` (eg ``'128k'`` for an average bitrate of 128k). The
          default is ~175kbs vbr.

        :param enable_matching: if ``True``, attempt to use `scan and match
          <http://support.google.com/googleplay/bin/answer.py?hl=en&answer=2920799&topic=2450455>`__
          to avoid uploading every song.
          **WARNING**: currently, mismatched songs can *not* be fixed with the 'Fix Incorrect Match'
          button or :func:`report_incorrect_match`. They would have to be deleted and reuploaded
          with the Music Manager.
          Fixing matches from gmusicapi will be supported in a future release; see issue `#89
          <https://github.com/simon-weber/Unofficial-Google-Music-API/issues/89>`__.

        All Google-supported filetypes are supported; see `Google's documentation
        <http://support.google.com/googleplay/bin/answer.py?hl=en&answer=1100462>`__.

        Unlike Google's Music Manager, this function will currently allow the same song to
        be uploaded more than once if its tags are changed. This is subject to change in the future.

        If ``PERMANENT_ERROR`` is given as a not_uploaded reason, attempts to reupload will never
        succeed. The file will need to be changed before the server will reconsider it; the easiest
        way is to change metadata tags (it's not important that the tag be uploaded, just that the
        contents of the file change somehow).
        """

        if self.uploader_id is None or self.uploader_name is None:
            raise NotLoggedIn("Not authenticated as an upload device;"
                              " run Api.login(...perform_upload_auth=True...)"
                              " first.")

        #TODO there is way too much code in this function.

        #To return.
        uploaded = {}
        matched = {}
        not_uploaded = {}

        #Gather local information on the files.
        local_info = {}  # {clientid: (path, Track)}
        for path in filepaths:
            try:
                track = musicmanager.UploadMetadata.fill_track_info(path)
            except BaseException as e:
                log.exception("problem gathering local info of '%r'", path)

                user_err_msg = str(e)

                if 'Non-ASCII strings must be converted to unicode' in str(e):
                    #This is a protobuf-specific error; they require either ascii or unicode.
                    #To keep behavior consistent, make no effort to guess - require users
                    # to decode first.
                    user_err_msg = (
                        "nonascii bytestrings must be decoded to unicode"
                        " (error: '%s')" % err_msg)

                not_uploaded[path] = user_err_msg
            else:
                local_info[track.client_id] = (path, track)

        if not local_info:
            return uploaded, matched, not_uploaded

        #TODO allow metadata faking

        #Upload metadata; the server tells us what to do next.
        res = self._make_call(musicmanager.UploadMetadata,
                              [track for (path, track) in local_info.values()],
                              self.uploader_id)

        #TODO checking for proper contents should be handled in verification
        md_res = res.metadata_response

        responses = [r for r in md_res.track_sample_response]
        sample_requests = [req for req in md_res.signed_challenge_info]

        #Send scan and match samples if requested.
        for sample_request in sample_requests:
            path, track = local_info[
                sample_request.challenge_info.client_track_id]

            try:
                res = self._make_call(musicmanager.ProvideSample, path,
                                      sample_request, track, self.uploader_id)
            except (IOError, ValueError) as e:
                log.warning(
                    "couldn't create scan and match sample for '%s': %s", path,
                    str(e))
                not_uploaded[path] = str(e)
            else:
                responses.extend(res.sample_response.track_sample_response)

        #Read sample responses and prep upload requests.
        to_upload = {}  # {serverid: (path, Track, do_not_rematch?)}
        for sample_res in responses:
            path, track = local_info[sample_res.client_track_id]

            if sample_res.response_code == upload_pb2.TrackSampleResponse.MATCHED:
                log.info("matched '%s' to sid %s", path,
                         sample_res.server_track_id)

                if enable_matching:
                    matched[path] = sample_res.server_track_id
                else:
                    # request a reupload session (ie, hit 'fix incorrect match').
                    time.sleep(10)  # wait for upload servers to sync
                    try:
                        self._make_call(webclient.ReportBadSongMatch,
                                        [sample_res.server_track_id])

                        #Wait for server to register our request.
                        retries = 0
                        while retries < 5:
                            jobs = self._make_call(musicmanager.GetUploadJobs,
                                                   self.uploader_id)
                            matching = [
                                job for job in
                                jobs.getjobs_response.tracks_to_upload
                                if (job.client_id == sample_res.client_track_id
                                    and job.status ==
                                    upload_pb2.TracksToUpload.FORCE_REUPLOAD)
                            ]
                            if matching:
                                reup_sid = matching[0].server_id
                                break

                            log.debug("wait for reup job (%s)", retries)
                            time.sleep(2)
                            retries += 1
                        else:
                            raise CallFailure(
                                "could not get reupload/rematch job for '%s'" %
                                path, 'GetUploadJobs')

                    except CallFailure as e:
                        log.exception(
                            "'%s' was matched without matching enabled", path)
                        matched[path] = sample_res.server_track_id
                    else:
                        log.info("will reupload '%s'", path)

                        to_upload[reup_sid] = (path, track, True)

            elif sample_res.response_code == upload_pb2.TrackSampleResponse.UPLOAD_REQUESTED:
                to_upload[sample_res.server_track_id] = (path, track, False)
            else:
                #Report the symbolic name of the response code enum.
                enum_desc = upload_pb2._TRACKSAMPLERESPONSE.enum_types[0]
                res_name = enum_desc.values_by_number[
                    sample_res.response_code].name

                err_msg = "TrackSampleResponse code %s: %s" % (
                    sample_res.response_code, res_name)

                log.warning("upload of '%s' rejected: %s", path, err_msg)
                not_uploaded[path] = err_msg

        #Send upload requests.
        if to_upload:
            #TODO reordering requests could avoid wasting time waiting for reup sync
            self._make_call(musicmanager.UpdateUploadState, 'start',
                            self.uploader_id)

            for server_id, (path, track, do_not_rematch) in to_upload.items():
                #It can take a few tries to get an session.
                should_retry = True
                attempts = 0

                while should_retry and attempts < 10:
                    session = self._make_call(musicmanager.GetUploadSession,
                                              self.uploader_id, len(uploaded),
                                              track, path, server_id,
                                              do_not_rematch)
                    attempts += 1

                    got_session, error_details = \
                        musicmanager.GetUploadSession.process_session(session)

                    if got_session:
                        log.info("got an upload session for '%s'", path)
                        break

                    should_retry, reason, error_code = error_details
                    log.debug(
                        "problem getting upload session: %s\ncode=%s retrying=%s",
                        reason, error_code, should_retry)

                    if error_code == 200 and do_not_rematch:
                        #reupload requests need to wait on a server sync
                        #200 == already uploaded, so force a retry in this case
                        should_retry = True

                    time.sleep(6)  # wait before retrying
                else:
                    err_msg = "GetUploadSession error %s: %s" % (error_code,
                                                                 reason)

                    log.warning("giving up on upload session for '%s': %s",
                                path, err_msg)
                    not_uploaded[path] = err_msg

                    continue  # to next upload

                #got a session, do the upload
                #this terribly inconsistent naming isn't my fault: Google--
                session = session['sessionStatus']
                external = session['externalFieldTransfers'][0]

                session_url = external['putInfo']['url']
                content_type = external.get('content_type', 'audio/mpeg')

                if track.original_content_type != locker_pb2.Track.MP3:
                    try:
                        log.info("transcoding '%s' to mp3", path)
                        contents = utils.transcode_to_mp3(
                            path, quality=transcode_quality)
                    except (IOError, ValueError) as e:
                        log.warning("error transcoding %s: %s", path, e)
                        not_uploaded[path] = "transcoding error: %s" % e
                        continue
                else:
                    with open(path, 'rb') as f:
                        contents = f.read()

                upload_response = self._make_call(musicmanager.UploadFile,
                                                  session_url, content_type,
                                                  contents)

                success = upload_response.get('sessionStatus', {}).get('state')
                if success:
                    uploaded[path] = server_id
                else:
                    #404 == already uploaded? serverside check on clientid?
                    log.debug(
                        "could not finalize upload of '%s'. response: %s",
                        path, upload_response)
                    not_uploaded[
                        path] = 'could not finalize upload; details in log'

            self._make_call(musicmanager.UpdateUploadState, 'stopped',
                            self.uploader_id)

        return uploaded, matched, not_uploaded