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>'
|
||||
__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)
|
||||
|
@ -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)
|
||||
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))
|
||||
if skip_button_tooltip:
|
||||
b.setToolTip(skip_button_tooltip)
|
||||
self.nb = b = bb.addButton(_('&Next'), bb.ActionRole)
|
||||
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.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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user