false
diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py
index 86aaa7c89c..4631f8557f 100644
--- a/src/calibre/gui2/ui.py
+++ b/src/calibre/gui2/ui.py
@@ -255,6 +255,13 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
md.addAction(_('Download only covers'))
md.addAction(_('Download only social metadata'))
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.addAction(_('Add books from a single directory'))
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)'),
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.addAction(_('Save to disk'))
@@ -317,6 +332,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.view_menu.addAction(_('View specific format'))
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.addAction(_('Remove selected books'))
self.delete_menu.addAction(
@@ -343,6 +363,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
Qt.QueuedConnection)
self.connect(self.action_open_containing_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()[1].triggered.connect(self.delete_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.create_device_menu()
self.action_edit.setMenu(md)
+ self.action_merge.setMenu(self.merge_menu)
self.action_save.setMenu(self.save_menu)
+
cm = QMenu()
cm.addAction(_('Convert individually'))
cm.addAction(_('Bulk convert'))
@@ -388,6 +415,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
setPopupMode(QToolButton.MenuButtonPopup)
self.tool_bar.widgetForAction(self.action_edit).\
setPopupMode(QToolButton.MenuButtonPopup)
+ self.tool_bar.widgetForAction(self.action_merge).\
+ setPopupMode(QToolButton.MenuButtonPopup)
self.tool_bar.widgetForAction(self.action_sync).\
setPopupMode(QToolButton.MenuButtonPopup)
self.tool_bar.widgetForAction(self.action_convert).\
@@ -440,14 +469,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.action_save,
self.action_open_containing_folder,
self.action_show_book_details,
+ self.action_merge,
self.action_del,
similar_menu=similar_menu)
+
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.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.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,
SIGNAL('files_dropped(PyQt_PyObject)'),
self.files_dropped, Qt.QueuedConnection)
@@ -815,7 +847,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
else:
print msg
-
def current_view(self):
'''Convenience method that returns the currently visible view '''
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(''+_('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.')
+ +'
', 'merge_books_safe', self):
+ return
+ self.add_formats(dest_id, src_books)
+ self.merge_metadata(dest_id, src_ids)
+ else:
+ if not confirm(''+_('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 deleted from your computer. '
+ 'Are you sure you want to proceed?')
+ +'
', 'merge_books', self):
+ return
+ if len(rows)>5:
+ if not confirm(''+_('You are about to merge more than 5 books. '
+ 'Are you sure you want to proceed?')
+ +'
', '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 ################################
def save_single_format_to_disk(self, checked):
@@ -2027,6 +2191,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.status_bar.reset_info()
if location == 'library':
self.action_edit.setEnabled(True)
+ self.action_merge.setEnabled(True)
self.action_convert.setEnabled(True)
self.view_menu.actions()[1].setEnabled(True)
self.action_open_containing_folder.setEnabled(True)
@@ -2037,6 +2202,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
action.setEnabled(True)
else:
self.action_edit.setEnabled(False)
+ self.action_merge.setEnabled(False)
self.action_convert.setEnabled(False)
self.view_menu.actions()[1].setEnabled(False)
self.action_open_containing_folder.setEnabled(False)