From 403b12bb82c15d41d12a5e02f25d66135ed2b29e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 2 May 2013 10:08:17 +0530 Subject: [PATCH] Bulk metadata download: Allow reviewing of the downloaded metadata before it is applied --- src/calibre/gui2/actions/edit_metadata.py | 68 ++++++++++++++++-- src/calibre/gui2/metadata/diff.py | 85 +++++++++++++++++------ 2 files changed, 124 insertions(+), 29 deletions(-) diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index 485bc5bf90..19c7ee127e 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -5,10 +5,10 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os, shutil +import os, shutil, copy from functools import partial -from PyQt4.Qt import QMenu, QModelIndex, QTimer +from PyQt4.Qt import QMenu, QModelIndex, QTimer, QIcon from calibre.gui2 import error_dialog, Dispatcher, question_dialog from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog @@ -16,7 +16,8 @@ from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.device_category_editor import DeviceCategoryEditor from calibre.gui2.actions import InterfaceAction from calibre.ebooks.metadata import authors_to_string -from calibre.ebooks.metadata.opf2 import OPF +from calibre.ebooks.metadata.book.base import Metadata +from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf from calibre.utils.icu import sort_key from calibre.db.errors import NoSuchFormat @@ -147,14 +148,18 @@ class EditMetadataAction(InterfaceAction): payload = (id_map, tdir, log_file, lm_map, failed_ids.union(failed_covers)) - self.gui.proceed_question(self.apply_downloaded_metadata, payload, + review_apply = partial(self.apply_downloaded_metadata, True) + normal_apply = partial(self.apply_downloaded_metadata, False) + self.gui.proceed_question(normal_apply, payload, log_file, _('Download log'), _('Download complete'), msg, det_msg=det_msg, show_copy_button=show_copy_button, cancel_callback=partial(self.cleanup_bulk_download, tdir), log_is_file=True, checkbox_msg=checkbox_msg, - checkbox_checked=False) + checkbox_checked=False, action_callback=review_apply, + action_label=_('Review downloaded metadata'), + action_icon=QIcon(I('auto_author_sort.png'))) - def apply_downloaded_metadata(self, payload, *args): + def apply_downloaded_metadata(self, review, payload, *args): good_ids, tdir, log_file, lm_map, failed_ids = payload if not good_ids: return @@ -194,6 +199,57 @@ class EditMetadataAction(InterfaceAction): cov = None id_map[bid] = (opf, cov) + if review: + def get_metadata(book_id): + oldmi = db.get_metadata(book_id, index_is_id=True, get_cover=True, cover_as_data=True) + opf, cov = id_map[book_id] + if opf is None: + newmi = Metadata(oldmi.title, authors=tuple(oldmi.authors)) + else: + with open(opf, 'rb') as f: + newmi = OPF(f, basedir=os.path.dirname(opf), populate_spine=False).to_book_metadata() + newmi.cover, newmi.cover_data = None, (None, None) + for x in ('title', 'authors'): + if newmi.is_null(x): + # Title and author are set to null if they are + # the same as the originals as an optimization, + # we undo that, as it is confusing. + newmi.set(x, copy.copy(oldmi.get(x))) + if cov: + with open(cov, 'rb') as f: + newmi.cover_data = ('jpg', f.read()) + return oldmi, newmi + from calibre.gui2.metadata.diff import CompareMany + d = CompareMany( + set(id_map), get_metadata, db.field_metadata, parent=self.gui, + window_title=_('Review downloaded metadata'), + reject_button_tooltip=_('Discard downloaded metadata for this book'), + accept_all_tooltip=_('Use the downloaded metadata for all remaining books'), + reject_all_tooltip=_('Discard downloaded metadata for all remaining books'), + revert_tooltip=_('Discard the downloaded value for: %s'), + intro_msg=_('The downloaded metadata is on the left and the original metadata' + ' is on the right. If a downloaded value is blank or unknown,' + ' the original value is used.') + ) + if d.exec_() == d.Accepted: + nid_map = {} + for book_id, (changed, mi) in d.accepted.iteritems(): + if mi is None: # discarded + continue + if changed: + opf, cov = id_map[book_id] + cfile = mi.cover + mi.cover, mi.cover_data = None, (None, None) + with open(opf, 'wb') as f: + f.write(metadata_to_opf(mi)) + if cfile: + shutil.copyfile(cfile, cov) + os.remove(cfile) + nid_map[book_id] = id_map[book_id] + id_map = nid_map + else: + id_map = {} + restrict_to_failed = bool(args and args[0]) if restrict_to_failed: db.data.set_marked_ids(failed_ids) diff --git a/src/calibre/gui2/metadata/diff.py b/src/calibre/gui2/metadata/diff.py index 5a024fae30..477743aa7a 100644 --- a/src/calibre/gui2/metadata/diff.py +++ b/src/calibre/gui2/metadata/diff.py @@ -78,7 +78,13 @@ class LineEdit(QLineEdit): @property def is_blank(self): - return not self.current_val.strip() + val = self.current_val.strip() + if self.field in {'title', 'authors'}: + return val in {'', _('Unknown')} + return not val + + def same_as(self, other): + return self.current_val == other.current_val class LanguagesEdit(LE): @@ -111,6 +117,9 @@ class LanguagesEdit(LE): def is_blank(self): return not self.current_val + def same_as(self, other): + return self.current_val == other.current_val + class RatingsEdit(RatingEdit): changed = pyqtSignal() @@ -135,6 +144,9 @@ class RatingsEdit(RatingEdit): def is_blank(self): return self.value() == 0 + def same_as(self, other): + return self.current_val == other.current_val + class DateEdit(PubdateEdit): changed = pyqtSignal() @@ -157,7 +169,10 @@ class DateEdit(PubdateEdit): @property def is_blank(self): - return self.current_val == UNDEFINED_DATE + return self.current_val.year <= UNDEFINED_DATE.year + + def same_as(self, other): + return self.text() == other.text() class SeriesEdit(LineEdit): @@ -229,6 +244,9 @@ class CommentsEdit(Editor): def is_blank(self): return not self.current_val.strip() + def same_as(self, other): + return self.current_val == other.current_val + class CoverView(QWidget): changed = pyqtSignal() @@ -288,6 +306,9 @@ class CoverView(QWidget): pt.write(pixmap_to_data(self.pixmap)) mi.cover = pt.name + def same_as(self, other): + return self.current_val == other.current_val + def sizeHint(self): return QSize(225, 300) @@ -383,7 +404,7 @@ class CompareSingle(QWidget): 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): + if not w.new.same_as(w.old) and (not self.blank_as_equal or not w.new.is_blank): w.label.setFont(self.changed_font) else: w.label.setFont(QApplication.font()) @@ -412,8 +433,14 @@ class CompareSingle(QWidget): 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): + def __init__(self, ids, get_metadata, field_metadata, parent=None, + window_title=None, + reject_button_tooltip=None, + accept_all_tooltip=None, + reject_all_tooltip=None, + revert_tooltip=None, + intro_msg=None, + **kwargs): QDialog.__init__(self, parent) self.l = l = QVBoxLayout() self.setLayout(l) @@ -424,7 +451,12 @@ class CompareMany(QDialog): self.accepted = OrderedDict() self.window_title = window_title or _('Compare metadata') - self.compare_widget = CompareSingle(field_metadata, parent=parent, **kwargs) + if intro_msg: + self.la = la = QLabel(intro_msg) + la.setWordWrap(True) + l.addWidget(la) + + self.compare_widget = CompareSingle(field_metadata, parent=parent, revert_tooltip=revert_tooltip, **kwargs) self.sa = sa = QScrollArea() l.addWidget(sa) sa.setWidget(self.compare_widget) @@ -432,20 +464,24 @@ class CompareMany(QDialog): 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'))) + if self.total > 1: + self.aarb = b = bb.addButton(_('&Accept all remaining'), bb.YesRole) + b.setIcon(QIcon(I('ok.png'))) + 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) + b.setIcon(QIcon(I('minus.png'))) + 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)) + b.setIcon(QIcon(I('minus.png'))) + if reject_button_tooltip: + b.setToolTip(reject_button_tooltip) + self.nb = b = bb.addButton(_('&Next') if self.total > 1 else _('&OK'), bb.ActionRole) + b.setIcon(QIcon(I('forward.png' if self.total > 1 else 'ok.png'))) b.clicked.connect(partial(self.next_item, True)) b.setDefault(True) l.addWidget(bb) @@ -460,6 +496,7 @@ class CompareMany(QDialog): geom = gprefs.get('diff_dialog_geom', None) if geom is not None: self.restoreGeometry(geom) + b.setFocus(Qt.OtherFocusReason) def accept(self): gprefs.set('diff_dialog_geom', bytearray(self.saveGeometry())) @@ -478,12 +515,14 @@ class CompareMany(QDialog): return self.accept() if self.current_mi is not None: changed = self.compare_widget.apply_changes() + if self.current_mi is not None: + old_id = self.ids.pop(0) + self.accepted[old_id] = (changed, self.current_mi) if accept else (False, None) + if not self.ids: + return self.accept() 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):