mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add merge_books function
This commit is contained in:
parent
2c20e69d63
commit
6211cd1504
@ -772,7 +772,7 @@ class BooksView(TableView):
|
|||||||
self.setItemDelegateForColumn(cm.index('series'), self.series_delegate)
|
self.setItemDelegateForColumn(cm.index('series'), self.series_delegate)
|
||||||
|
|
||||||
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
||||||
save, open_folder, book_details, delete, similar_menu=None):
|
save, open_folder, book_details, merge, delete, similar_menu=None):
|
||||||
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||||
self.context_menu = QMenu(self)
|
self.context_menu = QMenu(self)
|
||||||
if edit_metadata is not None:
|
if edit_metadata is not None:
|
||||||
@ -785,6 +785,8 @@ class BooksView(TableView):
|
|||||||
self.context_menu.addAction(save)
|
self.context_menu.addAction(save)
|
||||||
if open_folder is not None:
|
if open_folder is not None:
|
||||||
self.context_menu.addAction(open_folder)
|
self.context_menu.addAction(open_folder)
|
||||||
|
if merge is not None:
|
||||||
|
self.context_menu.addAction(merge)
|
||||||
if delete is not None:
|
if delete is not None:
|
||||||
self.context_menu.addAction(delete)
|
self.context_menu.addAction(delete)
|
||||||
if book_details is not None:
|
if book_details is not None:
|
||||||
|
@ -470,6 +470,7 @@
|
|||||||
</attribute>
|
</attribute>
|
||||||
<addaction name="action_add"/>
|
<addaction name="action_add"/>
|
||||||
<addaction name="action_edit"/>
|
<addaction name="action_edit"/>
|
||||||
|
<addaction name="action_merge"/>
|
||||||
<addaction name="action_convert"/>
|
<addaction name="action_convert"/>
|
||||||
<addaction name="action_view"/>
|
<addaction name="action_view"/>
|
||||||
<addaction name="action_news"/>
|
<addaction name="action_news"/>
|
||||||
@ -530,6 +531,21 @@
|
|||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_merge">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/merge_books.svg</normaloff>:/images/merge_books.svg</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Merge books</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>M</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoRepeat">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
<action name="action_sync">
|
<action name="action_sync">
|
||||||
<property name="enabled">
|
<property name="enabled">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
|
@ -255,6 +255,13 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
md.addAction(_('Download only covers'))
|
md.addAction(_('Download only covers'))
|
||||||
md.addAction(_('Download only social metadata'))
|
md.addAction(_('Download only social metadata'))
|
||||||
self.metadata_menu = md
|
self.metadata_menu = md
|
||||||
|
|
||||||
|
mb = QMenu()
|
||||||
|
mb.addAction(_('Merge into first selected book - delete others.'))
|
||||||
|
mb.addSeparator()
|
||||||
|
mb.addAction(_('Merge into first selected book - keep others.'))
|
||||||
|
self.merge_menu = mb
|
||||||
|
|
||||||
self.add_menu = QMenu()
|
self.add_menu = QMenu()
|
||||||
self.add_menu.addAction(_('Add books from a single directory'))
|
self.add_menu.addAction(_('Add books from a single directory'))
|
||||||
self.add_menu.addAction(_('Add books from directories, including '
|
self.add_menu.addAction(_('Add books from directories, including '
|
||||||
@ -301,6 +308,14 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
QObject.connect(md.actions()[7], SIGNAL('triggered(bool)'),
|
QObject.connect(md.actions()[7], SIGNAL('triggered(bool)'),
|
||||||
self.__em6__)
|
self.__em6__)
|
||||||
|
|
||||||
|
QObject.connect(self.action_merge, SIGNAL("triggered(bool)"),
|
||||||
|
self.merge_books)
|
||||||
|
self.__mb1__ = partial(self.merge_books)
|
||||||
|
QObject.connect(mb.actions()[0], SIGNAL('triggered(bool)'),
|
||||||
|
self.__mb1__)
|
||||||
|
self.__mb2__ = partial(self.merge_books_safe)
|
||||||
|
QObject.connect(mb.actions()[2], SIGNAL('triggered(bool)'),
|
||||||
|
self.__mb2__)
|
||||||
|
|
||||||
self.save_menu = QMenu()
|
self.save_menu = QMenu()
|
||||||
self.save_menu.addAction(_('Save to disk'))
|
self.save_menu.addAction(_('Save to disk'))
|
||||||
@ -317,6 +332,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.view_menu.addAction(_('View specific format'))
|
self.view_menu.addAction(_('View specific format'))
|
||||||
self.action_view.setMenu(self.view_menu)
|
self.action_view.setMenu(self.view_menu)
|
||||||
|
|
||||||
|
self.merge_menu = QMenu()
|
||||||
|
self.merge_menu.addAction(_('Merge into first selected book - delete others.'))
|
||||||
|
self.merge_menu.addAction(_('Merge into first selected book - keep others.'))
|
||||||
|
self.action_merge.setMenu(self.merge_menu)
|
||||||
|
|
||||||
self.delete_menu = QMenu()
|
self.delete_menu = QMenu()
|
||||||
self.delete_menu.addAction(_('Remove selected books'))
|
self.delete_menu.addAction(_('Remove selected books'))
|
||||||
self.delete_menu.addAction(
|
self.delete_menu.addAction(
|
||||||
@ -343,6 +363,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
Qt.QueuedConnection)
|
Qt.QueuedConnection)
|
||||||
self.connect(self.action_open_containing_folder,
|
self.connect(self.action_open_containing_folder,
|
||||||
SIGNAL('triggered(bool)'), self.view_folder)
|
SIGNAL('triggered(bool)'), self.view_folder)
|
||||||
|
|
||||||
|
self.merge_menu.actions()[0].triggered.connect(self.merge_books)
|
||||||
|
self.merge_menu.actions()[1].triggered.connect(self.merge_books_safe)
|
||||||
|
|
||||||
|
|
||||||
self.delete_menu.actions()[0].triggered.connect(self.delete_books)
|
self.delete_menu.actions()[0].triggered.connect(self.delete_books)
|
||||||
self.delete_menu.actions()[1].triggered.connect(self.delete_selected_formats)
|
self.delete_menu.actions()[1].triggered.connect(self.delete_selected_formats)
|
||||||
self.delete_menu.actions()[2].triggered.connect(self.delete_all_but_selected_formats)
|
self.delete_menu.actions()[2].triggered.connect(self.delete_all_but_selected_formats)
|
||||||
@ -354,7 +379,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.action_sync.setEnabled(True)
|
self.action_sync.setEnabled(True)
|
||||||
self.create_device_menu()
|
self.create_device_menu()
|
||||||
self.action_edit.setMenu(md)
|
self.action_edit.setMenu(md)
|
||||||
|
self.action_merge.setMenu(self.merge_menu)
|
||||||
self.action_save.setMenu(self.save_menu)
|
self.action_save.setMenu(self.save_menu)
|
||||||
|
|
||||||
cm = QMenu()
|
cm = QMenu()
|
||||||
cm.addAction(_('Convert individually'))
|
cm.addAction(_('Convert individually'))
|
||||||
cm.addAction(_('Bulk convert'))
|
cm.addAction(_('Bulk convert'))
|
||||||
@ -388,6 +415,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
setPopupMode(QToolButton.MenuButtonPopup)
|
setPopupMode(QToolButton.MenuButtonPopup)
|
||||||
self.tool_bar.widgetForAction(self.action_edit).\
|
self.tool_bar.widgetForAction(self.action_edit).\
|
||||||
setPopupMode(QToolButton.MenuButtonPopup)
|
setPopupMode(QToolButton.MenuButtonPopup)
|
||||||
|
self.tool_bar.widgetForAction(self.action_merge).\
|
||||||
|
setPopupMode(QToolButton.MenuButtonPopup)
|
||||||
self.tool_bar.widgetForAction(self.action_sync).\
|
self.tool_bar.widgetForAction(self.action_sync).\
|
||||||
setPopupMode(QToolButton.MenuButtonPopup)
|
setPopupMode(QToolButton.MenuButtonPopup)
|
||||||
self.tool_bar.widgetForAction(self.action_convert).\
|
self.tool_bar.widgetForAction(self.action_convert).\
|
||||||
@ -440,14 +469,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.action_save,
|
self.action_save,
|
||||||
self.action_open_containing_folder,
|
self.action_open_containing_folder,
|
||||||
self.action_show_book_details,
|
self.action_show_book_details,
|
||||||
|
self.action_merge,
|
||||||
self.action_del,
|
self.action_del,
|
||||||
similar_menu=similar_menu)
|
similar_menu=similar_menu)
|
||||||
|
|
||||||
self.memory_view.set_context_menu(None, None, None,
|
self.memory_view.set_context_menu(None, None, None,
|
||||||
self.action_view, self.action_save, None, None, self.action_del)
|
self.action_view, self.action_save, None, None, None, self.action_del)
|
||||||
self.card_a_view.set_context_menu(None, None, None,
|
self.card_a_view.set_context_menu(None, None, None,
|
||||||
self.action_view, self.action_save, None, None, self.action_del)
|
self.action_view, self.action_save, None, None, None, self.action_del)
|
||||||
self.card_b_view.set_context_menu(None, None, None,
|
self.card_b_view.set_context_menu(None, None, None,
|
||||||
self.action_view, self.action_save, None, None, self.action_del)
|
self.action_view, self.action_save, None, None, None, self.action_del)
|
||||||
|
|
||||||
QObject.connect(self.library_view,
|
QObject.connect(self.library_view,
|
||||||
SIGNAL('files_dropped(PyQt_PyObject)'),
|
SIGNAL('files_dropped(PyQt_PyObject)'),
|
||||||
self.files_dropped, Qt.QueuedConnection)
|
self.files_dropped, Qt.QueuedConnection)
|
||||||
@ -815,7 +847,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
else:
|
else:
|
||||||
print msg
|
print msg
|
||||||
|
|
||||||
|
|
||||||
def current_view(self):
|
def current_view(self):
|
||||||
'''Convenience method that returns the currently visible view '''
|
'''Convenience method that returns the currently visible view '''
|
||||||
idx = self.stack.currentIndex()
|
idx = self.stack.currentIndex()
|
||||||
@ -1517,6 +1548,139 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
|
|
||||||
############################################################################
|
############################################################################
|
||||||
|
|
||||||
|
############################### Merge books ##############################
|
||||||
|
def merge_books(self, safe_merge=False):
|
||||||
|
'''
|
||||||
|
Merge selected books in library.
|
||||||
|
'''
|
||||||
|
if self.stack.currentIndex() == 0:
|
||||||
|
self.db = self.library_view.model().db
|
||||||
|
rows = self.library_view.selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
d = error_dialog(self, _('Cannot merge books'),
|
||||||
|
_('No books selected'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
if len(rows) < 2:
|
||||||
|
d = error_dialog(self, _('Cannot merge books'),
|
||||||
|
_('At least two books must be selected for merging'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
r = self.books_to_merge(rows)
|
||||||
|
dest_id = r[0]
|
||||||
|
src_books = r[1]
|
||||||
|
src_ids = r[2]
|
||||||
|
if safe_merge:
|
||||||
|
if not confirm('<p>'+_('All book formats and metadata from the selected books '
|
||||||
|
'will be added to the first selected book. The second and '
|
||||||
|
'subsequently selected books will not be deleted or changed. '
|
||||||
|
'Please confirm you want to proceed.')
|
||||||
|
+'</p>', 'merge_books_safe', self):
|
||||||
|
return
|
||||||
|
self.add_formats(dest_id, src_books)
|
||||||
|
self.merge_metadata(dest_id, src_ids)
|
||||||
|
else:
|
||||||
|
if not confirm('<p>'+_('All book formats and metadata from the selected books will be merged '
|
||||||
|
'into the first selected book. After merger the second and '
|
||||||
|
'subsequently selected books will be deleted. '
|
||||||
|
'All book formats of the first selected book will be kept '
|
||||||
|
'and any duplicate formats in the second and subsequently selected books '
|
||||||
|
'will be permanently <b>deleted</b> from your computer. '
|
||||||
|
'Are you <b>sure</b> you want to proceed?')
|
||||||
|
+'</p>', 'merge_books', self):
|
||||||
|
return
|
||||||
|
if len(rows)>5:
|
||||||
|
if not confirm('<p>'+_('You are about to merge more than 5 books. '
|
||||||
|
'Are you <b>sure</b> you want to proceed?')
|
||||||
|
+'</p>', 'merge_too_many_books', self):
|
||||||
|
return
|
||||||
|
self.add_formats(dest_id, src_books)
|
||||||
|
self.merge_metadata(dest_id, src_ids)
|
||||||
|
self.delete_books_after_merge(src_ids)
|
||||||
|
# leave the selection highlight on first selected book
|
||||||
|
dest_row = rows[0].row()
|
||||||
|
for row in rows:
|
||||||
|
if row.row() < rows[0].row():
|
||||||
|
dest_row -= 1
|
||||||
|
ci = self.library_view.model().index(dest_row, 0)
|
||||||
|
if ci.isValid():
|
||||||
|
self.library_view.setCurrentIndex(ci)
|
||||||
|
|
||||||
|
def merge_books_safe(self):
|
||||||
|
self.merge_books(safe_merge=True)
|
||||||
|
|
||||||
|
def add_formats(self, dest_id, src_books, replace=False):
|
||||||
|
for src_book in src_books:
|
||||||
|
if src_book:
|
||||||
|
fmt = os.path.splitext(src_book)[-1].replace('.', '').upper()
|
||||||
|
with open(src_book, 'rb') as f:
|
||||||
|
self.db.add_format(dest_id, fmt, f, index_is_id=True,
|
||||||
|
notify=False, replace=replace)
|
||||||
|
|
||||||
|
def books_to_merge (self, rows):
|
||||||
|
dest_book = True
|
||||||
|
src_books = []
|
||||||
|
src_ids = []
|
||||||
|
for row in rows:
|
||||||
|
if dest_book:
|
||||||
|
dest_id = self.library_view.model().db.id(row.row())
|
||||||
|
else:
|
||||||
|
self._metadata_view_id = self.library_view.model().db.id(row.row())
|
||||||
|
dbfmts = self.library_view.model().db.formats(self._metadata_view_id, index_is_id=True)
|
||||||
|
id = self._metadata_view_id
|
||||||
|
src_ids.append(id)
|
||||||
|
library_path = self.library_path
|
||||||
|
book_path = self.library_view.model().db.construct_path_name(id)
|
||||||
|
book_fname = self.library_view.model().db.construct_file_name(id)
|
||||||
|
current_path_fname = library_path + '/' + book_path + '/' + book_fname
|
||||||
|
current_path_fname = current_path_fname.replace(os.sep, '/')
|
||||||
|
if dbfmts:
|
||||||
|
dbfmts = dbfmts.split(',')
|
||||||
|
for dbfmt in dbfmts:
|
||||||
|
file_name = current_path_fname + '.' + dbfmt
|
||||||
|
src_books.append(file_name)
|
||||||
|
dest_book = False
|
||||||
|
return [dest_id, src_books, src_ids]
|
||||||
|
|
||||||
|
def delete_books_after_merge(self, ids_to_delete):
|
||||||
|
for id in ids_to_delete:
|
||||||
|
self.library_view.model().db.delete_book(id, notify=True)
|
||||||
|
|
||||||
|
def merge_metadata(self, dest_id, src_ids):
|
||||||
|
db = self.library_view.model().db
|
||||||
|
dest_mi = db.get_metadata(dest_id, index_is_id=True, get_cover=True)
|
||||||
|
for src_id in src_ids:
|
||||||
|
src_mi = db.get_metadata(src_id, index_is_id=True, get_cover=True)
|
||||||
|
if not dest_mi.comments or len(dest_mi.comments) == 0:
|
||||||
|
dest_mi.comments = src_mi.comments
|
||||||
|
elif dest_mi.comments == src_mi.comments:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
dest_mi.comments = unicode(dest_mi.comments) + u'\r' + unicode(src_mi.comments)
|
||||||
|
if dest_mi.title == _('Unknown'):
|
||||||
|
dest_mi.title = src_mi.title
|
||||||
|
if dest_mi.authors[0] == _('Unknown'):
|
||||||
|
dest_mi.authors = src_mi.authors
|
||||||
|
dest_mi.author_sort = src_mi.author_sort
|
||||||
|
if not dest_mi.tags:
|
||||||
|
dest_mi.tags = src_mi.tags
|
||||||
|
else:
|
||||||
|
for tag in src_mi.tags:
|
||||||
|
dest_mi.tags.append(tag)
|
||||||
|
# sort tags?
|
||||||
|
if not dest_mi.cover:
|
||||||
|
dest_mi.cover = src_mi.cover
|
||||||
|
if not dest_mi.publisher:
|
||||||
|
dest_mi.publisher = src_mi.publisher
|
||||||
|
if not dest_mi.rating:
|
||||||
|
dest_mi.rating = src_mi.rating
|
||||||
|
if not dest_mi.series:
|
||||||
|
dest_mi.series = src_mi.series
|
||||||
|
dest_mi.series_index = src_mi.series_index
|
||||||
|
db.set_metadata(dest_id, dest_mi, ignore_errors=False)
|
||||||
|
|
||||||
|
############################################################################
|
||||||
|
|
||||||
|
|
||||||
############################## Save to disk ################################
|
############################## Save to disk ################################
|
||||||
def save_single_format_to_disk(self, checked):
|
def save_single_format_to_disk(self, checked):
|
||||||
@ -2027,6 +2191,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.status_bar.reset_info()
|
self.status_bar.reset_info()
|
||||||
if location == 'library':
|
if location == 'library':
|
||||||
self.action_edit.setEnabled(True)
|
self.action_edit.setEnabled(True)
|
||||||
|
self.action_merge.setEnabled(True)
|
||||||
self.action_convert.setEnabled(True)
|
self.action_convert.setEnabled(True)
|
||||||
self.view_menu.actions()[1].setEnabled(True)
|
self.view_menu.actions()[1].setEnabled(True)
|
||||||
self.action_open_containing_folder.setEnabled(True)
|
self.action_open_containing_folder.setEnabled(True)
|
||||||
@ -2037,6 +2202,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
action.setEnabled(True)
|
action.setEnabled(True)
|
||||||
else:
|
else:
|
||||||
self.action_edit.setEnabled(False)
|
self.action_edit.setEnabled(False)
|
||||||
|
self.action_merge.setEnabled(False)
|
||||||
self.action_convert.setEnabled(False)
|
self.action_convert.setEnabled(False)
|
||||||
self.view_menu.actions()[1].setEnabled(False)
|
self.view_menu.actions()[1].setEnabled(False)
|
||||||
self.action_open_containing_folder.setEnabled(False)
|
self.action_open_containing_folder.setEnabled(False)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user