E-book viewer: When exporting highlights, add a new Markdown export type, where the date of the highlight becomes a link to open the book in calibre showing the highlight

This commit is contained in:
Kovid Goyal 2020-12-24 13:08:13 +05:30
parent 1e123e35cf
commit 814afdf189
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 73 additions and 18 deletions

View File

@ -7,11 +7,13 @@ import json
import os import os
from functools import partial from functools import partial
from PyQt5.Qt import ( from PyQt5.Qt import (
QApplication, QCheckBox, QComboBox, QCursor, QDateTime, QFont, QFormLayout, QDialog, QAbstractItemView, QApplication, QCheckBox, QComboBox, QCursor, QDateTime,
QHBoxLayout, QIcon, QKeySequence, QLabel, QMenu, QPalette, QPlainTextEdit, QSize, QDialog, QDialogButtonBox, QFont, QFormLayout, QFrame, QHBoxLayout, QIcon,
QSplitter, Qt, QTextBrowser, QTimer, QToolButton, QTreeWidget, QTreeWidgetItem, QFrame, QKeySequence, QLabel, QMenu, QPalette, QPlainTextEdit, QSize, QSplitter, Qt,
QVBoxLayout, QWidget, pyqtSignal, QAbstractItemView, QDialogButtonBox QTextBrowser, QTimer, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
QWidget, pyqtSignal
) )
from urllib.parse import quote
from calibre import prepare_string_for_xml from calibre import prepare_string_for_xml
from calibre.ebooks.metadata import authors_to_string, fmt_sidx from calibre.ebooks.metadata import authors_to_string, fmt_sidx
@ -22,24 +24,39 @@ from calibre.gui2.widgets2 import Dialog
# rendering {{{ # rendering {{{
def render_highlight_as_text(hl, lines):
def render_highlight_as_text(hl, lines, as_markdown=False, link_prefix=None):
lines.append(hl['highlighted_text']) lines.append(hl['highlighted_text'])
date = QDateTime.fromString(hl['timestamp'], Qt.DateFormat.ISODate).toLocalTime().toString(Qt.DateFormat.SystemLocaleShortDate) date = QDateTime.fromString(hl['timestamp'], Qt.DateFormat.ISODate).toLocalTime().toString(Qt.DateFormat.SystemLocaleShortDate)
if as_markdown and link_prefix:
cfi = hl['start_cfi']
spine_index = (1 + hl['spine_index']) * 2
link = (link_prefix + quote(f'epubcfi(/{spine_index}{cfi})')).replace(')', '%29')
date = f'[{date}]({link})'
lines.append(date) lines.append(date)
notes = hl.get('notes') notes = hl.get('notes')
if notes: if notes:
lines.append('') lines.append('')
lines.append(notes) lines.append(notes)
lines.append('') lines.append('')
if as_markdown:
lines.append('-' * 20)
else:
lines.append('───') lines.append('───')
lines.append('') lines.append('')
def render_bookmark_as_text(b, lines): def render_bookmark_as_text(b, lines, as_markdown=False, link_prefix=None):
lines.append(b['title']) lines.append(b['title'])
date = QDateTime.fromString(b['timestamp'], Qt.DateFormat.ISODate).toLocalTime().toString(Qt.DateFormat.SystemLocaleShortDate) date = QDateTime.fromString(b['timestamp'], Qt.DateFormat.ISODate).toLocalTime().toString(Qt.DateFormat.SystemLocaleShortDate)
if as_markdown and link_prefix and b['pos_type'] == 'epubcfi':
link = (link_prefix + quote(b['pos'])).replace(')', '%29')
date = f'[{date}]({link})'
lines.append(date) lines.append(date)
lines.append('') lines.append('')
if as_markdown:
lines.append('-' * 20)
else:
lines.append('───') lines.append('───')
lines.append('') lines.append('')
@ -115,6 +132,7 @@ class Export(Dialog): # {{{
self.l = l = QFormLayout(self) self.l = l = QFormLayout(self)
self.export_format = ef = QComboBox(self) self.export_format = ef = QComboBox(self)
ef.addItem(_('Plain text'), 'txt') ef.addItem(_('Plain text'), 'txt')
ef.addItem(_('Markdown'), 'md')
ef.addItem(*self.file_type_data()) ef.addItem(*self.file_type_data())
idx = ef.findData(self.prefs[self.pref_name]) idx = ef.findData(self.prefs[self.pref_name])
if idx > -1: if idx > -1:
@ -151,7 +169,8 @@ class Export(Dialog): # {{{
self.accept() self.accept()
def exported_data(self): def exported_data(self):
if self.export_format.currentData() == 'calibre_annotation_collection': fmt = self.export_format.currentData()
if fmt == 'calibre_annotation_collection':
return json.dumps({ return json.dumps({
'version': 1, 'version': 1,
'type': 'calibre_annotation_collection', 'type': 'calibre_annotation_collection',
@ -160,6 +179,10 @@ class Export(Dialog): # {{{
lines = [] lines = []
db = current_db() db = current_db()
bid_groups = {} bid_groups = {}
as_markdown = fmt == 'md'
library_id = getattr(db, 'server_library_id', None)
if library_id:
library_id = '_hex_-' + library_id.encode('utf-8').hex()
for a in self.annotations: for a in self.annotations:
bid_groups.setdefault(a['book_id'], []).append(a) bid_groups.setdefault(a['book_id'], []).append(a)
for book_id, group in bid_groups.items(): for book_id, group in bid_groups.items():
@ -167,10 +190,14 @@ class Export(Dialog): # {{{
lines.append('') lines.append('')
for a in group: for a in group:
atype = a['type'] atype = a['type']
if library_id:
link_prefix = f'calibre://show-book/{library_id}/{book_id}/{a["format"]}?open_at='
else:
link_prefix = None
if atype == 'highlight': if atype == 'highlight':
render_highlight_as_text(a, lines) render_highlight_as_text(a, lines, as_markdown=as_markdown, link_prefix=link_prefix)
elif atype == 'bookmark': elif atype == 'bookmark':
render_bookmark_as_text(a, lines) render_bookmark_as_text(a, lines, as_markdown=as_markdown, link_prefix=link_prefix)
lines.append('') lines.append('')
return '\n'.join(lines).strip() return '\n'.join(lines).strip()
# }}} # }}}

View File

@ -3,3 +3,7 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
def get_current_book_data(set_val=False):
if set_val is not False:
setattr(get_current_book_data, 'ans', set_val)
return getattr(get_current_book_data, 'ans', {})

View File

@ -23,6 +23,7 @@ from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.library.annotations import ( from calibre.gui2.library.annotations import (
Details, Export as ExportBase, render_highlight_as_text, render_notes Details, Export as ExportBase, render_highlight_as_text, render_notes
) )
from calibre.gui2.viewer import get_current_book_data
from calibre.gui2.viewer.config import vprefs from calibre.gui2.viewer.config import vprefs
from calibre.gui2.viewer.search import SearchInput from calibre.gui2.viewer.search import SearchInput
from calibre.gui2.viewer.shortcuts import get_shortcut_for, index_to_key_sequence from calibre.gui2.viewer.shortcuts import get_shortcut_for, index_to_key_sequence
@ -130,15 +131,31 @@ class Export(ExportBase):
return _('highlights') return _('highlights')
def exported_data(self): def exported_data(self):
if self.export_format.currentData() == 'calibre_highlights': cbd = get_current_book_data()
link_prefix = library_id = None
if 'calibre_library_id' in cbd:
library_id = cbd['calibre_library_id']
book_id = cbd['calibre_book_id']
book_fmt = cbd['calibre_book_fmt']
elif cbd.get('book_library_details'):
bld = cbd['book_library_details']
book_id = bld['book_id']
book_fmt = bld['fmt'].upper()
library_id = bld['library_id']
if library_id:
library_id = '_hex_-' + library_id.encode('utf-8').hex()
link_prefix = f'calibre://show-book/{library_id}/{book_id}/{book_fmt}?open_at='
fmt = self.export_format.currentData()
if fmt == 'calibre_highlights':
return json.dumps({ return json.dumps({
'version': 1, 'version': 1,
'type': 'calibre_highlights', 'type': 'calibre_highlights',
'highlights': self.annotations, 'highlights': self.annotations,
}, ensure_ascii=False, sort_keys=True, indent=2) }, ensure_ascii=False, sort_keys=True, indent=2)
lines = [] lines = []
as_markdown = fmt == 'md'
for hl in self.annotations: for hl in self.annotations:
render_highlight_as_text(hl, lines) render_highlight_as_text(hl, lines, as_markdown=as_markdown, link_prefix=link_prefix)
return '\n'.join(lines).strip() return '\n'.join(lines).strip()

View File

@ -7,6 +7,7 @@ import re
def get_book_library_details(absolute_path_to_ebook): def get_book_library_details(absolute_path_to_ebook):
from calibre.srv.library_broker import correct_case_of_last_path_component, library_id_from_path
absolute_path_to_ebook = os.path.abspath(os.path.expanduser(absolute_path_to_ebook)) absolute_path_to_ebook = os.path.abspath(os.path.expanduser(absolute_path_to_ebook))
base = os.path.dirname(absolute_path_to_ebook) base = os.path.dirname(absolute_path_to_ebook)
m = re.search(r' \((\d+)\)$', os.path.basename(base)) m = re.search(r' \((\d+)\)$', os.path.basename(base))
@ -14,11 +15,13 @@ def get_book_library_details(absolute_path_to_ebook):
return return
book_id = int(m.group(1)) book_id = int(m.group(1))
library_dir = os.path.dirname(os.path.dirname(base)) library_dir = os.path.dirname(os.path.dirname(base))
corrected_path = correct_case_of_last_path_component(library_dir)
library_id = library_id_from_path(corrected_path)
dbpath = os.path.join(library_dir, 'metadata.db') dbpath = os.path.join(library_dir, 'metadata.db')
dbpath = os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH') or dbpath dbpath = os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH') or dbpath
if not os.path.exists(dbpath): if not os.path.exists(dbpath):
return return
return {'dbpath': dbpath, 'book_id': book_id, 'fmt': absolute_path_to_ebook.rpartition('.')[-1].upper()} return {'dbpath': dbpath, 'book_id': book_id, 'fmt': absolute_path_to_ebook.rpartition('.')[-1].upper(), 'library_id': library_id}
def database_has_annotations_support(cursor): def database_has_annotations_support(cursor):

View File

@ -9,12 +9,12 @@ import re
import sys import sys
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from hashlib import sha256 from hashlib import sha256
from threading import Thread
from PyQt5.Qt import ( from PyQt5.Qt import (
QApplication, QCursor, QDockWidget, QEvent, QMenu, QMimeData, QModelIndex, QApplication, QCursor, QDockWidget, QEvent, QMainWindow, QMenu, QMimeData,
QPixmap, Qt, QTimer, QToolBar, QUrl, QVBoxLayout, QWidget, pyqtSignal, QMainWindow QModelIndex, QPixmap, Qt, QTimer, QToolBar, QUrl, QVBoxLayout, QWidget,
pyqtSignal
) )
from threading import Thread
from calibre import prints from calibre import prints
from calibre.constants import DEBUG from calibre.constants import DEBUG
@ -24,6 +24,7 @@ from calibre.gui2 import choose_files, error_dialog
from calibre.gui2.dialogs.drm_error import DRMErrorMessage from calibre.gui2.dialogs.drm_error import DRMErrorMessage
from calibre.gui2.image_popup import ImagePopup from calibre.gui2.image_popup import ImagePopup
from calibre.gui2.main_window import MainWindow from calibre.gui2.main_window import MainWindow
from calibre.gui2.viewer import get_current_book_data
from calibre.gui2.viewer.annotations import ( from calibre.gui2.viewer.annotations import (
AnnotationsSaveWorker, annotations_dir, parse_annotations AnnotationsSaveWorker, annotations_dir, parse_annotations
) )
@ -109,6 +110,7 @@ class EbookViewer(MainWindow):
except EnvironmentError: except EnvironmentError:
pass pass
self.current_book_data = {} self.current_book_data = {}
get_current_book_data(self.current_book_data)
self.book_prepared.connect(self.load_finished, type=Qt.ConnectionType.QueuedConnection) self.book_prepared.connect(self.load_finished, type=Qt.ConnectionType.QueuedConnection)
self.dock_defs = dock_defs() self.dock_defs = dock_defs()
@ -457,6 +459,7 @@ class EbookViewer(MainWindow):
self.loading_overlay(_('Loading book, please wait')) self.loading_overlay(_('Loading book, please wait'))
self.save_annotations() self.save_annotations()
self.current_book_data = {} self.current_book_data = {}
get_current_book_data(self.current_book_data)
self.search_widget.clear_searches() self.search_widget.clear_searches()
t = Thread(name='LoadBook', target=self._load_ebook_worker, args=(pathtoebook, open_at, reload_book or self.force_reload)) t = Thread(name='LoadBook', target=self._load_ebook_worker, args=(pathtoebook, open_at, reload_book or self.force_reload))
t.daemon = True t.daemon = True
@ -513,6 +516,7 @@ class EbookViewer(MainWindow):
self.load_ebook(data['pathtoebook'], open_at=data['open_at'], reload_book=True) self.load_ebook(data['pathtoebook'], open_at=data['open_at'], reload_book=True)
return return
self.current_book_data = data self.current_book_data = data
get_current_book_data(self.current_book_data)
self.current_book_data['annotations_map'] = defaultdict(list) self.current_book_data['annotations_map'] = defaultdict(list)
self.current_book_data['annotations_path_key'] = path_key(data['pathtoebook']) + '.json' self.current_book_data['annotations_path_key'] = path_key(data['pathtoebook']) + '.json'
self.load_book_data(cbd) self.load_book_data(cbd)