diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py
index e84b8b138d..069408474a 100644
--- a/src/calibre/db/cache.py
+++ b/src/calibre/db/cache.py
@@ -28,7 +28,7 @@ from calibre.customize.ui import run_plugins_on_import, run_plugins_on_postadd,
from calibre.db import SPOOL_SIZE, _get_next_series_num_for_list
from calibre.db.annotations import merge_annotations
from calibre.db.categories import get_categories
-from calibre.db.constants import NOTES_DIR_NAME
+from calibre.db.constants import COVER_FILE_NAME, DATA_DIR_NAME, NOTES_DIR_NAME
from calibre.db.errors import NoSuchBook, NoSuchFormat
from calibre.db.fields import IDENTITY, InvalidLinkTable, create_field
from calibre.db.lazy import FormatMetadata, FormatsList, ProxyMetadata
@@ -3346,12 +3346,13 @@ class Cache:
self.backend.copy_extra_file_to(book_id, path, relpath, stream_or_path)
@write_api
- def merge_book_metadata(self, dest_id, src_ids, replace_cover=False):
+ def merge_book_metadata(self, dest_id, src_ids, replace_cover=False, save_alternate_cover=False):
dest_mi = self.get_metadata(dest_id)
merged_identifiers = self._field_for('identifiers', dest_id) or {}
orig_dest_comments = dest_mi.comments
dest_cover = orig_dest_cover = self.cover(dest_id)
had_orig_cover = bool(dest_cover)
+ alternate_covers = []
from calibre.utils.date import is_date_undefined
def is_null_date(x):
@@ -3379,8 +3380,14 @@ class Cache:
if not dest_cover or replace_cover:
src_cover = self.cover(src_id)
if src_cover:
+ if save_alternate_cover and dest_cover:
+ alternate_covers.append(dest_cover)
dest_cover = src_cover
replace_cover = False
+ elif save_alternate_cover:
+ src_cover = self.cover(src_id)
+ if src_cover:
+ alternate_covers.append(src_cover)
if not dest_mi.publisher:
dest_mi.publisher = src_mi.publisher
if not dest_mi.rating:
@@ -3401,6 +3408,17 @@ class Cache:
if dest_cover and (not had_orig_cover or dest_cover is not orig_dest_cover):
self._set_cover({dest_id: dest_cover})
+ if alternate_covers:
+ existing = {x[0] for x in self._list_extra_files(dest_id)}
+ h, ext = os.path.splitext(COVER_FILE_NAME)
+ template = f'{DATA_DIR_NAME}/{h}-{{:03d}}{ext}'
+ for cdata in alternate_covers:
+ for i in range(1, 1000):
+ q = template.format(i)
+ if q not in existing:
+ existing.add(q)
+ self._add_extra_files(dest_id, {q: BytesIO(cdata)}, replace=False, auto_rename=True)
+ break
for key in self.field_metadata: # loop thru all defined fields
fm = self.field_metadata[key]
diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py
index 1fa5d55777..2ddcedf322 100644
--- a/src/calibre/gui2/actions/edit_metadata.py
+++ b/src/calibre/gui2/actions/edit_metadata.py
@@ -579,7 +579,7 @@ class EditMetadataAction(InterfaceActionWithLibraryDrop):
if d.merge_formats:
self.add_formats(dest_id, self.formats_for_ids(list(src_ids)))
if d.merge_metadata:
- self.merge_metadata(dest_id, src_ids, replace_cover=d.replace_cover)
+ self.merge_metadata(dest_id, src_ids, replace_cover=d.replace_cover, save_alternate_cover=d.save_alternate_cover)
if d.replace_cover:
covers_replaced = True
if d.delete_books:
@@ -613,16 +613,17 @@ class EditMetadataAction(InterfaceActionWithLibraryDrop):
title = mi.title
hpos = self.gui.library_view.horizontalScrollBar().value()
if safe_merge:
- if not confirm_merge('
'+_(
+ confirmed, save_alternate_cover = confirm_merge('
'+_(
'Book formats and metadata from the selected books '
'will be added to the first selected book (%s).
'
'The second and subsequently selected books will not '
'be deleted or changed.
'
'Please confirm you want to proceed.')%title + '
',
- 'merge_books_safe', self.gui, mi):
+ 'merge_books_safe', self.gui, mi, ask_about_save_alternate_cover=True)
+ if not confirmed:
return
self.add_formats(dest_id, self.formats_for_books(rows))
- self.merge_metadata(dest_id, src_ids)
+ self.merge_metadata(dest_id, src_ids, save_alternate_cover=save_alternate_cover)
elif merge_only_formats:
if not confirm_merge(''+_(
'Book formats from the selected books will be merged '
@@ -640,7 +641,7 @@ class EditMetadataAction(InterfaceActionWithLibraryDrop):
self.add_formats(dest_id, self.formats_for_books(rows))
self.delete_books_after_merge(src_ids)
else:
- if not confirm_merge('
'+_(
+ confirmed, save_alternate_cover = confirm_merge('
'+_(
'Book formats and metadata from the selected books will be merged '
'into the first selected book (%s).
'
'After being merged, the second and '
@@ -649,10 +650,11 @@ class EditMetadataAction(InterfaceActionWithLibraryDrop):
'and any duplicate formats in the second and subsequently selected books '
'will be permanently deleted from your calibre library.
'
'Are you sure you want to proceed?')%title + '
',
- 'merge_books', self.gui, mi):
+ 'merge_books', self.gui, mi, ask_about_save_alternate_cover=True)
+ if not confirmed:
return
self.add_formats(dest_id, self.formats_for_books(rows))
- self.merge_metadata(dest_id, src_ids)
+ self.merge_metadata(dest_id, src_ids, save_alternate_cover=save_alternate_cover)
self.merge_data_files(dest_id, src_ids)
self.delete_books_after_merge(src_ids)
# leave the selection highlight on first selected book
@@ -709,8 +711,8 @@ class EditMetadataAction(InterfaceActionWithLibraryDrop):
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, replace_cover=False):
- self.gui.current_db.new_api.merge_book_metadata(dest_id, src_ids, replace_cover)
+ def merge_metadata(self, dest_id, src_ids, replace_cover=False, save_alternate_cover=False):
+ self.gui.current_db.new_api.merge_book_metadata(dest_id, src_ids, replace_cover, save_alternate_cover=save_alternate_cover)
# }}}
def edit_device_collections(self, view, oncard=None):
diff --git a/src/calibre/gui2/dialogs/confirm_merge.py b/src/calibre/gui2/dialogs/confirm_merge.py
index 8b73037a94..f76601a28e 100644
--- a/src/calibre/gui2/dialogs/confirm_merge.py
+++ b/src/calibre/gui2/dialogs/confirm_merge.py
@@ -50,8 +50,9 @@ class Target(QTextBrowser):
class ConfirmMerge(Dialog):
- def __init__(self, msg, name, parent, mi):
+ def __init__(self, msg, name, parent, mi, ask_about_save_alternate_cover=False):
self.msg, self.mi, self.conf_name = msg, mi, name
+ self.ask_about_save_alternate_cover = ask_about_save_alternate_cover
Dialog.__init__(self, _('Are you sure?'), 'confirm-merge-dialog', parent)
needed, sz = self.sizeHint(), self.size()
if needed.width() > sz.width() or needed.height() > sz.height():
@@ -71,6 +72,13 @@ class ConfirmMerge(Dialog):
self.la = la = QLabel(self.msg)
la.setWordWrap(True)
l.addWidget(la)
+ self.save_alternate_cover_cb = c = QCheckBox(_('Save replaced or discarded &cover'), self)
+ c.setToolTip(_('Save the replaced or discarded cover in the data files associated with the target book as an alternate cover'))
+ c.setObjectName('choose-merge-cb-save_alternate_cover')
+ c.setChecked(bool(gprefs.get(c.objectName(), False)))
+ l.addWidget(c)
+ c.setVisible(self.ask_about_save_alternate_cover)
+ c.toggled.connect(self.alternate_covers_toggled)
self.confirm = c = QCheckBox(_('Show this confirmation again'), self)
c.setChecked(True)
c.stateChanged.connect(self.toggle)
@@ -79,6 +87,9 @@ class ConfirmMerge(Dialog):
self.right = r = Target(self.mi, self)
s.addWidget(r)
+ def alternate_covers_toggled(self):
+ gprefs.set(self.save_alternate_cover_cb.objectName(), self.save_alternate_cover_cb.isChecked())
+
def toggle(self):
dynamic[confirm_config_name(self.conf_name)] = self.confirm.isChecked()
@@ -88,12 +99,12 @@ class ConfirmMerge(Dialog):
return ans
-def confirm_merge(msg, name, parent, mi):
+def confirm_merge(msg, name, parent, mi, ask_about_save_alternate_cover=False):
config_set = dynamic
if not config_set.get(confirm_config_name(name), True):
return True
- d = ConfirmMerge(msg, name, parent, mi)
- return d.exec() == QDialog.DialogCode.Accepted
+ d = ConfirmMerge(msg, name, parent, mi, ask_about_save_alternate_cover)
+ return d.exec() == QDialog.DialogCode.Accepted, d.save_alternate_cover_cb.isChecked()
class ChooseMerge(Dialog):
@@ -116,11 +127,11 @@ class ChooseMerge(Dialog):
w.fl = fl = FlowLayout()
l.addLayout(fl)
- def cb(name, text, tt=''):
+ def cb(name, text, tt='', defval=True):
ans = QCheckBox(text)
fl.addWidget(ans)
prefs_key = ans.prefs_key = 'choose-merge-cb-' + name
- ans.setChecked(gprefs.get(prefs_key, True))
+ ans.setChecked(gprefs.get(prefs_key, defval))
connect_lambda(ans.stateChanged, self, lambda self, state: self.state_changed(getattr(self, name), state), type=Qt.ConnectionType.QueuedConnection)
if tt:
ans.setToolTip(tt)
@@ -135,6 +146,8 @@ class ChooseMerge(Dialog):
'Delete the selected books after merging'))
cb('replace_cover', _('Replace existing cover'), _(
'Replace the cover in the target book with the dragged cover'))
+ cb('save_alternate_cover', _('Save alternate cover'), _(
+ 'Save the replaced or discarded cover in the data files associated with the target book as an alternate cover'), defval=False)
l.addStretch(10)
self.msg = la = QLabel(self)
la.setWordWrap(True)
@@ -197,7 +210,7 @@ class ChooseMerge(Dialog):
def merge_type(self):
return MergeData(
self.merge_metadata.isChecked(), self.merge_formats.isChecked(), self.delete_books.isChecked(),
- self.replace_cover.isChecked(),
+ self.replace_cover.isChecked(), self.save_alternate_cover.isChecked(),
)
@@ -206,6 +219,7 @@ class MergeData(NamedTuple):
merge_formats: bool = False
delete_books: bool = False
replace_cover: bool = False
+ save_alternate_cover: bool = False
def merge_drop(dest_id, src_ids, gui):