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
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