diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index a74951c8a8..d0b6da4122 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -809,6 +809,11 @@ class ActionDelete(InterfaceActionBase): actual_plugin = 'calibre.gui2.actions.delete:DeleteAction' description = _('Delete books from your calibre library or connected device') +class ActionEmbed(InterfaceActionBase): + name = 'Embed Metadata' + actual_plugin = 'calibre.gui2.actions.embed:EmbedAction' + description = _('Embed updated metadata into the actual book files in your calibre library') + class ActionEditMetadata(InterfaceActionBase): name = 'Edit Metadata' actual_plugin = 'calibre.gui2.actions.edit_metadata:EditMetadataAction' @@ -964,7 +969,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionAddToLibrary, ActionEditCollections, ActionMatchBooks, ActionChooseLibrary, ActionCopyToLibrary, ActionTweakEpub, ActionUnpackBook, ActionNextMatch, ActionStore, ActionPluginUpdater, ActionPickRandom, ActionEditToC, ActionSortBy, - ActionMarkBooks] + ActionMarkBooks, ActionEmbed] # }}} diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 2e1cc23c4e..4d97e972f1 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -351,7 +351,7 @@ def get_file_type_metadata(stream, ftype): continue return mi -def set_file_type_metadata(stream, mi, ftype): +def set_file_type_metadata(stream, mi, ftype, report_error=None): ftype = ftype.lower().strip() if _metadata_writers.has_key(ftype): for plugin in _metadata_writers[ftype]: @@ -363,8 +363,11 @@ def set_file_type_metadata(stream, mi, ftype): plugin.set_metadata(stream, mi, ftype.lower().strip()) break except: - print 'Failed to set metadata for', repr(getattr(mi, 'title', '')) - traceback.print_exc() + if report_error is None: + print 'Failed to set metadata for', repr(getattr(mi, 'title', '')) + traceback.print_exc() + else: + report_error(mi, ftype, traceback.format_exc()) # }}} diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index 92b9a5d2eb..41ba5adc41 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -1797,7 +1797,7 @@ class Cache(object): return {k:tuple(sorted(v, key=sort_key)) for k, v in ans.iteritems()} @write_api - def embed_metadata(self, book_ids, only_fmts=None): + def embed_metadata(self, book_ids, only_fmts=None, report_error=None): ''' Update metadata in all formats of the specified book_ids to current metadata in the database. ''' field = self.fields['formats'] from calibre.ebooks.metadata.opf2 import pretty_print @@ -1808,7 +1808,7 @@ class Cache(object): def doit(fmt, mi, stream): with apply_null_metadata, pretty_print: - set_metadata(stream, mi, stream_type=fmt) + set_metadata(stream, mi, stream_type=fmt, report_error=report_error) stream.seek(0, os.SEEK_END) return stream.tell() diff --git a/src/calibre/ebooks/metadata/meta.py b/src/calibre/ebooks/metadata/meta.py index 9c3a79cc70..f9ac3a75cf 100644 --- a/src/calibre/ebooks/metadata/meta.py +++ b/src/calibre/ebooks/metadata/meta.py @@ -117,10 +117,10 @@ def _get_metadata(stream, stream_type, use_libprs_metadata, return base -def set_metadata(stream, mi, stream_type='lrf'): +def set_metadata(stream, mi, stream_type='lrf', report_error=None): if stream_type: stream_type = stream_type.lower() - set_file_type_metadata(stream, mi, stream_type) + set_file_type_metadata(stream, mi, stream_type, report_error=report_error) def metadata_from_filename(name, pat=None, fallback_pat=None): diff --git a/src/calibre/gui2/actions/embed.py b/src/calibre/gui2/actions/embed.py new file mode 100644 index 0000000000..97094a8edd --- /dev/null +++ b/src/calibre/gui2/actions/embed.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2014, Kovid Goyal ' + +from functools import partial + +from PyQt4.Qt import QTimer, QProgressDialog, Qt + +from calibre.gui2 import warning_dialog +from calibre.gui2.actions import InterfaceAction + +class EmbedAction(InterfaceAction): + + name = 'Embed Metadata' + action_spec = (_('Embed metadata'), 'modified.png', _('Embed metadata into book files'), None) + action_type = 'current' + action_add_menu = True + action_menu_clone_qaction = _('Embed metadata into book files') + + accepts_drops = True + + def accept_enter_event(self, event, mime_data): + if mime_data.hasFormat("application/calibre+from_library"): + return True + return False + + def accept_drag_move_event(self, event, mime_data): + if mime_data.hasFormat("application/calibre+from_library"): + return True + return False + + def drop_event(self, event, mime_data): + mime = 'application/calibre+from_library' + if mime_data.hasFormat(mime): + self.dropped_ids = tuple(map(int, str(mime_data.data(mime)).split())) + QTimer.singleShot(1, self.do_drop) + return True + return False + + def do_drop(self): + book_ids = self.dropped_ids + del self.dropped_ids + if book_ids: + self.do_embed(book_ids) + + def genesis(self): + self.qaction.triggered.connect(self.embed) + self.embed_menu = self.qaction.menu() + m = partial(self.create_menu_action, self.embed_menu) + m('embed-specific', + _('Embed metadata into files of a specific format from selected books..'), + triggered=self.embed_selected_formats) + self.qaction.setMenu(self.embed_menu) + self.pd_timer = t = QTimer() + t.timeout.connect(self.do_one) + + def embed(self): + rb = self.gui.iactions['Remove Books'] + ids = rb._get_selected_ids(err_title=_('Cannot embed')) + if not ids: + return + self.do_embed(ids) + + def embed_selected_formats(self): + rb = self.gui.iactions['Remove Books'] + ids = rb._get_selected_ids(err_title=_('Cannot embed')) + if not ids: + return + fmts = rb._get_selected_formats( + _('Choose formats to be updated'), ids) + if not fmts: + return + self.do_embed(ids, fmts) + + def do_embed(self, book_ids, only_fmts=None): + pd = QProgressDialog(_('Embedding updated metadata into book files...'), _('&Stop'), 0, len(book_ids), self.gui) + pd.setWindowModality(Qt.WindowModal) + errors = [] + self.job_data = (0, tuple(book_ids), pd, only_fmts, errors) + self.pd_timer.start() + + def do_one(self): + try: + i, book_ids, pd, only_fmts, errors = self.job_data + except (TypeError, AttributeError): + return + if i >= len(book_ids) or pd.wasCanceled(): + pd.setValue(pd.maximum()) + pd.hide() + self.pd_timer.stop() + self.job_data = None + self.gui.library_view.model().refresh_ids(book_ids) + if errors: + det_msg = [_('The {0} format of {1}:\n{2}').format((fmt or '').upper(), mi.title, tb) for mi, fmt, tb in errors] + warning_dialog( + self.gui, _('Failed for some files'), _( + 'Failed to embed metadata into some book files. Click "Show details" for details.'), + det_msg='\n\n'.join(det_msg), show=True) + return + pd.setValue(i) + db = self.gui.current_db.new_api + def report_error(mi, fmt, tb): + errors.append((mi, fmt, tb)) + db.embed_metadata((book_ids[i],), only_fmts=only_fmts, report_error=report_error) + self.job_data = (i + 1, book_ids, pd, only_fmts, errors) +