mirror of
https://github.com/fergalmoran/picard.git
synced 2026-02-26 01:23:58 +00:00
Merge pull request #642 from antlarr/album-cover-art
PICARD-1000: Show album/cluster cover art
This commit is contained in:
@@ -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 '<Album %s %r>' % (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):
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 += '<br /><i>%s</i>' % 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()
|
||||
|
||||
@@ -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 = '<br/>'.join(map(lambda i: '<b>%s</b><br/>%s' %
|
||||
return '<br/>'.join(map(lambda i: '<b>%s</b><br/>%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 += '<hr />' + '<hr />'.join(info_files)
|
||||
self.ui.info.setText(text)
|
||||
|
||||
|
||||
class ClusterInfoDialog(InfoDialog):
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
79
picard/util/lrucache.py
Normal file
79
picard/util/lrucache.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2017 Antonio Larrosa <alarrosa@suse.com>
|
||||
#
|
||||
# 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 "<stdin>", line 1, in <module>
|
||||
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 "<stdin>", line 1, in <module>
|
||||
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)
|
||||
Reference in New Issue
Block a user