diff --git a/picard/album.py b/picard/album.py index 6cc225553..273fd5870 100644 --- a/picard/album.py +++ b/picard/album.py @@ -58,6 +58,7 @@ class Album(DataObject, Item): def __init__(self, id, discid=None): DataObject.__init__(self, id) self.metadata = Metadata() + self.orig_metadata = Metadata() self.tracks = [] self.loaded = False self.load_task = None @@ -71,6 +72,7 @@ class Album(DataObject, Item): self.errors = [] self.status = None self._album_artists = [] + self.update_metadata_images_enabled = True def __repr__(self): return '' % (self.id, self.metadata[u"album"]) @@ -83,6 +85,11 @@ class Album(DataObject, Item): for file in self.unmatched_files.iterfiles(): yield file + def enable_update_metadata_images(self, enabled): + self.update_metadata_images_enabled = enabled + if enabled: + self.update_metadata_images() + def append_album_artist(self, id): """Append artist id to the list of album artists and return an AlbumArtist instance""" @@ -254,6 +261,7 @@ class Album(DataObject, Item): self._tracks_loaded = True if not self._requests: + self.enable_update_metadata_images(False) # Prepare parser for user's script if config.setting["enable_tagger_scripts"]: for s_pos, s_name, s_enabled, s_text in config.setting["list_of_scripts"]: @@ -275,6 +283,7 @@ class Album(DataObject, Item): self._new_metadata.strip_whitespace() for track in self.tracks: + track.metadata_images_changed.connect(self.update_metadata_images) for file in list(track.linked_files): file.move(self.unmatched_files) self.metadata = self._new_metadata @@ -285,6 +294,7 @@ class Album(DataObject, Item): self.status = None self.match_files(self.unmatched_files.files) self.update() + self.enable_update_metadata_images(True) self.tagger.window.set_statusbar_message( N_('Album %(id)s loaded: %(artist)s - %(album)s'), { @@ -378,14 +388,19 @@ class Album(DataObject, Item): def update(self, update_tracks=True): if self.item: self.item.update(update_tracks) + self.update_metadata_images() def _add_file(self, track, file): self._files += 1 self.update(update_tracks=False) + file.metadata_images_changed.connect(self.update_metadata_images) + self.update_metadata_images() def _remove_file(self, track, file): self._files -= 1 self.update(update_tracks=False) + file.metadata_images_changed.disconnect(self.update_metadata_images) + self.update_metadata_images() def match_files(self, files, use_recordingid=True): """Match files to tracks on this album, based on metadata similarity or recordingid.""" @@ -552,6 +567,52 @@ class Album(DataObject, Item): self.tagger.albums[mbid] = self self.load(priority=True, refresh=True) + def update_metadata_images(self): + if not self.update_metadata_images_enabled: + return + + class State: + new_images = [] + orig_images = [] + has_common_new_images = True + has_common_orig_images = True + first_new_obj = True + first_orig_obj = True + + state = State() + + def process_images(state, obj): + # Check new images + if state.first_new_obj: + state.new_images = obj.metadata.images[:] + state.first_new_obj = False + else: + if state.new_images != obj.metadata.images: + state.has_common_new_images = False + state.new_images.extend([image for image in obj.metadata.images if image not in state.new_images]) + if isinstance(obj, Track): + return + # Check orig images, but not for Tracks (which don't have orig_metadata) + if state.first_orig_obj: + state.orig_images = obj.orig_metadata.images[:] + state.first_orig_obj = False + else: + if state.orig_images != obj.orig_metadata.images: + state.has_common_orig_images = False + state.orig_images.extend([image for image in obj.orig_metadata.images if image not in state.orig_images]) + + for track in self.tracks: + process_images(state, track) + for file in list(track.linked_files): + process_images(state, file) + for file in list(self.unmatched_files.files): + process_images(state, file) + + self.metadata.images = state.new_images + self.metadata.has_common_images = state.has_common_new_images + self.orig_metadata.images = state.orig_images + self.orig_metadata.has_common_images = state.has_common_orig_images + class NatAlbum(Album): diff --git a/picard/cluster.py b/picard/cluster.py index 290ec2a41..546cfe834 100644 --- a/picard/cluster.py +++ b/picard/cluster.py @@ -69,6 +69,9 @@ class Cluster(QtCore.QObject, Item): self.metadata.length += file.metadata.length file._move(self) file.update(signal=False) + cover = file.metadata.get_single_front_image() + if cover and cover[0] not in self.metadata.images: + self.metadata.append_image(cover[0]) self.files.extend(files) self.metadata['totaltracks'] = len(self.files) self.item.add_files(files) @@ -79,6 +82,9 @@ class Cluster(QtCore.QObject, Item): self.metadata['totaltracks'] = len(self.files) file._move(self) file.update(signal=False) + cover = file.metadata.get_single_front_image() + if cover and cover[0] not in self.metadata.images: + self.metadata.append_image(cover[0]) self.item.add_file(file) def remove_file(self, file): diff --git a/picard/const/__init__.py b/picard/const/__init__.py index 5f22eb3dc..878765398 100644 --- a/picard/const/__init__.py +++ b/picard/const/__init__.py @@ -113,3 +113,6 @@ PLUGINS_API = { # Default query limit QUERY_LIMIT = 25 + +# Maximum number of covers to draw in a stack in CoverArtThumbnail +MAX_COVERS_TO_STACK = 4 diff --git a/picard/coverart/image.py b/picard/coverart/image.py index 69fdd1157..cb036cd47 100644 --- a/picard/coverart/image.py +++ b/picard/coverart/image.py @@ -70,6 +70,9 @@ class DataHash: def __eq__(self, other): return self._hash == other._hash + def hash(self): + return self._hash + def delete_file(self): if self._filename: try: @@ -211,6 +214,11 @@ class CoverArtImage: else: return False + def __hash__(self): + if self.datahash is None: + return 0 + return hash(self.datahash.hash()) + def set_data(self, data): """Store image data in a file, if data already exists in such file it will be re-used and no file write occurs diff --git a/picard/file.py b/picard/file.py index cf20ed8cf..d2097259b 100644 --- a/picard/file.py +++ b/picard/file.py @@ -54,6 +54,8 @@ from picard.const import QUERY_LIMIT class File(QtCore.QObject, Item): + metadata_images_changed = QtCore.pyqtSignal() + UNDEFINED = -1 PENDING = 0 NORMAL = 1 @@ -156,6 +158,7 @@ class File(QtCore.QObject, Item): if acoustid: self.metadata["acoustid_id"] = acoustid + self.metadata_images_changed.emit() def has_error(self): return self.state == File.ERROR diff --git a/picard/metadata.py b/picard/metadata.py index da7fd8133..09c0dda4f 100644 --- a/picard/metadata.py +++ b/picard/metadata.py @@ -55,6 +55,11 @@ class Metadata(dict): def append_image(self, coverartimage): self.images.append(coverartimage) + def set_front_image(self, coverartimage): + # First remove all front images + self.images[:] = [img for img in self.images if not img.is_front_image()] + self.images.append(coverartimage) + @property def images_to_be_saved_to_tags(self): if not config.setting["save_images_to_tags"]: diff --git a/picard/track.py b/picard/track.py index 8cfbb1956..c1e33d1d4 100644 --- a/picard/track.py +++ b/picard/track.py @@ -19,6 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from functools import partial +from PyQt4 import QtCore from picard import config, log from picard.metadata import Metadata, run_track_metadata_processors from picard.dataobj import DataObject @@ -44,6 +45,8 @@ class TrackArtist(DataObject): class Track(DataObject, Item): + metadata_images_changed = QtCore.pyqtSignal() + def __init__(self, id, album=None): DataObject.__init__(self, id) self.album = album @@ -110,7 +113,7 @@ class Track(DataObject, Item): return True def can_view_info(self): - return self.num_linked_files == 1 + return self.num_linked_files == 1 or self.metadata.images def column(self, column): m = self.metadata diff --git a/picard/ui/coverartbox.py b/picard/ui/coverartbox.py index a188a1e50..e196cf0a3 100644 --- a/picard/ui/coverartbox.py +++ b/picard/ui/coverartbox.py @@ -27,6 +27,8 @@ from picard.coverart.image import CoverArtImage, CoverArtImageError from picard.track import Track from picard.file import File from picard.util import encode_filename, imageinfo +from picard.util.lrucache import LRUCache +from picard.const import MAX_COVERS_TO_STACK if sys.platform == 'darwin': try: @@ -38,7 +40,6 @@ if sys.platform == 'darwin': class ActiveLabel(QtGui.QLabel): - """Clickable QLabel.""" clicked = QtCore.pyqtSignal() @@ -83,7 +84,7 @@ class ActiveLabel(QtGui.QLabel): class CoverArtThumbnail(ActiveLabel): - def __init__(self, active=False, drops=False, *args, **kwargs): + def __init__(self, active=False, drops=False, pixmap_cache=None, *args, **kwargs): super(CoverArtThumbnail, self).__init__(active, drops, *args, **kwargs) self.data = None self.shadow = QtGui.QPixmap(":/images/CoverArtShadow.png") @@ -93,6 +94,7 @@ class CoverArtThumbnail(ActiveLabel): self.clicked.connect(self.open_release_page) self.image_dropped.connect(self.fetch_remote_image) self.related_images = list() + self._pixmap_cache = pixmap_cache def __eq__(self, other): if len(self.related_images) or len(other.related_images): @@ -103,7 +105,20 @@ class CoverArtThumbnail(ActiveLabel): def show(self): self.set_data(self.data, True) - def set_data(self, data, force=False, pixmap=None): + def decorate_cover(self, pixmap): + offx, offy, w, h = (1, 1, 121, 121) + cover = QtGui.QPixmap(self.shadow) + pixmap = pixmap.scaled(w, h, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) + painter = QtGui.QPainter(cover) + bgcolor = QtGui.QColor.fromRgb(0, 0, 0, 128) + painter.fillRect(QtCore.QRectF(offx, offy, w, h), bgcolor) + x = offx + (w - pixmap.width()) / 2 + y = offy + (h - pixmap.height()) / 2 + painter.drawPixmap(x, y, pixmap) + painter.end() + return cover + + def set_data(self, data, force=False, has_common_images=True): if not force and self.data == data: return @@ -111,43 +126,98 @@ class CoverArtThumbnail(ActiveLabel): if not force and self.parent().isHidden(): return - cover = self.shadow - if self.data: - if pixmap is None: + if not self.data: + self.setPixmap(self.shadow) + return + + w, h, displacements = (128, 128, 20) + key = hash(tuple(sorted(self.data)) + (has_common_images,)) + try: + pixmap = self._pixmap_cache[key] + except KeyError: + if len(self.data) == 1: pixmap = QtGui.QPixmap() - pixmap.loadFromData(self.data.data) - if not pixmap.isNull(): - offx, offy, w, h = (1, 1, 121, 121) - cover = QtGui.QPixmap(self.shadow) - pixmap = pixmap.scaled(w, h, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) - painter = QtGui.QPainter(cover) - bgcolor = QtGui.QColor.fromRgb(0, 0, 0, 128) - painter.fillRect(QtCore.QRectF(offx, offy, w, h), bgcolor) - x = offx + (w - pixmap.width()) / 2 - y = offy + (h - pixmap.height()) / 2 - painter.drawPixmap(x, y, pixmap) + pixmap.loadFromData(self.data[0].data) + pixmap = self.decorate_cover(pixmap) + else: + limited = len(self.data) > MAX_COVERS_TO_STACK + if limited: + data_to_paint = data[:MAX_COVERS_TO_STACK - 1] + offset = displacements * len(data_to_paint) + else: + data_to_paint = data + offset = displacements * (len(data_to_paint) - 1) + stack_width, stack_height = (w + offset, h + offset) + pixmap = QtGui.QPixmap(stack_width, stack_height) + bgcolor = self.palette().color(QtGui.QPalette.Window) + painter = QtGui.QPainter(pixmap) + painter.fillRect(QtCore.QRectF(0, 0, stack_width, stack_height), bgcolor) + cx = stack_width - w / 2 + cy = h / 2 + if limited: + x, y = (cx - self.shadow.width() / 2, cy - self.shadow.height() / 2) + for i in range(3): + painter.drawPixmap(x, y, self.shadow) + x -= displacements / 3 + y += displacements / 3 + cx -= displacements + cy += displacements + else: + cx = stack_width - w / 2 + cy = h / 2 + for image in reversed(data_to_paint): + if isinstance(image, QtGui.QPixmap): + thumb = image + else: + thumb = QtGui.QPixmap() + thumb.loadFromData(image.data) + thumb = self.decorate_cover(thumb) + x, y = (cx - thumb.width() / 2, cy - thumb.height() / 2) + painter.drawPixmap(x, y, thumb) + cx -= displacements + cy += displacements + if not has_common_images: + color = QtGui.QColor("darkgoldenrod") + border_length = 10 + for k in range(border_length): + color.setAlpha(255 - k * 255 / border_length) + painter.setPen(color) + painter.drawLine(x, y - k - 1, x + 121 + k + 1, y - k - 1) + painter.drawLine(x + 121 + k + 2, y - 1 - k, x + 121 + k + 2, y + 121 + 4) + for k in range(5): + bgcolor.setAlpha(80 + k * 255 / 7) + painter.setPen(bgcolor) + painter.drawLine(x + 121 + 2, y + 121 + 2 + k, x + 121 + border_length + 2, y + 121 + 2 + k) painter.end() - self.setPixmap(cover) + pixmap = pixmap.scaled(w, h, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) + self._pixmap_cache[key] = pixmap + + self.setPixmap(pixmap) def set_metadata(self, metadata): data = None self.related_images = [] if metadata and metadata.images: self.related_images = metadata.images - for image in metadata.images: - if image.is_front_image(): - data = image - break - else: + data = [image for image in metadata.images if image.is_front_image()] + if not data: # There's no front image, choose the first one available - data = metadata.images[0] - self.set_data(data) + data = [metadata.images[0]] + has_common_images = getattr(metadata, 'has_common_images', True) + self.set_data(data, has_common_images=has_common_images) release = None if metadata: release = metadata.get("musicbrainz_albumid", None) if release: self.setActive(True) - self.setToolTip(_(u"View release on MusicBrainz")) + text = _(u"View release on MusicBrainz") + if hasattr(metadata, 'has_common_images'): + if has_common_images: + note = _(u'Common images on all tracks') + else: + note = _(u'Tracks contain different images') + text += '
%s' % note + self.setToolTip(text) else: self.setActive(False) self.setToolTip("") @@ -172,12 +242,13 @@ class CoverArtBox(QtGui.QGroupBox): self.setStyleSheet('''QGroupBox{background-color:none;border:1px;}''') self.setFlat(True) self.item = None + self.pixmap_cache = LRUCache(40) self.cover_art_label = QtGui.QLabel('') self.cover_art_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter) - self.cover_art = CoverArtThumbnail(False, True, parent) + self.cover_art = CoverArtThumbnail(False, True, self.pixmap_cache, parent) spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) self.orig_cover_art_label = QtGui.QLabel('') - self.orig_cover_art = CoverArtThumbnail(False, False, parent) + self.orig_cover_art = CoverArtThumbnail(False, False, self.pixmap_cache, parent) self.orig_cover_art_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter) self.show_details_button = QtGui.QPushButton(_(u'Show more details'), self) self.layout.addWidget(self.cover_art_label) @@ -209,7 +280,7 @@ class CoverArtBox(QtGui.QGroupBox): # We want to show the 2 coverarts only if they are different # and orig_cover_art data is set and not the default cd shadow if self.orig_cover_art.data is None or self.cover_art == self.orig_cover_art: - self.show_details_button.setHidden(len(self.cover_art.related_images) <= 1) + self.show_details_button.setHidden(len(self.cover_art.related_images) == 0) self.orig_cover_art.setHidden(True) self.cover_art_label.setText('') self.orig_cover_art_label.setText('') @@ -290,6 +361,7 @@ class CoverArtBox(QtGui.QGroupBox): try: coverartimage = CoverArtImage( url=url.toString(), + types=[u'front'], data=data ) except CoverArtImageError as e: @@ -297,21 +369,29 @@ class CoverArtBox(QtGui.QGroupBox): return if isinstance(self.item, Album): album = self.item - album.metadata.append_image(coverartimage) + album.enable_update_metadata_images(False) for track in album.tracks: - track.metadata.append_image(coverartimage) + track.metadata.set_front_image(coverartimage) + track.metadata_images_changed.emit() for file in album.iterfiles(): - file.metadata.append_image(coverartimage) + file.metadata.set_front_image(coverartimage) + file.metadata_images_changed.emit() file.update() + album.enable_update_metadata_images(True) elif isinstance(self.item, Track): track = self.item - track.metadata.append_image(coverartimage) + track.album.enable_update_metadata_images(False) + track.metadata.set_front_image(coverartimage) + track.metadata_images_changed.emit() for file in track.iterfiles(): - file.metadata.append_image(coverartimage) + file.metadata.set_front_image(coverartimage) + file.metadata_images_changed.emit() file.update() + track.album.enable_update_metadata_images(True) elif isinstance(self.item, File): file = self.item - file.metadata.append_image(coverartimage) + file.metadata.set_front_image(coverartimage) + file.metadata_images_changed.emit() file.update() self.cover_art.set_metadata(self.item.metadata) self.show() diff --git a/picard/ui/infodialog.py b/picard/ui/infodialog.py index b4a90a2c0..5496f205b 100644 --- a/picard/ui/infodialog.py +++ b/picard/ui/infodialog.py @@ -24,6 +24,7 @@ from PyQt4 import QtGui, QtCore from picard import log from picard.file import File from picard.track import Track +from picard.album import Album from picard.coverart.image import CoverArtImageIOError from picard.util import format_time, encode_filename, bytes2human, webbrowser2, union_sorted_lists from picard.ui import PicardDialog @@ -100,10 +101,14 @@ class InfoDialog(PicardDialog): self.obj = obj self.ui = Ui_InfoDialog() self.display_existing_artwork = False - if isinstance(obj, File) and isinstance(obj.parent, Track) or \ - isinstance(obj, Track): + if (isinstance(obj, File) + and isinstance(obj.parent, Track) + or isinstance(obj, Track)): # Display existing artwork only if selected object is track object # or linked to a track object + if getattr(obj, 'orig_metadata', None) is not None: + self.display_existing_artwork = True + elif isinstance(obj, Album) and obj.get_num_total_files() > 0: self.display_existing_artwork = True self.ui.setupUi(self) @@ -236,8 +241,8 @@ class FileInfoDialog(InfoDialog): InfoDialog.__init__(self, file, parent) self.setWindowTitle(_("Info") + " - " + file.base_filename) - def _display_info_tab(self): - file = self.obj + @staticmethod + def format_file_info(file): info = [] info.append((_('Filename:'), file.filename)) if '~format' in file.orig_metadata: @@ -265,9 +270,13 @@ class FileInfoDialog(InfoDialog): else: ch = str(ch) info.append((_('Channels:'), ch)) - text = '
'.join(map(lambda i: '%s
%s' % + return '
'.join(map(lambda i: '%s
%s' % (cgi.escape(i[0]), cgi.escape(i[1])), info)) + + def _display_info_tab(self): + file = self.obj + text = FileInfoDialog.format_file_info(file) self.ui.info.setText(text) @@ -296,6 +305,29 @@ class AlbumInfoDialog(InfoDialog): tabWidget.setTabText(tab_index, _("&Info")) self.tab_hide(tab) +class TrackInfoDialog(FileInfoDialog): + + def __init__(self, track, parent=None): + InfoDialog.__init__(self, track, parent) + self.setWindowTitle(_("Track Info")) + + def _display_info_tab(self): + track = self.obj + tab = self.ui.info_tab + tabWidget = self.ui.tabWidget + tab_index = tabWidget.indexOf(tab) + if track.num_linked_files == 0: + tabWidget.setTabText(tab_index, _("&Info")) + self.tab_hide(tab) + return + + tabWidget.setTabText(tab_index, _("&Info")) + text = ungettext("%i file in this track", "%i files in this track", + track.num_linked_files) % track.num_linked_files + info_files = [FileInfoDialog.format_file_info(file) for file in track.linked_files] + text += '
' + '
'.join(info_files) + self.ui.info.setText(text) + class ClusterInfoDialog(InfoDialog): diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index 84f75bad2..0a704f7a4 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -33,7 +33,7 @@ from picard.ui.metadatabox import MetadataBox from picard.ui.filebrowser import FileBrowser from picard.ui.tagsfromfilenames import TagsFromFileNamesDialog from picard.ui.options.dialog import OptionsDialog -from picard.ui.infodialog import FileInfoDialog, AlbumInfoDialog, ClusterInfoDialog +from picard.ui.infodialog import FileInfoDialog, AlbumInfoDialog, TrackInfoDialog, ClusterInfoDialog from picard.ui.infostatus import InfoStatus from picard.ui.passworddialog import PasswordDialog, ProxyDialog from picard.ui.logview import LogView, HistoryView @@ -844,6 +844,9 @@ class MainWindow(QtGui.QMainWindow): elif isinstance(self.selected_objects[0], Cluster): cluster = self.selected_objects[0] dialog = ClusterInfoDialog(cluster, self) + elif isinstance(self.selected_objects[0], Track): + track = self.selected_objects[0] + dialog = TrackInfoDialog(track, self) else: file = self.tagger.get_files_from_objects(self.selected_objects)[0] dialog = FileInfoDialog(file, self) @@ -955,6 +958,9 @@ class MainWindow(QtGui.QMainWindow): } self.set_statusbar_message(msg, mparms, echo=None, history=None) + elif isinstance(obj, Album): + metadata = obj.metadata + orig_metadata = obj.orig_metadata elif obj.can_edit_tags(): metadata = obj.metadata diff --git a/picard/util/lrucache.py b/picard/util/lrucache.py new file mode 100644 index 000000000..42c147970 --- /dev/null +++ b/picard/util/lrucache.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# Copyright (C) 2017 Antonio Larrosa +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +class LRUCache(dict): + """ + Helper class to cache items using a Least Recently Used policy. + + It's originally used to cache generated pixmaps in the CoverArtBox object + but it's generic enough to be used for other purposes if necessary. + The cache will never hold more than max_size items and the item least + recently used will be discarded. + + >>> cache = LRUCache(3) + >>> cache['item1'] = 'some value' + >>> cache['item2'] = 'some other value' + >>> cache['item3'] = 'yet another value' + >>> cache['item1'] + 'some value' + >>> cache['item4'] = 'This will push item 2 out of the cache' + >>> cache['item2'] + Traceback (most recent call last): + File "", line 1, in + File "lrucache.py", line 48, in __getitem__ + return super(LRUCache, self).__getitem__(key) + KeyError: 'item2' + >>> cache['item5'] = 'This will push item3 out of the cache' + >>> cache['item3'] + Traceback (most recent call last): + File "", line 1, in + File "lrucache.py", line 48, in __getitem__ + return super(LRUCache, self).__getitem__(key) + KeyError: 'item3' + >>> cache['item1'] + 'some value' + """ + + def __init__(self, max_size): + self._ordered_keys = [] + self._max_size = max_size + + def __getitem__(self, key): + if key in self: + self._ordered_keys.remove(key) + self._ordered_keys.insert(0, key) + return super(LRUCache, self).__getitem__(key) + + def __setitem__(self, key, value): + if key in self: + self._ordered_keys.remove(key) + self._ordered_keys.insert(0, key) + + r = super(LRUCache, self).__setitem__(key, value) + + if len(self) > self._max_size: + item = self._ordered_keys.pop() + super(LRUCache, self).__delitem__(item) + + return r + + def __delitem__(self, key): + self._ordered_keys.remove(key) + super(LRUCache, self).__delitem__(key)