mirror of
				https://github.com/kovidgoyal/calibre.git
				synced 2025-11-04 03:27:00 -05: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