When merging books by drag-and-drop add an option to use the dragged cover instead of the cover in the target book. Fixes #2027794 [Merging books keeps metadata of first book but the cover of the of the second book](https://bugs.launchpad.net/calibre/+bug/2027794)

This commit is contained in:
Kovid Goyal 2023-08-17 13:27:51 +05:30
parent df7a0b3da3
commit 2ae0742c98
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 45 additions and 16 deletions

View File

@ -588,22 +588,27 @@ class EditMetadataAction(InterfaceAction):
'merge_too_many_books', self.gui)
def books_dropped(self, merge_map):
covers_replaced = False
for dest_id, src_ids in iteritems(merge_map):
if not self.confirm_large_merge(len(src_ids) + 1):
continue
from calibre.gui2.dialogs.confirm_merge import merge_drop
merge_metadata, merge_formats, delete_books = merge_drop(dest_id, src_ids, self.gui)
if merge_metadata is None:
d = merge_drop(dest_id, src_ids, self.gui)
if d is None:
return
if merge_formats:
if d.merge_formats:
self.add_formats(dest_id, self.formats_for_ids(list(src_ids)))
if merge_metadata:
self.merge_metadata(dest_id, src_ids)
if delete_books:
if d.merge_metadata:
self.merge_metadata(dest_id, src_ids, replace_cover=d.replace_cover)
if d.replace_cover:
covers_replaced = True
if d.delete_books:
self.delete_books_after_merge(src_ids)
# leave the selection highlight on the target book
row = self.gui.library_view.ids_to_rows([dest_id])[dest_id]
self.gui.library_view.set_current_row(row)
if covers_replaced:
self.gui.refresh_cover_browser()
def merge_books(self, safe_merge=False, merge_only_formats=False):
'''
@ -724,12 +729,12 @@ class EditMetadataAction(InterfaceAction):
def delete_books_after_merge(self, ids_to_delete):
self.gui.library_view.model().delete_books_by_id(ids_to_delete)
def merge_metadata(self, dest_id, src_ids):
def merge_metadata(self, dest_id, src_ids, replace_cover=False):
db = self.gui.library_view.model().db
dest_mi = db.get_metadata(dest_id, index_is_id=True)
merged_identifiers = db.get_identifiers(dest_id, index_is_id=True)
orig_dest_comments = dest_mi.comments
dest_cover = db.cover(dest_id, index_is_id=True)
dest_cover = orig_dest_cover = db.cover(dest_id, index_is_id=True)
had_orig_cover = bool(dest_cover)
def is_null_date(x):
@ -754,10 +759,11 @@ class EditMetadataAction(InterfaceAction):
dest_mi.tags = src_mi.tags
else:
dest_mi.tags.extend(src_mi.tags)
if not dest_cover:
if not dest_cover or replace_cover:
src_cover = db.cover(src_id, index_is_id=True)
if src_cover:
dest_cover = src_cover
replace_cover = False
if not dest_mi.publisher:
dest_mi.publisher = src_mi.publisher
if not dest_mi.rating:
@ -776,7 +782,7 @@ class EditMetadataAction(InterfaceAction):
dest_mi.set_identifiers(merged_identifiers)
db.set_metadata(dest_id, dest_mi, ignore_errors=False)
if not had_orig_cover and dest_cover:
if dest_cover and (not had_orig_cover or dest_cover is not orig_dest_cover):
db.set_cover(dest_id, dest_cover)
for key in db.field_metadata: # loop thru all defined fields

View File

@ -4,6 +4,8 @@
__license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
from typing import NamedTuple
from qt.core import (
QCheckBox, QDialog, QDialogButtonBox, QLabel, QSplitter, Qt, QTextBrowser,
QVBoxLayout, QWidget,
@ -13,7 +15,7 @@ from calibre.ebooks.metadata import authors_to_string
from calibre.ebooks.metadata.book.base import field_metadata
from calibre.gui2 import dynamic, gprefs
from calibre.gui2.dialogs.confirm_delete import confirm_config_name
from calibre.gui2.widgets2 import Dialog
from calibre.gui2.widgets2 import Dialog, FlowLayout
from calibre.startup import connect_lambda
from calibre.utils.config import tweaks
from calibre.utils.date import format_date
@ -35,10 +37,12 @@ class Target(QTextBrowser):
<tr><td>{fm[pubdate][name]}:</td><td>{published}</td></tr>
<tr><td>{fm[formats][name]}:</td><td>{formats}</td></tr>
<tr><td>{fm[series][name]}:</td><td>{series}</td></tr>
<tr><td>{has_cover_title}:</td><td>{has_cover}</td></tr>
</table>
'''.format(
mb=_('Target book'),
title=mi.title,
has_cover_title=_('Has cover'), has_cover=_('Yes') if mi.has_cover else _('No'),
authors=authors_to_string(mi.authors),
date=format_date(mi.timestamp, tweaks['gui_timestamp_display_format']), fm=fm,
published=(format_date(mi.pubdate, tweaks['gui_pubdate_display_format']) if mi.pubdate else ''),
@ -112,10 +116,12 @@ class ChooseMerge(Dialog):
s.addWidget(w)
w.l = l = QVBoxLayout(w)
l.setContentsMargins(0, 0, 0, 0)
w.fl = fl = FlowLayout()
l.addLayout(fl)
def cb(name, text, tt=''):
ans = QCheckBox(text)
l.addWidget(ans)
fl.addWidget(ans)
prefs_key = ans.prefs_key = 'choose-merge-cb-' + name
ans.setChecked(gprefs.get(prefs_key, True))
connect_lambda(ans.stateChanged, self, lambda self, state: self.state_changed(getattr(self, name), state), type=Qt.ConnectionType.QueuedConnection)
@ -130,6 +136,8 @@ class ChooseMerge(Dialog):
'Merge the book files of the selected books into the target book'))
cb('delete_books', _('Delete merged books'), _(
'Delete the selected books after merging'))
cb('replace_cover', _('Replace existing cover'), _(
'Replace the cover in the target book with the dragged cover'))
l.addStretch(10)
self.msg = la = QLabel(self)
la.setWordWrap(True)
@ -151,21 +159,26 @@ class ChooseMerge(Dialog):
mm = self.merge_metadata.isChecked()
mf = self.merge_formats.isChecked()
rm = self.delete_books.isChecked()
rc = self.replace_cover.isChecked()
msg = '<p>'
if mm and mf:
msg += _(
'Book formats and metadata from the selected books'
' will be merged into the target book ({title}).')
if rc or not self.mi.has_cover:
msg += ' ' + _('The dragged cover will be used.')
elif mf:
msg += _('Book formats from the selected books '
'will be merged into to the target book ({title}).'
' Metadata in the target book will not be changed.')
' Metadata and cover in the target book will not be changed.')
elif mm:
msg += _('Metadata from the selected books '
'will be merged into to the target book ({title}).'
' Formats will not be merged.')
if rc or not self.mi.has_cover:
msg += ' ' + _('The dragged cover will be used.')
msg += '<br>'
msg += _('All book formats of the first selected book will be kept.') + '<br><br>'
msg += _('All book formats of the target book will be kept.') + '<br><br>'
if rm:
msg += _('After being merged, the selected books will be <b>deleted</b>.')
if mf:
@ -185,11 +198,21 @@ class ChooseMerge(Dialog):
@property
def merge_type(self):
return self.merge_metadata.isChecked(), self.merge_formats.isChecked(), self.delete_books.isChecked()
return MergeData(
self.merge_metadata.isChecked(), self.merge_formats.isChecked(), self.delete_books.isChecked(),
self.replace_cover.isChecked(),
)
class MergeData(NamedTuple):
merge_metadata: bool = False
merge_formats: bool = False
delete_books: bool = False
replace_cover: bool = False
def merge_drop(dest_id, src_ids, gui):
d = ChooseMerge(dest_id, src_ids, gui)
if d.exec() != QDialog.DialogCode.Accepted:
return None, None, None
return None
return d.merge_type