def change_playlist(self, playlist_id, desired_playlist, safe=True):
        """Changes the order and contents of an existing playlist. Returns the id of the playlist when finished - which may not be the argument, in the case of a failure and recovery.
        
        :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)

        if safe: #make the backup.
            #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 - and it just failed.
            names_to_ids = self.get_all_playlist_ids(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")

        #Ensure CallFailures do not get suppressed in our subcalls.
        #Did not unsuppress the above copy_playlist call, since we should fail 
        # out if we can't ensure the backup was made.
        with self._unsuppress_failures():
            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: self._remove_entries_from_playlist(playlist_id, to_del_eids)

                ##Add new entries.
                to_add_sids = [pair[0] for pair in to_add.elements()]
                if to_add_sids:
                    new_pairs = self.add_songs_to_playlist(playlist_id, to_add_sids)

                    ##Update desired tracks with added tracks server-given eids.
                    #Map new sid -> [eids]
                    new_sid_to_eids = {}
                    for sid, eid in new_pairs:
                        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: self._wc_call("changeplaylistorder", playlist_id, sids, eids)

                ##Clean up the backup.
                if safe: self.delete_playlist(backup_id)

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

                if not safe: raise #there's nothing we can do
                else: #try to revert to the backup
                    self.log.warning("attempting to revert changes from playlist '%s_gmusicapi_backup'", playlist_name)

                    try:
                        self.delete_playlist(playlist_id)
                        self.change_playlist_name(backup_id, playlist_name)
                    except CallFailure:
                        self.log.error("failed to revert changes.")
                        raise
                    else:
                        self.log.warning("reverted changes safely; playlist id of '%s' is now '%s'", playlist_name, backup_id)
                        playlist_id = backup_id

            return playlist_id
Exemple #2
0
    def change_playlist(self, playlist_id, desired_playlist, safe=True):
        """Changes the order and contents of an existing playlist.
        Returns the id of the playlist when finished -
        this may be the same as the argument in the case of a failure and recovery.

        :param playlist_id: the id of the playlist being modified.
        :param desired_playlist: the desired contents and order as a list of
          :ref:`song dictionaries <songdict-format>`, 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 playlist mutations: addition, deletion, and reordering.
        This function will use these to automagically apply 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.
        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)

        if safe:
            #Make a backup.
            #The backup is stored on the server as a new playlist with "_gmusicapi_backup"
            # appended to the backed up name.
            names_to_ids = self.get_all_playlist_ids()['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 + u"_gmusicapi_backup")

        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:
                self._remove_entries_from_playlist(playlist_id, to_del_eids)

            ##Add new entries.
            to_add_sids = [pair[0] for pair in to_add.elements()]
            if to_add_sids:
                new_pairs = self.add_songs_to_playlist(playlist_id, to_add_sids)

                ##Update desired tracks with added tracks server-given eids.
                #Map new sid -> [eids]
                new_sid_to_eids = {}
                for sid, eid in new_pairs:
                    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.
            if desired_playlist:
                #can't *-unpack an empty list
                sids, eids = zip(*tools.get_id_pairs(desired_playlist[::-1]))

                if sids:
                    self._make_call(webclient.ChangePlaylistOrder, playlist_id, sids, eids)

            ##Clean up the backup.
            if safe:
                self.delete_playlist(backup_id)

        except CallFailure:
            self.logger.info("a subcall of change_playlist failed - "
                             "playlist %s is in an inconsistent state", playlist_id)

            if not safe:
                raise  # there's nothing we can do
            else:  # try to revert to the backup
                self.logger.info("attempting to revert changes from playlist "
                                 "'%s_gmusicapi_backup'", playlist_name)

                try:
                    self.delete_playlist(playlist_id)
                    self.change_playlist_name(backup_id, playlist_name)
                except CallFailure:
                    self.logger.warning("failed to revert failed change_playlist call on '%s'",
                                        playlist_name)
                    raise
                else:
                    self.logger.info("reverted changes safely; playlist id of '%s' is now '%s'",
                                     playlist_name, backup_id)
                    playlist_id = backup_id

        return playlist_id