From 890b5e4c57d2a1057837baf46292a827a9285175 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 30 Apr 2013 17:22:22 +0530 Subject: [PATCH] Implement a metadata comparison/merging tool --- src/calibre/gui2/comments_editor.py | 16 +- src/calibre/gui2/metadata/basic_widgets.py | 54 +-- src/calibre/gui2/metadata/diff.py | 472 +++++++++++++++++++++ src/calibre/library/field_metadata.py | 2 +- 4 files changed, 515 insertions(+), 29 deletions(-) diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index 41f92b509a..c043fccba4 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -66,6 +66,7 @@ class EditorWidget(QWebView): # {{{ def __init__(self, parent=None): QWebView.__init__(self, parent) + self.readonly = False self.comments_pat = re.compile(r'', re.DOTALL) @@ -163,7 +164,11 @@ class EditorWidget(QWebView): # {{{ self.page().linkClicked.connect(self.link_clicked) self.setHtml('') - self.page().setContentEditable(True) + self.set_readonly(False) + + def set_readonly(self, what): + self.readonly = what + self.page().setContentEditable(not self.readonly) def clear_text(self, *args): us = self.page().undoStack() @@ -313,7 +318,7 @@ class EditorWidget(QWebView): # {{{ # toList() is needed because PyQt on Debian is old/broken for body in self.page().mainFrame().documentElement().findAll('body').toList(): body.setAttribute('style', style) - self.page().setContentEditable(True) + self.page().setContentEditable(not self.readonly) def keyPressEvent(self, ev): if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab): @@ -585,6 +590,7 @@ class Editor(QWidget): # {{{ self.tabs.addTab(self.code_edit, _('HTML Source')) self.tabs.currentChanged[int].connect(self.change_tab) self.highlighter = Highlighter(self.code_edit.document()) + self.layout().setContentsMargins(0, 0, 0, 0) # toolbar1 {{{ self.toolbar1.addAction(self.editor.action_undo) @@ -666,6 +672,12 @@ class Editor(QWidget): # {{{ self.toolbar2.setVisible(False) self.toolbar3.setVisible(False) + def set_readonly(self, what): + self.editor.set_readonly(what) + + def hide_tabs(self): + self.tabs.tabBar().setVisible(False) + # }}} if __name__ == '__main__': diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 984986affe..5544c52273 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -101,7 +101,7 @@ class TitleEdit(EnLineEdit): getattr(db, 'set_'+ self.TITLE_ATTR)(id_, title, notify=False, commit=False) except (IOError, OSError) as err: - if getattr(err, 'errno', None) == errno.EACCES: # Permission denied + if getattr(err, 'errno', None) == errno.EACCES: # Permission denied import traceback fname = getattr(err, 'filename', None) p = 'Locked file: %s\n\n'%fname if fname else '' @@ -273,7 +273,7 @@ class AuthorsEdit(EditWithComplete): self.books_to_refresh |= db.set_authors(id_, authors, notify=False, allow_case_change=True) except (IOError, OSError) as err: - if getattr(err, 'errno', None) == errno.EACCES: # Permission denied + if getattr(err, 'errno', None) == errno.EACCES: # Permission denied import traceback fname = getattr(err, 'filename', None) p = 'Locked file: %s\n\n'%fname if fname else '' @@ -485,7 +485,7 @@ class SeriesEdit(EditWithComplete): def initialize(self, db, id_): self.books_to_refresh = set([]) all_series = db.all_series() - all_series.sort(key=lambda x : sort_key(x[1])) + all_series.sort(key=lambda x: sort_key(x[1])) self.update_items_cache([x[1] for x in all_series]) series_id = db.series_id(id_, index_is_id=True) inval = '' @@ -586,7 +586,7 @@ class SeriesIndexEdit(QDoubleSpinBox): # }}} -class BuddyLabel(QLabel): # {{{ +class BuddyLabel(QLabel): # {{{ def __init__(self, buddy): QLabel.__init__(self, buddy.LABEL) @@ -698,11 +698,11 @@ class FormatsManager(QWidget): self.formats.setIconSize(QSize(32, 32)) self.formats.setMaximumWidth(200) - l.addWidget(self.cover_from_format_button, 0, 0, 1, 1) + l.addWidget(self.cover_from_format_button, 0, 0, 1, 1) l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1) - l.addWidget(self.add_format_button, 0, 2, 1, 1) - l.addWidget(self.remove_format_button, 2, 2, 1, 1) - l.addWidget(self.formats, 0, 1, 3, 1) + l.addWidget(self.add_format_button, 0, 2, 1, 1) + l.addWidget(self.remove_format_button, 2, 2, 1, 1) + l.addWidget(self.formats, 0, 1, 3, 1) self.temp_files = [] @@ -882,7 +882,7 @@ class FormatsManager(QWidget): self.temp_files = [] # }}} -class Cover(ImageView): # {{{ +class Cover(ImageView): # {{{ download_cover = pyqtSignal() @@ -1052,7 +1052,7 @@ class Cover(ImageView): # {{{ # }}} -class CommentsEdit(Editor): # {{{ +class CommentsEdit(Editor): # {{{ @dynamic_property def current_val(self): @@ -1076,7 +1076,7 @@ class CommentsEdit(Editor): # {{{ return True # }}} -class RatingEdit(QSpinBox): # {{{ +class RatingEdit(QSpinBox): # {{{ LABEL = _('&Rating:') TOOLTIP = _('Rating of this book. 0-5 stars') @@ -1120,7 +1120,7 @@ class RatingEdit(QSpinBox): # {{{ # }}} -class TagsEdit(EditWithComplete): # {{{ +class TagsEdit(EditWithComplete): # {{{ LABEL = _('Ta&gs:') TOOLTIP = '

'+_('Tags categorize the book. This is particularly ' 'useful while searching.

They can be any words ' @@ -1174,7 +1174,6 @@ class TagsEdit(EditWithComplete): # {{{ self.current_val = d.tags self.all_items = db.all_tags() - def commit(self, db, id_): self.books_to_refresh |= db.set_tags( id_, self.current_val, notify=False, commit=False, @@ -1183,7 +1182,7 @@ class TagsEdit(EditWithComplete): # {{{ # }}} -class LanguagesEdit(LE): # {{{ +class LanguagesEdit(LE): # {{{ LABEL = _('&Languages:') TOOLTIP = _('A comma separated list of languages for this book') @@ -1194,8 +1193,10 @@ class LanguagesEdit(LE): # {{{ @dynamic_property def current_val(self): - def fget(self): return self.lang_codes - def fset(self, val): self.lang_codes = val + def fget(self): + return self.lang_codes + def fset(self, val): + self.lang_codes = val return property(fget=fget, fset=fset) def initialize(self, db, id_): @@ -1221,7 +1222,7 @@ class LanguagesEdit(LE): # {{{ return True # }}} -class IdentifiersEdit(QLineEdit): # {{{ +class IdentifiersEdit(QLineEdit): # {{{ LABEL = _('I&ds:') BASE_TT = _('Edit the identifiers for this book. ' 'For example: \n\n%s')%( @@ -1309,7 +1310,7 @@ class IdentifiersEdit(QLineEdit): # {{{ # }}} -class ISBNDialog(QDialog) : # {{{ +class ISBNDialog(QDialog): # {{{ def __init__(self, parent, txt): QDialog.__init__(self, parent) @@ -1320,7 +1321,7 @@ class ISBNDialog(QDialog) : # {{{ l.addWidget(w, 0, 0, 1, 2) w = QLabel(_('ISBN:')) l.addWidget(w, 1, 0, 1, 1) - self.line_edit = w = QLineEdit(); + self.line_edit = w = QLineEdit() w.setText(txt) w.selectAll() w.textChanged.connect(self.checkText) @@ -1361,7 +1362,7 @@ class ISBNDialog(QDialog) : # {{{ # }}} -class PublisherEdit(EditWithComplete): # {{{ +class PublisherEdit(EditWithComplete): # {{{ LABEL = _('&Publisher:') def __init__(self, parent): @@ -1388,7 +1389,7 @@ class PublisherEdit(EditWithComplete): # {{{ def initialize(self, db, id_): self.books_to_refresh = set([]) all_publishers = db.all_publishers() - all_publishers.sort(key=lambda x : sort_key(x[1])) + all_publishers.sort(key=lambda x: sort_key(x[1])) self.update_items_cache([x[1] for x in all_publishers]) publisher_id = db.publisher_id(id_, index_is_id=True) inval = '' @@ -1421,7 +1422,7 @@ class DateEdit(QDateTimeEdit): ATTR = 'timestamp' TWEAK = 'gui_timestamp_display_format' - def __init__(self, parent): + def __init__(self, parent, create_clear_button=True): QDateTimeEdit.__init__(self, parent) self.setToolTip(self.TOOLTIP) self.setWhatsThis(self.TOOLTIP) @@ -1435,10 +1436,11 @@ class DateEdit(QDateTimeEdit): self.setCalendarWidget(self.cw) self.setMinimumDateTime(UNDEFINED_QDATETIME) self.setSpecialValueText(_('Undefined')) - self.clear_button = QToolButton(parent) - self.clear_button.setIcon(QIcon(I('trash.png'))) - self.clear_button.setToolTip(_('Clear date')) - self.clear_button.clicked.connect(self.reset_date) + if create_clear_button: + self.clear_button = QToolButton(parent) + self.clear_button.setIcon(QIcon(I('trash.png'))) + self.clear_button.setToolTip(_('Clear date')) + self.clear_button.clicked.connect(self.reset_date) def reset_date(self, *args): self.current_val = None diff --git a/src/calibre/gui2/metadata/diff.py b/src/calibre/gui2/metadata/diff.py index 2dfbaa2fb8..8c1b613a41 100644 --- a/src/calibre/gui2/metadata/diff.py +++ b/src/calibre/gui2/metadata/diff.py @@ -6,5 +6,477 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' +import os +from collections import OrderedDict, namedtuple +from functools import partial +from PyQt4.Qt import ( + QDialog, QWidget, QGridLayout, QLineEdit, QLabel, QToolButton, QIcon, + QVBoxLayout, QDialogButtonBox, QApplication, pyqtSignal, QFont, QPixmap, + QSize, QPainter, Qt, QColor, QPen, QSizePolicy, QScrollArea, QFrame) + +from calibre import fit_image +from calibre.ebooks.metadata import title_sort, authors_to_sort_string +from calibre.gui2 import pixmap_to_data, gprefs +from calibre.gui2.comments_editor import Editor +from calibre.gui2.metadata.basic_widgets import PubdateEdit, RatingEdit +from calibre.ptempfile import PersistentTemporaryFile +from calibre.utils.date import UNDEFINED_DATE + +Widgets = namedtuple('Widgets', 'new old label button') + +# Widgets {{{ + +class LineEdit(QLineEdit): + + changed = pyqtSignal() + + def __init__(self, field, is_new, parent, metadata, extra): + QLineEdit.__init__(self, parent) + self.is_new = is_new + self.field = field + self.metadata = metadata + if not is_new: + self.setReadOnly(True) + self.textChanged.connect(self.changed) + + def from_mi(self, mi): + val = mi.get(self.field, default='') or '' + ism = self.metadata['is_multiple'] + if ism: + if not val: + val = '' + else: + val = ism['list_to_ui'].join(val) + self.setText(val) + self.setCursorPosition(0) + + def to_mi(self, mi): + val = unicode(self.text()).strip() + ism = self.metadata['is_multiple'] + if ism: + if not val: + val = [] + else: + val = [x.strip() for x in val.split(ism['list_to_ui']) if x.strip()] + mi.set(self.field, val) + if self.field == 'title': + mi.set('title_sort', title_sort(val, lang=mi.language)) + elif self.field == 'authors': + mi.set('author_sort', authors_to_sort_string(val)) + + @dynamic_property + def current_val(self): + def fget(self): + return unicode(self.text()) + def fset(self, val): + self.setText(val) + self.setCursorPosition(0) + return property(fget=fget, fset=fset) + + @property + def is_blank(self): + return not self.current_val.strip() + +class RatingsEdit(RatingEdit): + + changed = pyqtSignal() + + def __init__(self, field, is_new, parent, metadata, extra): + RatingEdit.__init__(self, parent) + self.is_new = is_new + self.field = field + self.metadata = metadata + self.valueChanged.connect(self.changed) + if not is_new: + self.setReadOnly(True) + + def from_mi(self, mi): + val = (mi.get(self.field, default=0) or 0)/2 + self.setValue(val) + + def to_mi(self, mi): + mi.set(self.field, self.value() * 2) + + @property + def is_blank(self): + return self.value() == 0 + +class DateEdit(PubdateEdit): + + changed = pyqtSignal() + + def __init__(self, field, is_new, parent, metadata, extra): + PubdateEdit.__init__(self, parent, create_clear_button=False) + self.is_new = is_new + self.field = field + self.metadata = metadata + self.setDisplayFormat(extra) + self.dateTimeChanged.connect(self.changed) + if not is_new: + self.setReadOnly(True) + + def from_mi(self, mi): + self.current_val = mi.get(self.field, default=None) + + def to_mi(self, mi): + mi.set(self.field, self.current_val) + + @property + def is_blank(self): + return self.current_val == UNDEFINED_DATE + +class SeriesEdit(LineEdit): + + def from_mi(self, mi): + series = mi.get(self.field, default='') + series_index = mi.get(self.field + '_index', default=1.0) + val = '' + if series: + val = '%s [%s]' % (series, mi.format_series_index(series_index)) + self.setText(val) + self.setCursorPosition(0) + + def to_mi(self, mi): + val = unicode(self.text()).strip() + try: + series_index = float(val.rpartition('[')[-1].rstrip(']').strip()) + except: + series_index = 1.0 + series = val.rpartition('[')[0].strip() or None + mi.set(self.field, series) + mi.set(self.field + '_index', series_index) + +class IdentifiersEdit(LineEdit): + + def from_mi(self, mi): + val = ('%s:%s' % (k, v) for k, v in mi.identifiers.iteritems()) + self.setText(', '.join(val)) + + def to_mi(self, mi): + parts = (x.strip() for x in self.current_val.split(',') if x.strip()) + val = {x.partition(':')[0].strip():x.partition(':')[-1].strip() for x in parts} + mi.set_identifiers({k:v for k, v in val.iteritems() if k and v}) + +class CommentsEdit(Editor): + + changed = pyqtSignal() + + def __init__(self, field, is_new, parent, metadata, extra): + Editor.__init__(self, parent, one_line_toolbar=False) + self.is_new = is_new + self.field = field + self.metadata = metadata + self.hide_tabs() + if not is_new: + self.hide_toolbars() + self.set_readonly(True) + + @dynamic_property + def current_val(self): + def fget(self): + return self.html + def fset(self, val): + self.html = val or '' + self.changed.emit() + return property(fget=fget, fset=fset) + + def from_mi(self, mi): + val = mi.get(self.field, default='') + self.current_val = val + + def to_mi(self, mi): + mi.set(self.field, self.current_val) + + def sizeHint(self): + return QSize(450, 200) + + @property + def is_blank(self): + return not self.current_val.strip() + +class CoverView(QWidget): + + changed = pyqtSignal() + + def __init__(self, field, is_new, parent, metadata, extra): + QWidget.__init__(self, parent) + self.is_new = is_new + self.field = field + self.metadata = metadata + self.pixmap = None + self.blank = QPixmap(I('blank.png')) + self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.GrowFlag|QSizePolicy.ExpandFlag) + self.sizePolicy().setHeightForWidth(True) + + @property + def is_blank(self): + return self.pixmap is None + + @dynamic_property + def current_val(self): + def fget(self): + return self.pixmap + def fset(self, val): + self.pixmap = val + self.changed.emit() + self.update() + return property(fget=fget, fset=fset) + + def from_mi(self, mi): + p = getattr(mi, 'cover', None) + if p and os.path.exists(p): + pmap = QPixmap() + with open(p, 'rb') as f: + pmap.loadFromData(f.read()) + if not pmap.isNull(): + self.pixmap = pmap + self.update() + self.changed.emit() + return + cd = getattr(mi, 'cover_data', (None, None)) + if cd and cd[1]: + pmap = QPixmap() + pmap.loadFromData(cd[1]) + if not pmap.isNull(): + self.pixmap = pmap + self.update() + self.changed.emit() + return + self.pixmap = None + self.update() + self.changed.emit() + + def to_mi(self, mi): + mi.cover, mi.cover_data = None, (None, None) + if self.pixmap is not None and not self.pixmap.isNull(): + with PersistentTemporaryFile('.jpg') as pt: + pt.write(pixmap_to_data(self.pixmap)) + mi.cover = pt.name + + def sizeHint(self): + return QSize(225, 300) + + def paintEvent(self, event): + pmap = self.blank if self.pixmap is None or self.pixmap.isNull() else self.pixmap + target = self.rect() + scaled, width, height = fit_image(pmap.width(), pmap.height(), target.width(), target.height()) + target.setRect(target.x(), target.y(), width, height) + p = QPainter(self) + p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) + p.drawPixmap(target, pmap) + + if self.pixmap is not None and not self.pixmap.isNull(): + sztgt = target.adjusted(0, 0, 0, -4) + f = p.font() + f.setBold(True) + p.setFont(f) + sz = u'\u00a0%d x %d\u00a0'%(self.pixmap.width(), self.pixmap.height()) + flags = Qt.AlignBottom|Qt.AlignRight|Qt.TextSingleLine + szrect = p.boundingRect(sztgt, flags, sz) + p.fillRect(szrect.adjusted(0, 0, 0, 4), QColor(0, 0, 0, 200)) + p.setPen(QPen(QColor(255,255,255))) + p.drawText(sztgt, flags, sz) + p.end() +# }}} + +class CompareSingle(QWidget): + + def __init__( + self, field_metadata, parent=None, revert_tooltip=None, + datetime_fmt='MMMM yyyy', blank_as_equal=True, + fields=('title', 'authors', 'series', 'tags', 'rating', 'publisher', 'pubdate', 'identifiers', 'comments', 'cover')): + QWidget.__init__(self, parent) + self.l = l = QGridLayout() + l.setContentsMargins(0, 0, 0, 0) + self.setLayout(l) + revert_tooltip = revert_tooltip or _('Revert %s') + self.current_mi = None + self.changed_font = QFont(QApplication.font()) + self.changed_font.setBold(True) + self.changed_font.setItalic(True) + self.blank_as_equal = blank_as_equal + + self.widgets = OrderedDict() + row = 0 + + for field in fields: + m = field_metadata[field] + dt = m['datatype'] + extra = None + if 'series' in {field, dt}: + cls = SeriesEdit + elif field == 'identifiers': + cls = IdentifiersEdit + elif 'comments' in {field, dt}: + cls = CommentsEdit + elif 'rating' in {field, dt}: + cls = RatingsEdit + elif dt == 'datetime': + extra = datetime_fmt + cls = DateEdit + elif field == 'cover': + cls = CoverView + elif dt in {'text', 'enum'}: + cls = LineEdit + else: + continue + neww = cls(field, True, self, m, extra) + neww.changed.connect(partial(self.changed, field)) + oldw = cls(field, False, self, m, extra) + newl = QLabel('&%s:' % m['name']) + newl.setBuddy(neww) + button = QToolButton(self) + button.setIcon(QIcon(I('back.png'))) + button.clicked.connect(partial(self.revert, field)) + button.setToolTip(revert_tooltip % m['name']) + self.widgets[field] = Widgets(neww, oldw, newl, button) + for i, w in enumerate((newl, neww, button, oldw)): + c = i if i < 2 else i + 1 + if w is oldw: + c += 1 + l.addWidget(w, row, c) + row += 1 + + self.sep = f = QFrame(self) + f.setFrameShape(f.VLine) + l.addWidget(f, 0, 2, row, 1) + self.sep2 = f = QFrame(self) + f.setFrameShape(f.VLine) + l.addWidget(f, 0, 4, row, 1) + + def changed(self, field): + w = self.widgets[field] + if w.new.current_val != w.old.current_val and (not self.blank_as_equal or not w.new.is_blank): + w.label.setFont(self.changed_font) + else: + w.label.setFont(QApplication.font()) + + def revert(self, field): + widgets = self.widgets[field] + neww, oldw = widgets[:2] + neww.current_val = oldw.current_val + + def __call__(self, oldmi, newmi): + self.current_mi = newmi + self.initial_vals = {} + for field, widgets in self.widgets.iteritems(): + widgets.old.from_mi(oldmi) + widgets.new.from_mi(newmi) + self.initial_vals[field] = widgets.new.current_val + + def apply_changes(self): + changed = False + for field, widgets in self.widgets.iteritems(): + val = widgets.new.current_val + if val != self.initial_vals[field]: + widgets.new.to_mi(self.current_mi) + changed = True + return changed + +class CompareMany(QDialog): + + def __init__(self, ids, get_metadata, field_metadata, parent=None, window_title=None, skip_button_tooltip=None, + accept_all_tooltip=None, reject_all_tooltip=None, **kwargs): + QDialog.__init__(self, parent) + self.l = l = QVBoxLayout() + self.setLayout(l) + self.setWindowIcon(QIcon(I('auto_author_sort.png'))) + self.get_metadata = get_metadata + self.ids = list(ids) + self.total = len(self.ids) + self.accepted = OrderedDict() + self.window_title = window_title or _('Compare metadata') + + self.compare_widget = CompareSingle(field_metadata, parent=parent, **kwargs) + self.sa = sa = QScrollArea() + l.addWidget(sa) + sa.setWidget(self.compare_widget) + sa.setWidgetResizable(True) + + self.bb = bb = QDialogButtonBox(QDialogButtonBox.Cancel) + bb.rejected.connect(self.reject) + self.aarb = b = bb.addButton(_('&Accept all remaining'), bb.YesRole) + if accept_all_tooltip: + b.setToolTip(accept_all_tooltip) + b.clicked.connect(self.accept_all_remaining) + self.rarb = b = bb.addButton(_('Re&ject all remaining'), bb.NoRole) + if reject_all_tooltip: + b.setToolTip(reject_all_tooltip) + b.clicked.connect(self.reject_all_remaining) + self.sb = b = bb.addButton(_('&Reject'), bb.ActionRole) + b.clicked.connect(partial(self.next_item, False)) + if skip_button_tooltip: + b.setToolTip(skip_button_tooltip) + self.nb = b = bb.addButton(_('&Next'), bb.ActionRole) + b.setIcon(QIcon(I('forward.png'))) + b.clicked.connect(partial(self.next_item, True)) + b.setDefault(True) + l.addWidget(bb) + + self.next_item(True) + + desktop = QApplication.instance().desktop() + geom = desktop.availableGeometry(parent or self) + width = max(700, min(950, geom.width()-50)) + height = max(650, min(1000, geom.height()-100)) + self.resize(QSize(width, height)) + geom = gprefs.get('diff_dialog_geom', None) + if geom is not None: + self.restoreGeometry(geom) + + def accept(self): + gprefs.set('diff_dialog_geom', bytearray(self.saveGeometry())) + super(CompareMany, self).accept() + + def reject(self): + gprefs.set('diff_dialog_geom', bytearray(self.saveGeometry())) + super(CompareMany, self).reject() + + @property + def current_mi(self): + return self.compare_widget.current_mi + + def next_item(self, accept): + if not self.ids: + return self.accept() + if self.current_mi is not None: + changed = self.compare_widget.apply_changes() + self.setWindowTitle(self.window_title + _(' [%(num)d of %(tot)d]') % dict( + num=(self.total - len(self.ids) + 1), tot=self.total)) + oldmi, newmi = self.get_metadata(self.ids[0]) + old_id = self.ids.pop(0) + if self.current_mi is not None: + self.accepted[old_id] = (changed, self.current_mi) if accept else (False, None) + self.compare_widget(oldmi, newmi) + + def accept_all_remaining(self): + self.next_item(True) + for id_ in self.ids: + oldmi, newmi = self.get_metadata(id_) + self.accepted[id_] = (False, newmi) + self.ids = [] + self.accept() + + def reject_all_remaining(self): + self.next_item(False) + for id_ in self.ids: + oldmi, newmi = self.get_metadata(id_) + self.accepted[id_] = (False, None) + self.ids = [] + self.accept() + +if __name__ == '__main__': + app = QApplication([]) + from calibre.library import db + db = db() + ids = sorted(db.all_ids(), reverse=True) + ids = [(x, ids[i+1]) for i, x in enumerate(ids[0::2])] + gm = partial(db.get_metadata, index_is_id=True, get_cover=True, cover_as_data=True) + get_metadata = lambda x:map(gm, ids[x]) + d = CompareMany(list(xrange(len(ids))), get_metadata, db.field_metadata) + if d.exec_() == d.Accepted: + for changed, mi in d.accepted.itervalues(): + if changed and mi is not None: + print (mi) diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 2b683da909..105b66a677 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -252,7 +252,7 @@ class FieldMetadata(dict): 'datatype':'int', 'is_multiple':{}, 'kind':'field', - 'name':None, + 'name':_('Cover'), 'search_terms':['cover'], 'is_custom':False, 'is_category':False,