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>'
__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)

View File

@ -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):