def about(self): ''' Display a short help message ''' from os.path import join from calibre.ptempfile import TemporaryDirectory from calibre.gui2.dialogs.message_box import MessageBox from calibre_plugins.prince_pdf.help import help_txt, license_txt from calibre_plugins.prince_pdf import PrincePDFPlugin from calibre_plugins.prince_pdf import __license__ author = PrincePDFPlugin.author version = "%i.%i.%i" % PrincePDFPlugin.version license = __license__ with TemporaryDirectory('xxx') as tdir: for x in ('prince_icon.png', 'small_icon.png'): with open(join(tdir, x), 'w') as f: f.write(get_resources('images/' + x)) help_box = MessageBox(type_ = MessageBox.INFO, \ title = _('About the Prince PDF Plugin'), \ msg = help_txt % {'author':author, 'version':version, 'license':license, 'dir':tdir, 'code':'style="font-family:monospace ; font-weight:bold"'}, \ det_msg = 'Copyright \u00a9 %s\n%s' % (__copyright__, license_txt), \ q_icon = self.icon, \ show_copy_button = False) #help_box.gridLayout.addWidget(help_box.icon_widget,0,0,Qt.AlignTop) help_box.gridLayout.setAlignment(help_box.icon_widget, Qt.AlignTop) help_box.exec_()
def about(self): ''' Display a short help message ''' from os.path import join from calibre.ptempfile import TemporaryDirectory from calibre.gui2.dialogs.message_box import MessageBox from calibre_plugins.prince_pdf.help import help_txt, license_txt from calibre_plugins.prince_pdf import PrincePDFPlugin from calibre_plugins.prince_pdf import __license__ author = PrincePDFPlugin.author version = "%i.%i.%i" % PrincePDFPlugin.version license = __license__ with TemporaryDirectory('xxx') as tdir: for x in ('prince_icon.png', 'small_icon.png'): with open(join(tdir, x),'w') as f: f.write(get_resources('images/' + x)) help_box = MessageBox(type_ = MessageBox.INFO, \ title = _('About the Prince PDF Plugin'), \ msg = help_txt % {'author':author, 'version':version, 'license':license, 'dir':tdir, 'code':'style="font-family:monospace ; font-weight:bold"'}, \ det_msg = 'Copyright \u00a9 %s\n%s' % (__copyright__, license_txt), \ q_icon = self.icon, \ show_copy_button = False) #help_box.gridLayout.addWidget(help_box.icon_widget,0,0,Qt.AlignTop) help_box.gridLayout.setAlignment(help_box.icon_widget,Qt.AlignTop) help_box.exec_()
def add_pdf(self, book_id, pdf_file, exists): ''' Add the PDF file to the book record, asking for replacement :param book_id: The book identifier :param pdf_file: The path to the PDF file :param exists: True if there is already a PDF in the book ''' from calibre.constants import DEBUG from calibre.gui2.dialogs.message_box import MessageBox add_it = True if (exists): msg = MessageBox( MessageBox.QUESTION, _('Existing format'), _('The selected book already contains a PDF format. Are you sure you want to replace it?' ), _("The temporary file can be found in:\n%s") % pdf_file) msg.toggle_det_msg() add_it = (msg.exec_()) if (add_it): if DEBUG: print(_('Adding PDF...')) self.db.new_api.add_format(book_id, 'pdf', pdf_file) self.gui.library_view.model().refresh_ids([book_id]) self.gui.library_view.refresh_book_details() self.gui.tags_view.recount()
def question_dialog(parent, title, msg, det_msg='', show_copy_button=False, default_yes=True, # Skippable dialogs # Set skip_dialog_name to a unique name for this dialog # Set skip_dialog_msg to a message displayed to the user skip_dialog_name=None, skip_dialog_msg=_('Show this confirmation again'), skip_dialog_skipped_value=True, skip_dialog_skip_precheck=True, # Override icon (QIcon to be used as the icon for this dialog) override_icon=None): from calibre.gui2.dialogs.message_box import MessageBox auto_skip = set(gprefs.get('questions_to_auto_skip', [])) if (skip_dialog_name is not None and skip_dialog_name in auto_skip): return bool(skip_dialog_skipped_value) d = MessageBox(MessageBox.QUESTION, title, msg, det_msg, parent=parent, show_copy_button=show_copy_button, default_yes=default_yes, q_icon=override_icon) if skip_dialog_name is not None and skip_dialog_msg: tc = d.toggle_checkbox tc.setVisible(True) tc.setText(skip_dialog_msg) tc.setChecked(bool(skip_dialog_skip_precheck)) ret = d.exec_() == d.Accepted if skip_dialog_name is not None and not d.toggle_checkbox.isChecked(): auto_skip.add(skip_dialog_name) gprefs.set('questions_to_auto_skip', list(auto_skip)) return ret
def info_dialog(parent, title, msg, det_msg="", show=False, show_copy_button=True): from calibre.gui2.dialogs.message_box import MessageBox d = MessageBox(MessageBox.INFO, title, msg, det_msg, parent=parent, show_copy_button=show_copy_button) if show: return d.exec_() return d
def restart_required(self, state): title = _('Restart required') msg = _('To apply changes, restart calibre.') d = MessageBox(MessageBox.WARNING, title, msg, show_copy_button=False) self._log_location("WARNING: %s" % (msg)) d.exec_()
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 i > 0: self.gui.status_bar.show_message( ngettext('Embedded metadata in one book', 'Embedded metadata in {} books', i).format(i), 5000) if errors: det_msg = '\n\n'.join([ _('The {0} format of {1}:\n\n{2}\n').format( (fmt or '').upper(), force_unicode(mi.title), force_unicode(tb)) for mi, fmt, tb in errors ]) from calibre.gui2.dialogs.message_box import MessageBox title, msg = _('Failed for some files'), _( 'Failed to embed metadata into some book files. Click "Show details" for details.' ) d = MessageBox(MessageBox.WARNING, _('WARNING:') + ' ' + title, msg, det_msg, parent=self.gui, show_copy_button=True) tc = d.toggle_checkbox tc.setVisible(True), tc.setText( _('Show the &failed books in the main book list')) tc.setChecked(gprefs.get('show-embed-failed-books', False)) d.resize_needed.emit() d.exec_() gprefs['show-embed-failed-books'] = tc.isChecked() if tc.isChecked(): failed_ids = {mi.book_id for mi, fmt, tb in errors} db = self.gui.current_db db.data.set_marked_ids(failed_ids) self.gui.search.set_search_string('marked:true') return pd.setValue(i) db = self.gui.current_db.new_api book_id = book_ids[i] def report_error(mi, fmt, tb): mi.book_id = book_id errors.append((mi, fmt, tb)) db.embed_metadata((book_id, ), only_fmts=only_fmts, report_error=report_error) self.job_data = (i + 1, book_ids, pd, only_fmts, errors)
def warning_dialog(parent, title, msg, det_msg='', show=False, show_copy_button=True): from calibre.gui2.dialogs.message_box import MessageBox d = MessageBox(MessageBox.WARNING, _('WARNING:')+ ' ' + title, msg, det_msg, parent=parent, show_copy_button=show_copy_button) if show: return d.exec_() return d
def error_dialog(parent, title, msg, det_msg='', show=False, show_copy_button=True): from calibre.gui2.dialogs.message_box import MessageBox d = MessageBox(MessageBox.ERROR, _('ERROR:')+ ' ' + title, msg, det_msg, parent=parent, show_copy_button=show_copy_button) if show: return d.exec_() return d
def info_dialog(parent, title, msg, det_msg='', show=False, show_copy_button=True): from calibre.gui2.dialogs.message_box import MessageBox d = MessageBox(MessageBox.INFO, title, msg, det_msg, parent=parent, show_copy_button=show_copy_button) if show: return d.exec_() return d
def count_message(action, count, show_diff=False): msg = _('%(action)s %(num)s occurrences of %(query)s' % dict(num=count, query=errfind, action=action)) if show_diff and count > 0: d = MessageBox(MessageBox.INFO, _('Searching done'), prepare_string_for_xml(msg), parent=gui_parent, show_copy_button=False) d.diffb = b = d.bb.addButton(_('See what &changed'), d.bb.ActionRole) b.setIcon(QIcon(I('diff.png'))), d.set_details(None), b.clicked.connect(d.accept) b.clicked.connect(partial(show_current_diff, allow_revert=True)) d.exec_() else: info_dialog(gui_parent, _('Searching done'), prepare_string_for_xml(msg), show=True)
def news_clippings_destination_changed(self): qs_new_destination_name = self.cfg_news_clippings_lineEdit.text() if not re.match(r'^\S+[A-Za-z0-9 ]+$', qs_new_destination_name): # Complain about News clippings title title = _('Invalid title for News clippings') msg = _("Supply a valid title for News clippings, for example 'My News Clippings'.") d = MessageBox(MessageBox.WARNING, title, msg, show_copy_button=False) self._log_location("WARNING: %s" % msg) d.exec_()
def __init__(self, filename, parent=None): MessageBox.__init__( self, MessageBox.INFO, _('Downloading book'), _( 'The book {0} will be downloaded and added to your' ' calibre library automatically.').format(filename), show_copy_button=False, parent=parent ) self.toggle_checkbox.setChecked(True) self.toggle_checkbox.setVisible(True) self.toggle_checkbox.setText(_('Show this message again')) self.toggle_checkbox.toggled.connect(self.show_again_changed) self.resize_needed.emit()
def configure_appearance(self): ''' ''' self._log_location() appearance_settings = { 'appearance_css': default_elements, 'appearance_hr_checkbox': False, 'appearance_timestamp_format': default_timestamp } # Save, hash the original settings original_settings = {} osh = hashlib.md5() for setting in appearance_settings: original_settings[setting] = plugin_prefs.get(setting, appearance_settings[setting]) osh.update(repr(plugin_prefs.get(setting, appearance_settings[setting]))) # Display the Annotations appearance dialog aa = AnnotationsAppearance(self, self.annotations_icon, plugin_prefs) cancelled = False if aa.exec_(): # appearance_hr_checkbox and appearance_timestamp_format changed live to prefs during previews plugin_prefs.set('appearance_css', aa.elements_table.get_data()) # Generate a new hash nsh = hashlib.md5() for setting in appearance_settings: nsh.update(repr(plugin_prefs.get(setting, appearance_settings[setting]))) else: for setting in appearance_settings: plugin_prefs.set(setting, original_settings[setting]) nsh = osh # If there were changes, and there are existing annotations, # and there is an active Annotations field, offer to re-render field = get_cc_mapping('annotations', 'field', None) if osh.digest() != nsh.digest() and existing_annotations(self.parent, field): title = 'Update annotations?' msg = '<p>Update existing annotations to new appearance settings?</p>' d = MessageBox(MessageBox.QUESTION, title, msg, show_copy_button=False) self._log_location("QUESTION: %s" % msg) if d.exec_(): self._log_location("Updating existing annotations to modified appearance") # Wait for indexing to complete while not self.annotated_books_scanner.isFinished(): Application.processEvents() move_annotations(self, self.annotated_books_scanner.annotation_map, field, field, window_title="Updating appearance")
def _remove_all_assignments(self): ''' ''' self._log_location() self.stored_command = 'clear_all_collections' # Confirm title = "Are you sure?" msg = ("<p>Delete all collection assignments from calibre and Marvin?</p>") d = MessageBox(MessageBox.QUESTION, title, msg, show_copy_button=False) if d.exec_(): self.calibre_lw.clear() self.marvin_lw.clear()
def configure_appearance(self): ''' ''' from calibre_plugins.annotations.appearance import default_elements from calibre_plugins.annotations.appearance import default_timestamp appearance_settings = { 'appearance_css': default_elements, 'appearance_hr_checkbox': False, 'appearance_timestamp_format': default_timestamp } # Save, hash the original settings original_settings = {} osh = hashlib.md5() for setting in appearance_settings: original_settings[setting] = plugin_prefs.get(setting, appearance_settings[setting]) osh.update(repr(plugin_prefs.get(setting, appearance_settings[setting]))) # Display the appearance dialog aa = AnnotationsAppearance(self, get_icon('images/annotations.png'), plugin_prefs) cancelled = False if aa.exec_(): # appearance_hr_checkbox and appearance_timestamp_format changed live to prefs during previews plugin_prefs.set('appearance_css', aa.elements_table.get_data()) # Generate a new hash nsh = hashlib.md5() for setting in appearance_settings: nsh.update(repr(plugin_prefs.get(setting, appearance_settings[setting]))) else: for setting in appearance_settings: plugin_prefs.set(setting, original_settings[setting]) nsh = osh # If there were changes, and there are existing annotations, offer to re-render field = get_cc_mapping('annotations', 'field', None) if osh.digest() != nsh.digest() and existing_annotations(self.opts.parent,field): title = _('Update annotations?') msg = _('<p>Update existing annotations to new appearance settings?</p>') d = MessageBox(MessageBox.QUESTION, title, msg, show_copy_button=False) self._log_location("QUESTION: %s" % msg) if d.exec_(): self._log_location("Updating existing annotations to modified appearance") if self.annotated_books_scanner.isRunning(): self.annotated_books_scanner.wait() move_annotations(self, self.annotated_books_scanner.annotation_map, field, field, window_title=_("Updating appearance"))
def save_pdf(self, pdf_file, pdf_base_file): ''' Save the PDF file in the final location :param pdf_file: The path to the PDF file :param pdf_base_file: The desired file name and relative path ''' from os import makedirs from os.path import basename, dirname, join, exists from shutil import move from calibre.constants import DEBUG from calibre.gui2 import choose_dir from calibre.gui2.dialogs.message_box import MessageBox from calibre.gui2 import error_dialog path = choose_dir(self.gui, 'save to disk dialog', _('Choose destination directory')) if not path: return save_file = join(path, pdf_base_file) base_dir = dirname(save_file) try: makedirs(base_dir) except BaseException: if not exists(base_dir): raise try: move(pdf_file, save_file) except: error_dialog(self.gui, _('Could not save PDF'), _("Error writing the PDF file:\n%s" % save_file), show=True) return if DEBUG: print(save_file) MessageBox(MessageBox.INFO, _('File saved'), _("PDF file saved in:\n%s") % save_file).exec_()
def _remove_collection_assignment(self): ''' Only one panel can have active selection ''' def _remove_assignments(deletes, list_widget): row = list_widget.row(deletes[0]) for item in deletes: list_widget.takeItem(list_widget.row(item)) if row >= list_widget.count(): row = list_widget.count() - 1 if row >= 0: list_widget.scrollToItem(list_widget.item(row)) self._log_location() if self.calibre_lw.selectedItems(): deletes = self.calibre_lw.selectedItems() _remove_assignments(deletes, self.calibre_lw) elif self.marvin_lw.selectedItems(): deletes = self.marvin_lw.selectedItems() _remove_assignments(deletes, self.marvin_lw) else: title = "No collection selected" msg = ("<p>Select a collection assignment to remove.</p>") MessageBox(MessageBox.INFO, title, msg, show_copy_button=False).exec_()
def __init__(self, parent, title, msg, log='', det_msg=''): ''' :param log: An HTML log :param title: The title for this popup :param msg: The msg to display :param det_msg: Detailed message ''' MessageBox.__init__(self, MessageBox.INFO, title, msg, det_msg=det_msg, show_copy_button=False, parent=parent) self.log = log self.vlb = self.bb.addButton(_('View Report'), self.bb.ActionRole) self.vlb.setIcon(QIcon(I('dialog_information.png'))) self.vlb.clicked.connect(self.show_log) self.det_msg_toggle.setVisible(bool(det_msg)) self.vlb.setVisible(bool(log))
def _get_folder_location(self): ''' Confirm specified folder location contains Marvin subfolder ''' dfl = self.prefs.get('dropbox_folder', None) msg = None title = 'Invalid Dropbox folder' folder_location = None if not dfl: msg = '<p>No Dropbox folder location specified in Configuration dialog.</p>' else: # Confirm presence of Marvin subfolder if not os.path.exists(dfl): msg = "<p>Specified Dropbox folder <tt>{0}</tt> not found.".format( dfl) else: path = os.path.join(dfl, 'Apps', 'com.marvinapp') if os.path.exists(path): folder_location = path else: msg = '<p>com.marvinapp not found in Apps folder.</p>' if msg: self._log_location("{0}: {1}".format(title, msg)) MessageBox(MessageBox.WARNING, title, msg, det_msg='', show_copy_button=False).exec_() return folder_location
def import_opml(self): opml_files = choose_files(self, 'OPML chooser dialog', _('Select OPML file'), filters=[(_('OPML'), ['opml'])] ) if not opml_files: return opml = OPML(self.oldest_article.oldest_article, self.max_articles.max_articles); for opml_file in opml_files: opml.load(opml_file) outlines = opml.parse() opml.import_recipes(outlines) # show a messagebox statingthat import finished msg_box = MessageBox(MessageBox.INFO, "Finished", "OPML to Recipe conversion complete", parent=self, show_copy_button=False) msg_box.exec_()
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 i > 0: self.gui.status_bar.show_message(ngettext( 'Embedded metadata in one book', 'Embedded metadata in {} books', i).format(i), 5000) if errors: det_msg = '\n\n'.join([_('The {0} format of {1}:\n\n{2}\n').format( (fmt or '').upper(), force_unicode(mi.title), force_unicode(tb)) for mi, fmt, tb in errors]) from calibre.gui2.dialogs.message_box import MessageBox title, msg = _('Failed for some files'), _( 'Failed to embed metadata into some book files. Click "Show details" for details.') d = MessageBox(MessageBox.WARNING, _('WARNING:')+ ' ' + title, msg, det_msg, parent=self.gui, show_copy_button=True) tc = d.toggle_checkbox tc.setVisible(True), tc.setText(_('Show the &failed books in the main book list')) tc.setChecked(gprefs.get('show-embed-failed-books', False)) d.resize_needed.emit() d.exec_() gprefs['show-embed-failed-books'] = tc.isChecked() if tc.isChecked(): failed_ids = {mi.book_id for mi, fmt, tb in errors} db = self.gui.current_db db.data.set_marked_ids(failed_ids) self.gui.search.set_search_string('marked:true') return pd.setValue(i) db = self.gui.current_db.new_api book_id = book_ids[i] def report_error(mi, fmt, tb): mi.book_id = book_id errors.append((mi, fmt, tb)) db.embed_metadata((book_id,), only_fmts=only_fmts, report_error=report_error) self.job_data = (i + 1, book_ids, pd, only_fmts, errors)
def __init__(self, parent, title, msg, log=None, det_msg=''): ''' :param log: An HTML or plain text log :param title: The title for this popup :param msg: The msg to display :param det_msg: Detailed message ''' MessageBox.__init__(self, MessageBox.INFO, title, msg, det_msg=det_msg, show_copy_button=False, parent=parent) self.log = log self.vlb = self.bb.addButton(_('View log'), self.bb.ActionRole) self.vlb.setIcon(QIcon(I('debug.png'))) self.vlb.clicked.connect(self.show_log) self.det_msg_toggle.setVisible(bool(det_msg)) self.vlb.setVisible(True)
def forget_service(self): ''' Remove the currently selected sync app ''' self._log_location() key = str(self.sync_apps.currentText()) title = "Forget syncing application".format(key) msg = ("<p>Forget '{}' syncing application?".format(key)) dlg = MessageBox(MessageBox.QUESTION, title, msg, parent=self.gui, show_copy_button=False) if dlg.exec_(): # Delete key from prefs sync_apps = self.prefs.get('sync_apps', {}) del sync_apps[key] self.prefs.set('sync_apps', sync_apps) # Remove from combobox index = self.sync_apps.currentIndex() self.sync_apps.removeItem(index)
def add_pdf(self, book_id, pdf_file, exists): ''' Add the PDF file to the book record, asking for replacement :param book_id: The book identifier :param pdf_file: The path to the PDF file :param exists: True if there is already a PDF in the book ''' from calibre.constants import DEBUG from calibre.gui2.dialogs.message_box import MessageBox add_it = True if (exists): msg = MessageBox(MessageBox.QUESTION, _('Existing format'), _('The selected book already contains a PDF format. Are you sure you want to replace it?'), _("The temporary file can be found in:\n%s") % pdf_file) msg.toggle_det_msg() add_it = (msg.exec_()) if (add_it): if DEBUG: print(_('Adding PDF...')) self.db.new_api.add_format(book_id, 'pdf', pdf_file) self.gui.library_view.model().refresh_ids([book_id]) self.gui.library_view.refresh_book_details() self.gui.tags_view.recount()
def _rename_collection(self): ''' Only one panel can have active selection ''' self._log_location() if self.calibre_lw.selectedItems(): self.rename_calibre_tag() elif self.marvin_lw.selectedItems(): self.rename_marvin_tag() else: title = "No collection selected" msg = ("<p>Select a collection to rename.</p>") MessageBox(MessageBox.INFO, title, msg, show_copy_button=False).exec_()
def get_selected_book_mi(opts, msg=None, det_msg=None): # Get currently selected books rows = opts.gui.library_view.selectionModel().selectedRows() if len(rows) == 0 or len(rows) > 1: MessageBox(MessageBox.WARNING, _('Select a book to receive annotations'), msg, det_msg=det_msg, show_copy_button=False, parent=opts.gui).exec_() return None # Get the current metadata for this book from the db ids = list(map(opts.gui.library_view.model().id, rows)) if ids: mi = opts.gui.current_db.get_metadata(ids[0], index_is_id=True) return mi else: return None
def move_annotations(parent, annotation_map, old_destination_field, new_destination_field, window_title=_("Moving annotations")): ''' Move annotations from old_destination_field to new_destination_field annotation_map precalculated in thread in config.py ''' import calibre_plugins.annotations.config as cfg _log_location("%s -> %s" % (old_destination_field, new_destination_field)) library_db = parent.opts.gui.current_db id = library_db.FIELD_MAP['id'] # Show progress pb = ProgressBar(parent=parent, window_title=window_title, on_top=True) total_books = len(annotation_map) pb.set_maximum(total_books) pb.set_value(1) pb.set_label('{:^100}'.format('%s for %d books' % (window_title, total_books))) pb.show() id_map_old_destination_field = {} id_map_new_destination_field = {} transient_db = 'transient' # Prepare a new COMMENTS_DIVIDER comments_divider = '<div class="comments_divider"><p style="text-align:center;margin:1em 0 1em 0">{0}</p></div>'.format( cfg.plugin_prefs.get( 'COMMENTS_DIVIDER', '· · • · ✦ · • · ·' )) for cid in annotation_map: mi = library_db.get_metadata(cid, index_is_id=True) # Comments -> custom if old_destination_field == 'Comments' and new_destination_field.startswith( '#'): if mi.comments: old_soup = BeautifulSoup(mi.comments) uas = old_soup.find('div', 'user_annotations') if uas: # Remove user_annotations from Comments uas.extract() # Remove comments_divider from Comments cd = old_soup.find('div', 'comments_divider') if cd: cd.extract() # Capture content annotation_list = parent.opts.db.capture_content( uas, cid, transient_db) # Regurgitate content with current CSS style new_soup = parent.opts.db.rerender_to_html_from_list( annotation_list) id_map_old_destination_field[cid] = unicode(old_soup) id_map_new_destination_field[cid] = unicode(new_soup) pb.increment() # custom -> Comments elif old_destination_field.startswith( '#') and new_destination_field == 'Comments': if mi.get_user_metadata(old_destination_field, False)['#value#'] is not None: old_soup = BeautifulSoup( mi.get_user_metadata(old_destination_field, False)['#value#']) uas = old_soup.find('div', 'user_annotations') if uas: # Remove user_annotations from custom field uas.extract() # Capture content annotation_list = parent.opts.db.capture_content( uas, cid, transient_db) # Regurgitate content with current CSS style new_soup = parent.opts.db.rerender_to_html_from_list( annotation_list) # Add user_annotations to Comments new_comments = '' if mi.comments is None: new_comments = unicode(new_soup) else: new_comments = mi.comments + \ unicode(comments_divider) + \ unicode(new_soup) # # Update the record with stripped custom field, updated Comments # library_db.set_metadata(cid, mi, set_title=False, set_authors=False, # commit=True, force_changes=True, notify=True) id_map_old_destination_field[cid] = unicode(old_soup) id_map_new_destination_field[cid] = new_comments pb.increment() # custom -> custom elif old_destination_field.startswith( '#') and new_destination_field.startswith('#'): if mi.get_user_metadata(old_destination_field, False)['#value#'] is not None: old_soup = BeautifulSoup( mi.get_user_metadata(old_destination_field, False)['#value#']) uas = old_soup.find('div', 'user_annotations') if uas: # Remove user_annotations from originating custom field uas.extract() # Capture content annotation_list = parent.opts.db.capture_content( uas, cid, transient_db) # Regurgitate content with current CSS style new_soup = parent.opts.db.rerender_to_html_from_list( annotation_list) id_map_old_destination_field[cid] = unicode(old_soup) id_map_new_destination_field[cid] = unicode(new_soup) pb.increment() # same field -> same field - called from config:configure_appearance() elif (old_destination_field == new_destination_field): pb.set_label('{:^100}'.format( _('Updating annotations for {0} books').format(total_books))) if new_destination_field == 'Comments': if mi.comments: old_soup = BeautifulSoup(mi.comments) uas = old_soup.find('div', 'user_annotations') if uas: # Remove user_annotations from Comments uas.extract() # Remove comments_divider from Comments cd = old_soup.find('div', 'comments_divider') if cd: cd.extract() # Save stripped Comments mi.comments = unicode(old_soup) # Capture content annotation_list = parent.opts.db.capture_content( uas, cid, transient_db) # Regurgitate content with current CSS style new_soup = parent.opts.db.rerender_to_html_from_list( annotation_list) # Add user_annotations to Comments new_comments = '' if mi.comments is None: new_comments = unicode(new_soup) else: new_comments = mi.comments + \ unicode(comments_divider) + \ unicode(new_soup) # Update the record with stripped custom field, updated Comments # library_db.set_metadata(cid, mi, set_title=False, set_authors=False, # commit=True, force_changes=True, notify=True) id_map_old_destination_field[cid] = unicode(old_soup) id_map_new_destination_field[cid] = unicode(new_soup) pb.increment() else: # Update custom field old_soup = BeautifulSoup( mi.get_user_metadata(old_destination_field, False)['#value#']) uas = old_soup.find('div', 'user_annotations') if uas: # Remove user_annotations from originating custom field uas.extract() # Capture content annotation_list = parent.opts.db.capture_content( uas, cid, transient_db) # Regurgitate content with current CSS style new_soup = parent.opts.db.rerender_to_html_from_list( annotation_list) # # Add stripped old_soup plus new_soup to destination field # um = mi.metadata_for_field(new_destination_field) # um['#value#'] = unicode(old_soup) + unicode(new_soup) # mi.set_user_metadata(new_destination_field, um) # # # Update the record # library_db.set_metadata(cid, mi, set_title=False, set_authors=False, # commit=True, force_changes=True, notify=True) id_map_old_destination_field[cid] = unicode(old_soup) id_map_new_destination_field[cid] = unicode(new_soup) pb.increment() if len(id_map_old_destination_field) > 0: debug_print( "move_annotations - Updating metadata - for column: %s number of changes=%d" % (old_destination_field, len(id_map_old_destination_field))) library_db.new_api.set_field(old_destination_field.lower(), id_map_old_destination_field) if len(id_map_new_destination_field) > 0: debug_print( "move_annotations - Updating metadata - for column: %s number of changes=%d" % (new_destination_field, len(id_map_new_destination_field))) library_db.new_api.set_field(new_destination_field.lower(), id_map_new_destination_field) # Hide the progress bar pb.hide() # Change field value to friendly name if old_destination_field.startswith('#'): for cf in parent.custom_fields: if parent.custom_fields[cf]['field'] == old_destination_field: old_destination_field = cf break if new_destination_field.startswith('#'): for cf in parent.custom_fields: if parent.custom_fields[cf]['field'] == new_destination_field: new_destination_field = cf break # Report what happened if len(annotation_map) == 1: book_word = _('book') else: book_word = _('books') if old_destination_field == new_destination_field: msg = _( "Annotations updated to new appearance settings for {0} {1}.</p>" ).format(len(annotation_map), book_word) else: msg = _("Annotations for {0} {1} moved from <b>{2}</b> to <b>{3}</b>." ).format(len(annotation_map), book_word, old_destination_field, new_destination_field) msg = "<p>{0}</p>".format(msg) MessageBox(MessageBox.INFO, '', msg=msg, show_copy_button=False, parent=parent.gui).exec_() _log_location() _log("INFO: %s" % msg)
def move_annotations(parent, annotation_map, old_destination_field, new_destination_field, window_title="Moving annotations"): ''' Move annotations from old_destination_field to new_destination_field annotation_map precalculated in thread in config.py ''' import calibre_plugins.marvin_manager.config as cfg _log_location(annotation_map) _log(" %s -> %s" % (old_destination_field, new_destination_field)) db = parent.opts.gui.current_db id = db.FIELD_MAP['id'] # Show progress pb = ProgressBar(parent=parent, window_title=window_title) total_books = len(annotation_map) pb.set_maximum(total_books) pb.set_value(1) pb.set_label('{:^100}'.format('Moving annotations for %d books' % total_books)) pb.show() transient_db = 'transient' # Prepare a new COMMENTS_DIVIDER comments_divider = '<div class="comments_divider"><p style="text-align:center;margin:1em 0 1em 0">{0}</p></div>'.format( cfg.plugin_prefs.get( 'COMMENTS_DIVIDER', '· · • · ✦ · • · ·' )) for cid in annotation_map: mi = db.get_metadata(cid, index_is_id=True) # Comments -> custom if old_destination_field == 'Comments' and new_destination_field.startswith( '#'): if mi.comments: old_soup = BeautifulSoup(mi.comments) uas = old_soup.find('div', 'user_annotations') if uas: # Remove user_annotations from Comments uas.extract() # Remove comments_divider from Comments cd = old_soup.find('div', 'comments_divider') if cd: cd.extract() # Save stripped Comments mi.comments = unicode(old_soup) # Capture content parent.opts.db.capture_content(uas, cid, transient_db) # Regurgitate content with current CSS style new_soup = parent.opts.db.rerender_to_html( transient_db, cid) # Add user_annotations to destination um = mi.metadata_for_field(new_destination_field) um['#value#'] = unicode(new_soup) mi.set_user_metadata(new_destination_field, um) # Update the record with stripped Comments, populated custom field db.set_metadata(cid, mi, set_title=False, set_authors=False, commit=True, force_changes=True, notify=True) pb.increment() # custom -> Comments elif old_destination_field.startswith( '#') and new_destination_field == 'Comments': if mi.get_user_metadata(old_destination_field, False)['#value#'] is not None: old_soup = BeautifulSoup( mi.get_user_metadata(old_destination_field, False)['#value#']) uas = old_soup.find('div', 'user_annotations') if uas: # Remove user_annotations from custom field uas.extract() # Capture content parent.opts.db.capture_content(uas, cid, transient_db) # Regurgitate content with current CSS style new_soup = parent.opts.db.rerender_to_html( transient_db, cid) # Save stripped custom field data um = mi.metadata_for_field(old_destination_field) um['#value#'] = unicode(old_soup) mi.set_user_metadata(old_destination_field, um) # Add user_annotations to Comments if mi.comments is None: mi.comments = unicode(new_soup) else: mi.comments = mi.comments + \ unicode(comments_divider) + \ unicode(new_soup) # Update the record with stripped custom field, updated Comments db.set_metadata(cid, mi, set_title=False, set_authors=False, commit=True, force_changes=True, notify=True) pb.increment() # custom -> custom elif old_destination_field.startswith( '#') and new_destination_field.startswith('#'): if mi.get_user_metadata(old_destination_field, False)['#value#'] is not None: old_soup = BeautifulSoup( mi.get_user_metadata(old_destination_field, False)['#value#']) uas = old_soup.find('div', 'user_annotations') if uas: # Remove user_annotations from originating custom field uas.extract() # Capture content parent.opts.db.capture_content(uas, cid, transient_db) # Regurgitate content with current CSS style new_soup = parent.opts.db.rerender_to_html( transient_db, cid) # Save stripped custom field data um = mi.metadata_for_field(old_destination_field) um['#value#'] = unicode(old_soup) mi.set_user_metadata(old_destination_field, um) # Add new_soup to destination field um = mi.metadata_for_field(new_destination_field) um['#value#'] = unicode(new_soup) mi.set_user_metadata(new_destination_field, um) # Update the record db.set_metadata(cid, mi, set_title=False, set_authors=False, commit=True, force_changes=True, notify=True) pb.increment() # same field -> same field - called from config:configure_appearance() elif (old_destination_field == new_destination_field): pb.set_label('{:^100}'.format('Updating annotations for %d books' % total_books)) if new_destination_field == 'Comments': if mi.comments: old_soup = BeautifulSoup(mi.comments) uas = old_soup.find('div', 'user_annotations') if uas: # Remove user_annotations from Comments uas.extract() # Remove comments_divider from Comments cd = old_soup.find('div', 'comments_divider') if cd: cd.extract() # Save stripped Comments mi.comments = unicode(old_soup) # Capture content parent.opts.db.capture_content(uas, cid, transient_db) # Regurgitate content with current CSS style new_soup = parent.opts.db.rerender_to_html( transient_db, cid) # Add user_annotations to Comments if mi.comments is None: mi.comments = unicode(new_soup) else: mi.comments = mi.comments + \ unicode(comments_divider) + \ unicode(new_soup) # Update the record with stripped custom field, updated Comments db.set_metadata(cid, mi, set_title=False, set_authors=False, commit=True, force_changes=True, notify=True) pb.increment() else: # Update custom field old_soup = BeautifulSoup( mi.get_user_metadata(old_destination_field, False)['#value#']) uas = old_soup.find('div', 'user_annotations') if uas: # Remove user_annotations from originating custom field uas.extract() # Capture content parent.opts.db.capture_content(uas, cid, transient_db) # Regurgitate content with current CSS style new_soup = parent.opts.db.rerender_to_html( transient_db, cid) # Add stripped old_soup plus new_soup to destination field um = mi.metadata_for_field(new_destination_field) um['#value#'] = unicode(old_soup) + unicode(new_soup) mi.set_user_metadata(new_destination_field, um) # Update the record db.set_metadata(cid, mi, set_title=False, set_authors=False, commit=True, force_changes=True, notify=True) pb.increment() # Hide the progress bar pb.hide() # Get the eligible custom fields all_custom_fields = db.custom_field_keys() custom_fields = {} for cf in all_custom_fields: field_md = db.metadata_for_field(cf) if field_md['datatype'] in ['comments']: custom_fields[field_md['name']] = { 'field': cf, 'datatype': field_md['datatype'] } # Change field value to friendly name if old_destination_field.startswith('#'): for cf in custom_fields: if custom_fields[cf]['field'] == old_destination_field: old_destination_field = cf break if new_destination_field.startswith('#'): for cf in custom_fields: if custom_fields[cf]['field'] == new_destination_field: new_destination_field = cf break # Report what happened if old_destination_field == new_destination_field: msg = "<p>Annotations updated to new appearance settings for %d {0}.</p>" % len( annotation_map) else: msg = ( "<p>Annotations for %d {0} moved from <b>%s</b> to <b>%s</b>.</p>" % (len(annotation_map), old_destination_field, new_destination_field)) if len(annotation_map) == 1: msg = msg.format('book') else: msg = msg.format('books') MessageBox(MessageBox.INFO, '', msg=msg, show_copy_button=False, parent=parent.gui).exec_() _log("INFO: %s" % msg) # Update the UI updateCalibreGUIView()
def parse_exported_highlights(self, raw, log_failure=True): """ Extract highlights from pasted Annotation summary email Return True if no problems Return False if error """ # Create the annotations, books table as needed self.annotations_db = "%s_imported_annotations" % self.app_name_ self.create_annotations_table(self.annotations_db) self.books_db = "%s_imported_books" % self.app_name_ self.create_books_table(self.books_db) self.annotated_book_list = [] self.selected_books = None self._log("raw highlights: {0}".format(raw)) # Generate the book metadata from the selected book row = self.opts.gui.library_view.currentIndex() book_id = self.opts.gui.library_view.model().id(row) db = self.opts.gui.current_db mi = db.get_metadata(book_id, index_is_id=True) # Grab the title from the front of raw try: title = re.match(r'(?m)File: (?P<title>.*)$', raw).group('title') self._log("title='{0}".format(title)) # Populate a BookStruct book_mi = BookStruct() book_mi.active = True book_mi.author = 'Unknown' book_mi.book_id = mi.id book_mi.title = title book_mi.uuid = None book_mi.last_update = time.mktime(time.localtime()) book_mi.reader_app = self.app_name book_mi.cid = mi.id gr_annotations = raw.split('\n') num_lines = len(gr_annotations) highlights = {} # Find the first annotation i = 0 line = gr_annotations[i] self._log("Looking for Page: Line number={0} line='{1}'".format( i, line)) while not line.startswith('--- Page'): self._log(" unable to parse GoodReader Annotation summary") i += 1 line = gr_annotations[i] self._log( "Looking for Page: Line number={0} line='{1}'".format( i, line)) while i < num_lines and not line.startswith( '(report generated by GoodReader)'): # Extract the page number page_num = re.search('--- (Page \w+) ---', line) self._log("regex result: page_num={0}".format(page_num)) if page_num: page_num = page_num.group(1) self._log("page_num={0}".format(page_num)) # Extract the highlight i += 1 line = gr_annotations[i] self._log( "Looking for annotation start: Line number={0} line='{1}'" .format(i, line)) prefix = None while True: prefix = re.search( '^(?P<ann_type>{0})'.format( '|'.join(self.ANNOTATION_TYPES + self.SKIP_TYPES)), line) self._log("Searched for prefix={0}".format(prefix)) if prefix and prefix.group( 'ann_type') in self.SKIP_TYPES: i += 1 line = gr_annotations[i] self._log( "Looking for annotation start: Line number={0} line='{1}'" .format(i, line)) while not re.search( '^(?P<ann_type>{0})'.format('|'.join( self.ANNOTATION_TYPES)), line): i += 1 line = gr_annotations[i] self._log( "Looking for annotation start after a SKIP type: Line number={0} line='{1}'" .format(i, line)) continue elif prefix: self._log( "Have annotation start: Line number={0} line='{1}' prefix={2}" .format(i, line, prefix)) break else: i += 1 line = gr_annotations[i] self._log( "Looking for annotation start 2: Line number={0} line='{1}'" .format(i, line)) annotation = self._extract_highlight( line, prefix.group('ann_type')) annotation.page_num = page_num self._log( "Started annotation: page_num={0} annotation='{1}'". format(page_num, annotation)) # Get the annotation(s) i += 1 line = gr_annotations[i] self._log( "Reading annotation text 1: Line number={0} line='{1}'" .format(i, line)) ann = '' while i < num_lines \ and not line.startswith('--- Page') \ and not line.startswith('(report generated by GoodReader)'): if line: prefix = re.search( '^(?P<ann_type>{0})'.format( '|'.join(self.ANNOTATION_TYPES + self.SKIP_TYPES)), line) if prefix and prefix.group( 'ann_type') in self.SKIP_TYPES: # Continue until next ann_type i += 1 line = gr_annotations[i] while not re.search( '^(?P<ann_type>{0})'.format('|'.join( self.ANNOTATION_TYPES)), line): i += 1 if i == num_lines: break line = gr_annotations[i] continue elif prefix: # Additional highlight on the same page # write current annotation, start new annotation self._store_annotation(highlights, annotation) annotation = self._extract_highlight( line, prefix.group('ann_type')) annotation.page_num = page_num annotation.ann_type = prefix.group('ann_type') ann = '' i += 1 line = gr_annotations[i] continue if not ann: ann = line else: ann += '\n' + line i += 1 line = gr_annotations[i] annotation.ann = ann # Back up so that the next line is '--- Page' or '(report generated' i -= 1 self._store_annotation(highlights, annotation) i += 1 if i == num_lines: break line = gr_annotations[i] except Exception as e: import traceback self._log("Exception parsing GoodReader Annotation summary: %s" % e) traceback.print_exc() if log_failure: self._log(" unable to parse GoodReader Annotation summary") self._log("{:~^80}".format(" Imported Annotation summary ")) self._log(raw) self._log( "{:~^80}".format(" end imported Annotations summary ")) import traceback traceback.print_exc() msg = ('Unable to parse Annotation summary from %s. ' % self.app_name + 'Paste entire contents of emailed summary.') MessageBox(MessageBox.WARNING, 'Error importing annotations', msg, show_copy_button=False, parent=self.opts.gui).exec_() self._log_location("WARNING: %s" % msg) return False # Finalize book_mi book_mi.annotations = len(highlights) # Add book to books_db self.add_to_books_db(self.books_db, book_mi) self.annotated_book_list.append(book_mi) sorted_keys = sorted(list(highlights.keys())) for dt in sorted_keys: highlight_text = None if 'text' in highlights[dt]: highlight_text = highlights[dt]['text'] note_text = None if 'note' in highlights[dt]: note_text = highlights[dt]['note'] # Populate an AnnotationStruct a_mi = AnnotationStruct() a_mi.annotation_id = dt a_mi.book_id = book_mi['book_id'] a_mi.highlight_color = highlights[dt]['color'] a_mi.highlight_text = highlight_text a_mi.location = highlights[dt]['page'] a_mi.last_modification = dt a_mi.note_text = note_text # Location sort page_literal = re.match(r'^Page (?P<page>[0-9ivx]+).*$', a_mi.location).group('page') if re.match('[IXVL]', page_literal.upper()): whole = 0 decimal = self._roman_to_int(page_literal) else: whole = int(page_literal) decimal = 0 a_mi.location_sort = "%05d.%05d" % (whole, decimal) # Add annotation self.add_to_annotations_db(self.annotations_db, a_mi) self.update_book_last_annotation(self.books_db, dt, book_mi['book_id']) # Update the timestamp self.update_timestamp(self.annotations_db) self.update_timestamp(self.books_db) self.commit() return True
def parse_exported_highlights(self, raw, log_failure=True): """ Extract highlights from pasted Annotations summary, add them to selected book in calibre library Construct a BookStruct object with the book's metadata. Starred items are minimally required. BookStruct properties: *active: [True|False] *author: "John Smith" author_sort: (if known) *book_id: an int uniquely identifying the book. Highlights are associated with books through book_id genre: "Fiction" (if known) *title: "The Story of John Smith" title_sort: "Story of John Smith, The" (if known) uuid: Calibre's uuid for this book, if known Construct an AnnotationStruct object with the highlight's metadata. Starred items are minimally required. Dashed items (highlight_text and note_text) may be one or both. AnnotationStruct properties: annotation_id: an int uniquely identifying the annotation *book_id: The book this annotation is associated with highlight_color: [Blue|Gray|Green|Pink|Purple|Underline|Yellow] -highlight_text: A list of paragraphs constituting the highlight last_modification: The timestamp of the annotation location: location of highlight in the book -note_text: A list of paragraphs constituting the note *timestamp: Unique timestamp of highlight's creation/modification time """ # Create the annotations, books table as needed self.annotations_db = "%s_imported_annotations" % self.app_name_ self.create_annotations_table(self.annotations_db) self.books_db = "%s_imported_books" % self.app_name_ self.create_books_table(self.books_db) self.annotated_book_list = [] self.selected_books = None # Generate the book metadata from the selected book row = self.opts.gui.library_view.currentIndex() book_id = self.opts.gui.library_view.model().id(row) db = self.opts.gui.current_db mi = db.get_metadata(book_id, index_is_id=True) try: lines = raw.split('\n') if len(lines) < 5: raise AnnotationsException("Invalid annotations summary") index = 0 annotations = {} # Get the title, author, publisher from the first three lines title = lines[index] index += 1 author = lines[index] index += 1 publisher = lines[index] index += 1 # Next line should be the first timestamp/location while index < len(lines): tsl = re.match(r'^(?P<timestamp>.*) \((?P<location>Page .*)\)', lines[index]) if tsl: ts = tsl.group('timestamp') isoformat = parse_date(ts, as_utc=False) isoformat = isoformat.replace(hour=12) timestamp = mktime(isoformat.timetuple()) while timestamp in annotations: timestamp += 60 location = tsl.group('location') index += 1 # Continue with highlight highlight_text = lines[index] index += 1 # Next line is either Note: or a new tsl note = re.match(r'^Notes: (?P<note_text>.*)', lines[index]) note_text = None if note: note_text = note.group('note_text') index += 1 if re.match(r'^(?P<timestamp>.*) \((?P<location>Page .*)\)', lines[index]): # New note - store the old one, continue ann = AnnotationStruct() ann.book_id = mi.id ann.annotation_id = index ann.highlight_color = 'Yellow' ann.highlight_text = highlight_text ann.location = location ann.location_sort = "%05d" % int(re.match(r'^Page (?P<page>\d+).*$', location).group('page')) ann.note_text = note_text ann.last_modification = timestamp # Add annotation to db annotations[timestamp] = ann continue else: # Store the last one ann = AnnotationStruct() ann.book_id = mi.id ann.annotation_id = index ann.highlight_color = 'Yellow' ann.highlight_text = highlight_text ann.location = location ann.location_sort = "%05d" % int(re.match(r'^Page (?P<page>\d+).*$', location).group('page')) ann.note_text = note_text ann.last_modification = timestamp annotations[timestamp] = ann break except: if log_failure: self._log(" unable to parse %s Annotations" % self.app_name) self._log("{:~^80}".format(" Imported Annotation summary ")) self._log(raw) self._log("{:~^80}".format(" end imported Annotations summary ")) import traceback traceback.print_exc() msg = ('Unable to parse Annotation summary from %s. ' % self.app_name + 'Paste entire contents of emailed summary.') MessageBox(MessageBox.WARNING, 'Error importing annotations', msg, show_copy_button=False, parent=self.opts.gui).exec_() self._log_location("WARNING: %s" % msg) return False # Populate a BookStruct book_mi = BookStruct() book_mi.active = True book_mi.author = author book_mi.book_id = mi.id book_mi.title = title book_mi.uuid = None book_mi.last_update = time.mktime(time.localtime()) book_mi.reader_app = self.app_name book_mi.cid = mi.id book_mi.annotations = len(annotations) # Add book to books_db self.add_to_books_db(self.books_db, book_mi) self.annotated_book_list.append(book_mi) # Add the annotations for timestamp in sorted(annotations.keys()): self.add_to_annotations_db(self.annotations_db, annotations[timestamp]) self.update_book_last_annotation(self.books_db, timestamp, mi.id) self.opts.pb.increment() self.update_book_last_annotation(self.books_db, timestamp, mi.id) # Update the timestamp self.update_timestamp(self.annotations_db) self.update_timestamp(self.books_db) self.commit() # Return True if successful return True