From b1c36e751473f0a096f1f6dc6f39f463d225786a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 20 Jun 2014 21:55:46 +0530 Subject: [PATCH] New tool to set metadata in the actual book files in the calibre library from the updated metadata in the calibre database. To use it go to Preferences->Toolbars and add the "Embed metadata" tool to the main toolbar. Then simply select the books whose files you want to update and click the Embed metadata button. Normally, calibre updated metadata in the book files whenever a file is exported from calibre. This tool is useful for people who want the files int he calibre library to have updated metadata as well. --- src/calibre/customize/builtins.py | 7 +- src/calibre/customize/ui.py | 9 ++- src/calibre/db/cache.py | 4 +- src/calibre/ebooks/metadata/meta.py | 4 +- src/calibre/gui2/actions/embed.py | 110 ++++++++++++++++++++++++++++ 5 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 src/calibre/gui2/actions/embed.py 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) +