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

View File

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