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):