mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
When merging books add an option to store discarded or replaced covers as an alternate cover in the data folder of the target book. Fixes #2071033 [[Enhancement] Merging Books: Copy cover of second book to data files](https://bugs.launchpad.net/calibre/+bug/2071033)
This commit is contained in:
parent
0c75bde03f
commit
d528bc6a35
@ -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]
|
||||
|
@ -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('<p>'+_(
|
||||
confirmed, save_alternate_cover = confirm_merge('<p>'+_(
|
||||
'Book formats and metadata from the selected books '
|
||||
'will be added to the <b>first selected book</b> (%s).<br> '
|
||||
'The second and subsequently selected books will not '
|
||||
'be deleted or changed.<br><br>'
|
||||
'Please confirm you want to proceed.')%title + '</p>',
|
||||
'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('<p>'+_(
|
||||
'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('<p>'+_(
|
||||
confirmed, save_alternate_cover = confirm_merge('<p>'+_(
|
||||
'Book formats and metadata from the selected books will be merged '
|
||||
'into the <b>first selected book</b> (%s).<br><br>'
|
||||
'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 <b>deleted</b> from your calibre library.<br><br> '
|
||||
'Are you <b>sure</b> you want to proceed?')%title + '</p>',
|
||||
'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):
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user