예제 #1
0
    def _wc_call(self, service_name, *args, **kw):
        """Returns the response of a web client call.
        :param service_name: the name of the call, eg ``search``
        additional positional arguments are passed to ``build_body``for the retrieved protocol.
        if a 'query_args' key is present in kw, it is assumed to be a dictionary of additional key/val pairs to append to the query string.
        """

        protocol = getattr(self.wc_protocol, service_name)

        #Always log the request.
        self.log.debug("wc_call %s %s", service_name, args)

        body, res_schema = protocol.build_transaction(*args)

        #Encode the body. It might be None (empty).
        if body is not None:  #body can be {}, which is different from None. {} is falsey.
            body = "json=" + quote_plus(json.dumps(body))

        extra_query_args = None
        if 'query_args' in kw:
            extra_query_args = kw['query_args']

        res = self.session.open_web_url(protocol.build_url, extra_query_args,
                                        body)

        read = res.read()
        res = json.loads(read)

        if protocol.gets_logged:
            self.log.debug("wc_call response %s", res)
        else:
            self.log.debug("wc_call response <suppressed>")

        #Check if the server reported success.
        success = utils.call_succeeded(res)
        if not success:
            self.log.error("call to %s failed", service_name)
            self.log.debug("full response: %s", res)

            if not self.suppress_failure:
                calling_func_name = inspect.stack()[1][3]
                raise CallFailure(
                    calling_func_name,
                    res)  #normally caused by bad arguments to the server

        #Calls are not required to have a schema, and
        # schemas are only for successful calls.
        if success and res_schema:
            try:
                validictory.validate(res, res_schema)
            except ValueError as details:
                self.log.warning(
                    "Received an unexpected response from call %s.",
                    service_name)
                self.log.debug("full response: %s", res)
                self.log.debug("failed schema: %s", res_schema)
                self.log.warning("error was: %s", details)

        return res
예제 #2
0
    def _wc_call(self, service_name, *args, **kw):
        """Returns the response of a web client call.
        :param service_name: the name of the call, eg ``search``
        additional positional arguments are passed to ``build_body``for the retrieved protocol.
        if a 'query_args' key is present in kw, it is assumed to be a dictionary of additional key/val pairs to append to the query string.
        """


        protocol = getattr(self.wc_protocol, service_name)

        #Always log the request.
        self.log.debug("wc_call %s %s", service_name, args)
        
        body, res_schema = protocol.build_transaction(*args)
        

        #Encode the body. It might be None (empty).
        if body is not None: #body can be {}, which is different from None. {} is falsey.
            body = "json=" + quote_plus(json.dumps(body))

        extra_query_args = None
        if 'query_args' in kw:
            extra_query_args = kw['query_args']

        res = self.session.open_web_url(protocol.build_url, extra_query_args, body)
        
        read = res.read()
        res = json.loads(read)

        if protocol.gets_logged:
            self.log.debug("wc_call response %s", res)
        else:
            self.log.debug("wc_call response <suppressed>")

        #Check if the server reported success.
        success = utils.call_succeeded(res)
        if not success:
            self.log.error("call to %s failed", service_name)
            self.log.debug("full response: %s", res)
            
            if not self.suppress_failure:
                calling_func_name = inspect.stack()[1][3]
                raise CallFailure(calling_func_name, res) #normally caused by bad arguments to the server

        #Calls are not required to have a schema, and
        # schemas are only for successful calls.
        if success and res_schema:
            try:
                validictory.validate(res, res_schema)
            except ValueError as details:
                self.log.warning("Received an unexpected response from call %s.", service_name)
                self.log.debug("full response: %s", res)
                self.log.debug("failed schema: %s", res_schema)
                self.log.warning("error was: %s", details)
                    
        return res
예제 #3
0
    def _wc_call(self, service_name, *args, **kw):
        """Returns the response of a web client call.
        :param service_name: the name of the call, eg ``search``
        additional positional arguments are passed to ``build_body``for the retrieved protocol.
        if a 'query_args' key is present in kw, it is assumed to be a dictionary of additional key/val pairs to append to the query string.
        """


        protocol = getattr(self.wc_protocol, service_name)

        #Always log the request.
        self.log.debug("wc_call %s %s", service_name, args)
        
        body, res_schema = protocol.build_transaction(*args)
        

        #Encode the body. It might be None (empty).
        #This should probably be done in protocol, and an encoded body grabbed here.
        if body != None: #body can be {}, which is different from None. {} is falsey.
            body = "json=" + urllib.quote_plus(json.dumps(body))

        extra_query_args = None
        if 'query_args' in kw:
            extra_query_args = kw['query_args']

        if protocol.requires_login:
            res = self.wc_session.open_authed_https_url(protocol.build_url, extra_query_args, body)
        else:
            res = self.wc_session.open_https_url(protocol.build_url, extra_query_args, body)
        
        res = json.loads(res.read())

        #Calls are not required to have a schema.
        if res_schema:
            try:
                validictory.validate(res, res_schema)
            except ValueError as details:
                self.log.warning("Received an unexpected response from call %s.", service_name)
                self.log.debug("full response: %s", res)
                self.log.debug("failed schema: %s", res_schema)
                self.log.warning("error was: %s", details)
                    
        if protocol.gets_logged:
            self.log.debug("wc_call response %s", res)
        else:
            self.log.debug("wc_call response <suppressed>")

        #Check if the server reported success.
        #It's likely a failure will not pass validation, as well.
        if not utils.call_succeeded(res):
            self.log.warning("call to %s failed", service_name)
            self.log.debug("full response: %s", res)

        return res
예제 #4
0
    def change_playlist(self, playlist_id, desired_playlist, safe=True):
        """Changes the order and contents of an existing playlist. Returns True on success, False if the playlist could be in an inconsistent state due to a problem.
        
        :param playlist_id: the id of the playlist being modified.
        :param desired_playlist: the desired contents and order as a list of song dictionaries, like is returned from :func:`get_playlist_songs`.
        :param safe: if True, ensure playlists will not be lost if a problem occurs. This may slow down updates.

        The server only provides 3 basic (atomic) playlist mutations: addition, deletion, and reordering. This function will automagically use these to apply a list representation of the desired changes.

        However, this might involve multiple calls to the server, and if a call fails, the playlist will be left in an inconsistent state. The `safe` option makes a backup of the playlist before doing anything, so it can be rolled back if a problem occurs. This is enabled by default. Note that this might slow down updates of very large playlists.

        There will always be a warning logged if a problem occurs, even if `safe` is False.
        """
        
        #We'll be modifying the entries in the playlist, and need to copy it.
        #Copying ensures two things:
        # 1. the user won't see our changes
        # 2. changing a key for one entry won't change it for another - which would be the case
        #     if the user appended the same song twice, for example.
        desired_playlist = [copy.deepcopy(t) for t in desired_playlist]
        server_tracks = self.get_playlist_songs(playlist_id)

        ##Make the backup.
        if safe:
            #The backup is stored on the server as a new playlist with "_gmusicapi_backup" appended to the backed up name.
            #We can't just store the backup here, since when rolling back we'd be relying on this function - which just failed.
            names_to_ids = self.get_playlists(always_id_lists=True)['user']
            playlist_name = (ni_pair[0] 
                             for ni_pair in names_to_ids.iteritems()
                             if playlist_id in ni_pair[1]).next()

            backup_id = self.copy_playlist(playlist_id, playlist_name + "_gmusicapi_backup")

        ##Try to change.
        try:
            #Counter, Counter, and set of id pairs to delete, add, and keep.
            to_del, to_add, to_keep = tools.find_playlist_changes(server_tracks, desired_playlist)

            ##Delete unwanted entries.
            to_del_eids = [pair[1] for pair in to_del.elements()]
            if to_del_eids and not utils.call_succeeded(self._remove_entries_from_playlist(playlist_id, to_del_eids)):
                raise PlaylistModificationError

            ##Add new entries.
            to_add_sids = [pair[0] for pair in to_add.elements()]
            if to_add_sids:
                res = self.add_songs_to_playlist(playlist_id, to_add_sids)
                if not utils.call_succeeded(res):
                    raise PlaylistModificationError

                ##Update desired tracks with added tracks server-given eids.
                #Map new sid -> [eids]
                new_sid_to_eids = {}
                for sid, eid in ((s["songId"], s["playlistEntryId"]) for s in res["songIds"]):
                    if not sid in new_sid_to_eids:
                        new_sid_to_eids[sid] = []
                    new_sid_to_eids[sid].append(eid)

                    
                for d_t in desired_playlist:
                    if d_t["id"] in new_sid_to_eids:
                        #Found a matching sid.
                        match = d_t
                        sid = match["id"]
                        eid = match.get("playlistEntryId") 
                        pair = (sid, eid)

                        if pair in to_keep:
                            to_keep.remove(pair) #only keep one of the to_keep eids.
                        else:
                            match["playlistEntryId"] = new_sid_to_eids[sid].pop()
                            if len(new_sid_to_eids[sid]) == 0:
                                del new_sid_to_eids[sid]
                            

            ##Now, the right eids are in the playlist.
            ##Set the order of the tracks:

            #The web client has no way to dictate the order without block insertion,
            # but the api actually supports setting the order to a given list.
            #For whatever reason, though, it needs to be set backwards; might be
            # able to get around this by messing with afterEntry and beforeEntry parameters.
            sids, eids = zip(*tools.get_id_pairs(desired_playlist[::-1]))

            if sids and not utils.call_succeeded(self._wc_call("changeplaylistorder", playlist_id, sids, eids)):
                raise PlaylistModificationError

            ##Clean up the backup.
            #Nothing to do if this fails (retry?), so assume it succeeds.
            if safe: self.delete_playlist(backup_id)
            return True

        except PlaylistModificationError:
            self.log.warning("a subcall of change_playlist failed - playlist %s is in an inconsistent state", playlist_id)
            reverted = False

            if safe:
                self.log.warning("attempting to revert changes from playlist '%s_gmusicapi_backup'", playlist_name)

                if all(map(utils.call_succeeded,
                           [self.delete_playlist(playlist_id),
                            self.change_playlist_name(backup_id, playlist_name)])):
                    reverted = True
                    
            if reverted:
                self.log.warning("reverted changes safely; playlist id of '%s' is now '%s'", playlist_name, backup_id)
            
            return reverted