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 import SPOOL_SIZE, _get_next_series_num_for_list
|
||||||
from calibre.db.annotations import merge_annotations
|
from calibre.db.annotations import merge_annotations
|
||||||
from calibre.db.categories import get_categories
|
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.errors import NoSuchBook, NoSuchFormat
|
||||||
from calibre.db.fields import IDENTITY, InvalidLinkTable, create_field
|
from calibre.db.fields import IDENTITY, InvalidLinkTable, create_field
|
||||||
from calibre.db.lazy import FormatMetadata, FormatsList, ProxyMetadata
|
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)
|
self.backend.copy_extra_file_to(book_id, path, relpath, stream_or_path)
|
||||||
|
|
||||||
@write_api
|
@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)
|
dest_mi = self.get_metadata(dest_id)
|
||||||
merged_identifiers = self._field_for('identifiers', dest_id) or {}
|
merged_identifiers = self._field_for('identifiers', dest_id) or {}
|
||||||
orig_dest_comments = dest_mi.comments
|
orig_dest_comments = dest_mi.comments
|
||||||
dest_cover = orig_dest_cover = self.cover(dest_id)
|
dest_cover = orig_dest_cover = self.cover(dest_id)
|
||||||
had_orig_cover = bool(dest_cover)
|
had_orig_cover = bool(dest_cover)
|
||||||
|
alternate_covers = []
|
||||||
from calibre.utils.date import is_date_undefined
|
from calibre.utils.date import is_date_undefined
|
||||||
|
|
||||||
def is_null_date(x):
|
def is_null_date(x):
|
||||||
@ -3379,8 +3380,14 @@ class Cache:
|
|||||||
if not dest_cover or replace_cover:
|
if not dest_cover or replace_cover:
|
||||||
src_cover = self.cover(src_id)
|
src_cover = self.cover(src_id)
|
||||||
if src_cover:
|
if src_cover:
|
||||||
|
if save_alternate_cover and dest_cover:
|
||||||
|
alternate_covers.append(dest_cover)
|
||||||
dest_cover = src_cover
|
dest_cover = src_cover
|
||||||
replace_cover = False
|
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:
|
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:
|
||||||
@ -3401,6 +3408,17 @@ class Cache:
|
|||||||
|
|
||||||
if dest_cover and (not had_orig_cover or dest_cover is not orig_dest_cover):
|
if dest_cover and (not had_orig_cover or dest_cover is not orig_dest_cover):
|
||||||
self._set_cover({dest_id: 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
|
for key in self.field_metadata: # loop thru all defined fields
|
||||||
fm = self.field_metadata[key]
|
fm = self.field_metadata[key]
|
||||||
|
@ -579,7 +579,7 @@ class EditMetadataAction(InterfaceActionWithLibraryDrop):
|
|||||||
if d.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 d.merge_metadata:
|
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:
|
if d.replace_cover:
|
||||||
covers_replaced = True
|
covers_replaced = True
|
||||||
if d.delete_books:
|
if d.delete_books:
|
||||||
@ -613,16 +613,17 @@ class EditMetadataAction(InterfaceActionWithLibraryDrop):
|
|||||||
title = mi.title
|
title = mi.title
|
||||||
hpos = self.gui.library_view.horizontalScrollBar().value()
|
hpos = self.gui.library_view.horizontalScrollBar().value()
|
||||||
if safe_merge:
|
if safe_merge:
|
||||||
if not confirm_merge('<p>'+_(
|
confirmed, save_alternate_cover = confirm_merge('<p>'+_(
|
||||||
'Book formats and metadata from the selected books '
|
'Book formats and metadata from the selected books '
|
||||||
'will be added to the <b>first selected book</b> (%s).<br> '
|
'will be added to the <b>first selected book</b> (%s).<br> '
|
||||||
'The second and subsequently selected books will not '
|
'The second and subsequently selected books will not '
|
||||||
'be deleted or changed.<br><br>'
|
'be deleted or changed.<br><br>'
|
||||||
'Please confirm you want to proceed.')%title + '</p>',
|
'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
|
return
|
||||||
self.add_formats(dest_id, self.formats_for_books(rows))
|
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:
|
elif merge_only_formats:
|
||||||
if not confirm_merge('<p>'+_(
|
if not confirm_merge('<p>'+_(
|
||||||
'Book formats from the selected books will be merged '
|
'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.add_formats(dest_id, self.formats_for_books(rows))
|
||||||
self.delete_books_after_merge(src_ids)
|
self.delete_books_after_merge(src_ids)
|
||||||
else:
|
else:
|
||||||
if not confirm_merge('<p>'+_(
|
confirmed, save_alternate_cover = confirm_merge('<p>'+_(
|
||||||
'Book formats and metadata from the selected books will be merged '
|
'Book formats and metadata from the selected books will be merged '
|
||||||
'into the <b>first selected book</b> (%s).<br><br>'
|
'into the <b>first selected book</b> (%s).<br><br>'
|
||||||
'After being merged, the second and '
|
'After being merged, the second and '
|
||||||
@ -649,10 +650,11 @@ class EditMetadataAction(InterfaceActionWithLibraryDrop):
|
|||||||
'and any duplicate formats in the second and subsequently selected books '
|
'and any duplicate formats in the second and subsequently selected books '
|
||||||
'will be permanently <b>deleted</b> from your calibre library.<br><br> '
|
'will be permanently <b>deleted</b> from your calibre library.<br><br> '
|
||||||
'Are you <b>sure</b> you want to proceed?')%title + '</p>',
|
'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
|
return
|
||||||
self.add_formats(dest_id, self.formats_for_books(rows))
|
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.merge_data_files(dest_id, src_ids)
|
||||||
self.delete_books_after_merge(src_ids)
|
self.delete_books_after_merge(src_ids)
|
||||||
# leave the selection highlight on first selected book
|
# leave the selection highlight on first selected book
|
||||||
@ -709,8 +711,8 @@ class EditMetadataAction(InterfaceActionWithLibraryDrop):
|
|||||||
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, replace_cover=False):
|
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)
|
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):
|
def edit_device_collections(self, view, oncard=None):
|
||||||
|
@ -50,8 +50,9 @@ class Target(QTextBrowser):
|
|||||||
|
|
||||||
class ConfirmMerge(Dialog):
|
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.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)
|
Dialog.__init__(self, _('Are you sure?'), 'confirm-merge-dialog', parent)
|
||||||
needed, sz = self.sizeHint(), self.size()
|
needed, sz = self.sizeHint(), self.size()
|
||||||
if needed.width() > sz.width() or needed.height() > sz.height():
|
if needed.width() > sz.width() or needed.height() > sz.height():
|
||||||
@ -71,6 +72,13 @@ class ConfirmMerge(Dialog):
|
|||||||
self.la = la = QLabel(self.msg)
|
self.la = la = QLabel(self.msg)
|
||||||
la.setWordWrap(True)
|
la.setWordWrap(True)
|
||||||
l.addWidget(la)
|
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)
|
self.confirm = c = QCheckBox(_('Show this confirmation again'), self)
|
||||||
c.setChecked(True)
|
c.setChecked(True)
|
||||||
c.stateChanged.connect(self.toggle)
|
c.stateChanged.connect(self.toggle)
|
||||||
@ -79,6 +87,9 @@ class ConfirmMerge(Dialog):
|
|||||||
self.right = r = Target(self.mi, self)
|
self.right = r = Target(self.mi, self)
|
||||||
s.addWidget(r)
|
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):
|
def toggle(self):
|
||||||
dynamic[confirm_config_name(self.conf_name)] = self.confirm.isChecked()
|
dynamic[confirm_config_name(self.conf_name)] = self.confirm.isChecked()
|
||||||
|
|
||||||
@ -88,12 +99,12 @@ class ConfirmMerge(Dialog):
|
|||||||
return ans
|
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
|
config_set = dynamic
|
||||||
if not config_set.get(confirm_config_name(name), True):
|
if not config_set.get(confirm_config_name(name), True):
|
||||||
return True
|
return True
|
||||||
d = ConfirmMerge(msg, name, parent, mi)
|
d = ConfirmMerge(msg, name, parent, mi, ask_about_save_alternate_cover)
|
||||||
return d.exec() == QDialog.DialogCode.Accepted
|
return d.exec() == QDialog.DialogCode.Accepted, d.save_alternate_cover_cb.isChecked()
|
||||||
|
|
||||||
|
|
||||||
class ChooseMerge(Dialog):
|
class ChooseMerge(Dialog):
|
||||||
@ -116,11 +127,11 @@ class ChooseMerge(Dialog):
|
|||||||
w.fl = fl = FlowLayout()
|
w.fl = fl = FlowLayout()
|
||||||
l.addLayout(fl)
|
l.addLayout(fl)
|
||||||
|
|
||||||
def cb(name, text, tt=''):
|
def cb(name, text, tt='', defval=True):
|
||||||
ans = QCheckBox(text)
|
ans = QCheckBox(text)
|
||||||
fl.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, defval))
|
||||||
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)
|
||||||
if tt:
|
if tt:
|
||||||
ans.setToolTip(tt)
|
ans.setToolTip(tt)
|
||||||
@ -135,6 +146,8 @@ class ChooseMerge(Dialog):
|
|||||||
'Delete the selected books after merging'))
|
'Delete the selected books after merging'))
|
||||||
cb('replace_cover', _('Replace existing cover'), _(
|
cb('replace_cover', _('Replace existing cover'), _(
|
||||||
'Replace the cover in the target book with the dragged 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)
|
l.addStretch(10)
|
||||||
self.msg = la = QLabel(self)
|
self.msg = la = QLabel(self)
|
||||||
la.setWordWrap(True)
|
la.setWordWrap(True)
|
||||||
@ -197,7 +210,7 @@ class ChooseMerge(Dialog):
|
|||||||
def merge_type(self):
|
def merge_type(self):
|
||||||
return MergeData(
|
return MergeData(
|
||||||
self.merge_metadata.isChecked(), self.merge_formats.isChecked(), self.delete_books.isChecked(),
|
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
|
merge_formats: bool = False
|
||||||
delete_books: bool = False
|
delete_books: bool = False
|
||||||
replace_cover: bool = False
|
replace_cover: bool = False
|
||||||
|
save_alternate_cover: bool = False
|
||||||
|
|
||||||
|
|
||||||
def merge_drop(dest_id, src_ids, gui):
|
def merge_drop(dest_id, src_ids, gui):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user