Bulk metadata download: Allow reviewing of the downloaded metadata before it is applied

This commit is contained in:
Kovid Goyal 2013-05-02 10:08:17 +05:30
parent 9ea1c45ab4
commit 403b12bb82
2 changed files with 124 additions and 29 deletions

View File

@ -5,10 +5,10 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, shutil import os, shutil, copy
from functools import partial 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 import error_dialog, Dispatcher, question_dialog
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog 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.dialogs.device_category_editor import DeviceCategoryEditor
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
from calibre.ebooks.metadata import authors_to_string 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.utils.icu import sort_key
from calibre.db.errors import NoSuchFormat from calibre.db.errors import NoSuchFormat
@ -147,14 +148,18 @@ class EditMetadataAction(InterfaceAction):
payload = (id_map, tdir, log_file, lm_map, payload = (id_map, tdir, log_file, lm_map,
failed_ids.union(failed_covers)) 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, log_file, _('Download log'), _('Download complete'), msg,
det_msg=det_msg, show_copy_button=show_copy_button, det_msg=det_msg, show_copy_button=show_copy_button,
cancel_callback=partial(self.cleanup_bulk_download, tdir), cancel_callback=partial(self.cleanup_bulk_download, tdir),
log_is_file=True, checkbox_msg=checkbox_msg, 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 good_ids, tdir, log_file, lm_map, failed_ids = payload
if not good_ids: if not good_ids:
return return
@ -194,6 +199,57 @@ class EditMetadataAction(InterfaceAction):
cov = None cov = None
id_map[bid] = (opf, cov) 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]) restrict_to_failed = bool(args and args[0])
if restrict_to_failed: if restrict_to_failed:
db.data.set_marked_ids(failed_ids) db.data.set_marked_ids(failed_ids)

View File

@ -78,7 +78,13 @@ class LineEdit(QLineEdit):
@property @property
def is_blank(self): 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): class LanguagesEdit(LE):
@ -111,6 +117,9 @@ class LanguagesEdit(LE):
def is_blank(self): def is_blank(self):
return not self.current_val return not self.current_val
def same_as(self, other):
return self.current_val == other.current_val
class RatingsEdit(RatingEdit): class RatingsEdit(RatingEdit):
changed = pyqtSignal() changed = pyqtSignal()
@ -135,6 +144,9 @@ class RatingsEdit(RatingEdit):
def is_blank(self): def is_blank(self):
return self.value() == 0 return self.value() == 0
def same_as(self, other):
return self.current_val == other.current_val
class DateEdit(PubdateEdit): class DateEdit(PubdateEdit):
changed = pyqtSignal() changed = pyqtSignal()
@ -157,7 +169,10 @@ class DateEdit(PubdateEdit):
@property @property
def is_blank(self): 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): class SeriesEdit(LineEdit):
@ -229,6 +244,9 @@ class CommentsEdit(Editor):
def is_blank(self): def is_blank(self):
return not self.current_val.strip() return not self.current_val.strip()
def same_as(self, other):
return self.current_val == other.current_val
class CoverView(QWidget): class CoverView(QWidget):
changed = pyqtSignal() changed = pyqtSignal()
@ -288,6 +306,9 @@ class CoverView(QWidget):
pt.write(pixmap_to_data(self.pixmap)) pt.write(pixmap_to_data(self.pixmap))
mi.cover = pt.name mi.cover = pt.name
def same_as(self, other):
return self.current_val == other.current_val
def sizeHint(self): def sizeHint(self):
return QSize(225, 300) return QSize(225, 300)
@ -383,7 +404,7 @@ class CompareSingle(QWidget):
def changed(self, field): def changed(self, field):
w = self.widgets[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) w.label.setFont(self.changed_font)
else: else:
w.label.setFont(QApplication.font()) w.label.setFont(QApplication.font())
@ -412,8 +433,14 @@ class CompareSingle(QWidget):
class CompareMany(QDialog): class CompareMany(QDialog):
def __init__(self, ids, get_metadata, field_metadata, parent=None, window_title=None, skip_button_tooltip=None, def __init__(self, ids, get_metadata, field_metadata, parent=None,
accept_all_tooltip=None, reject_all_tooltip=None, **kwargs): 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) QDialog.__init__(self, parent)
self.l = l = QVBoxLayout() self.l = l = QVBoxLayout()
self.setLayout(l) self.setLayout(l)
@ -424,7 +451,12 @@ class CompareMany(QDialog):
self.accepted = OrderedDict() self.accepted = OrderedDict()
self.window_title = window_title or _('Compare metadata') 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() self.sa = sa = QScrollArea()
l.addWidget(sa) l.addWidget(sa)
sa.setWidget(self.compare_widget) sa.setWidget(self.compare_widget)
@ -432,20 +464,24 @@ class CompareMany(QDialog):
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Cancel) self.bb = bb = QDialogButtonBox(QDialogButtonBox.Cancel)
bb.rejected.connect(self.reject) bb.rejected.connect(self.reject)
self.aarb = b = bb.addButton(_('&Accept all remaining'), bb.YesRole) if self.total > 1:
if accept_all_tooltip: self.aarb = b = bb.addButton(_('&Accept all remaining'), bb.YesRole)
b.setToolTip(accept_all_tooltip) b.setIcon(QIcon(I('ok.png')))
b.clicked.connect(self.accept_all_remaining) if accept_all_tooltip:
self.rarb = b = bb.addButton(_('Re&ject all remaining'), bb.NoRole) b.setToolTip(accept_all_tooltip)
if reject_all_tooltip: b.clicked.connect(self.accept_all_remaining)
b.setToolTip(reject_all_tooltip) self.rarb = b = bb.addButton(_('Re&ject all remaining'), bb.NoRole)
b.clicked.connect(self.reject_all_remaining) b.setIcon(QIcon(I('minus.png')))
self.sb = b = bb.addButton(_('&Reject'), bb.ActionRole) if reject_all_tooltip:
b.clicked.connect(partial(self.next_item, False)) b.setToolTip(reject_all_tooltip)
if skip_button_tooltip: b.clicked.connect(self.reject_all_remaining)
b.setToolTip(skip_button_tooltip) self.sb = b = bb.addButton(_('&Reject'), bb.ActionRole)
self.nb = b = bb.addButton(_('&Next'), bb.ActionRole) b.clicked.connect(partial(self.next_item, False))
b.setIcon(QIcon(I('forward.png'))) 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.clicked.connect(partial(self.next_item, True))
b.setDefault(True) b.setDefault(True)
l.addWidget(bb) l.addWidget(bb)
@ -460,6 +496,7 @@ class CompareMany(QDialog):
geom = gprefs.get('diff_dialog_geom', None) geom = gprefs.get('diff_dialog_geom', None)
if geom is not None: if geom is not None:
self.restoreGeometry(geom) self.restoreGeometry(geom)
b.setFocus(Qt.OtherFocusReason)
def accept(self): def accept(self):
gprefs.set('diff_dialog_geom', bytearray(self.saveGeometry())) gprefs.set('diff_dialog_geom', bytearray(self.saveGeometry()))
@ -478,12 +515,14 @@ class CompareMany(QDialog):
return self.accept() return self.accept()
if self.current_mi is not None: if self.current_mi is not None:
changed = self.compare_widget.apply_changes() 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( self.setWindowTitle(self.window_title + _(' [%(num)d of %(tot)d]') % dict(
num=(self.total - len(self.ids) + 1), tot=self.total)) num=(self.total - len(self.ids) + 1), tot=self.total))
oldmi, newmi = self.get_metadata(self.ids[0]) 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) self.compare_widget(oldmi, newmi)
def accept_all_remaining(self): def accept_all_remaining(self):