mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Bulk metadata download: Allow reviewing of the downloaded metadata before it is applied
This commit is contained in:
parent
9ea1c45ab4
commit
403b12bb82
@ -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)
|
||||||
|
@ -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)
|
||||||
|
if self.total > 1:
|
||||||
self.aarb = b = bb.addButton(_('&Accept all remaining'), bb.YesRole)
|
self.aarb = b = bb.addButton(_('&Accept all remaining'), bb.YesRole)
|
||||||
|
b.setIcon(QIcon(I('ok.png')))
|
||||||
if accept_all_tooltip:
|
if accept_all_tooltip:
|
||||||
b.setToolTip(accept_all_tooltip)
|
b.setToolTip(accept_all_tooltip)
|
||||||
b.clicked.connect(self.accept_all_remaining)
|
b.clicked.connect(self.accept_all_remaining)
|
||||||
self.rarb = b = bb.addButton(_('Re&ject all remaining'), bb.NoRole)
|
self.rarb = b = bb.addButton(_('Re&ject all remaining'), bb.NoRole)
|
||||||
|
b.setIcon(QIcon(I('minus.png')))
|
||||||
if reject_all_tooltip:
|
if reject_all_tooltip:
|
||||||
b.setToolTip(reject_all_tooltip)
|
b.setToolTip(reject_all_tooltip)
|
||||||
b.clicked.connect(self.reject_all_remaining)
|
b.clicked.connect(self.reject_all_remaining)
|
||||||
self.sb = b = bb.addButton(_('&Reject'), bb.ActionRole)
|
self.sb = b = bb.addButton(_('&Reject'), bb.ActionRole)
|
||||||
b.clicked.connect(partial(self.next_item, False))
|
b.clicked.connect(partial(self.next_item, False))
|
||||||
if skip_button_tooltip:
|
b.setIcon(QIcon(I('minus.png')))
|
||||||
b.setToolTip(skip_button_tooltip)
|
if reject_button_tooltip:
|
||||||
self.nb = b = bb.addButton(_('&Next'), bb.ActionRole)
|
b.setToolTip(reject_button_tooltip)
|
||||||
b.setIcon(QIcon(I('forward.png')))
|
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):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user