mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Code to read and write annotations to the calibre db
This commit is contained in:
parent
628ce9aa84
commit
6bda5e6aad
@ -152,7 +152,7 @@ CREATE TABLE annotations ( id INTEGER PRIMARY KEY,
|
|||||||
annot_type TEXT NOT NULL,
|
annot_type TEXT NOT NULL,
|
||||||
annot_data TEXT NOT NULL,
|
annot_data TEXT NOT NULL,
|
||||||
searchable_text TEXT NOT NULL,
|
searchable_text TEXT NOT NULL,
|
||||||
UNIQUE(book, user_type, user, format, annot_id)
|
UNIQUE(book, user_type, user, format, annot_type, annot_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE VIEW meta AS
|
CREATE VIEW meta AS
|
||||||
|
@ -283,6 +283,41 @@ def AumSortedConcatenate():
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
# Annotations {{{
|
||||||
|
def annotations_for_book(cursor, book_id, fmt, user_type='local', user='viewer'):
|
||||||
|
for (data,) in cursor.execute(
|
||||||
|
'SELECT annot_data FROM annotations WHERE book=? AND format=? AND user_type=? AND user=?',
|
||||||
|
(book_id, fmt.upper(), user_type, user)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
yield json.loads(data)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def save_annotations_for_book(cursor, book_id, fmt, annots_list, user_type='local', user='viewer'):
|
||||||
|
data = []
|
||||||
|
fmt = fmt.upper()
|
||||||
|
for annot, timestamp_in_secs in annots_list:
|
||||||
|
atype = annot['type']
|
||||||
|
if atype == 'bookmark':
|
||||||
|
aid = text = annot['title']
|
||||||
|
elif atype == 'highlight':
|
||||||
|
aid = annot['uuid']
|
||||||
|
text = annot.get('highlighed_text') or ''
|
||||||
|
notes = annot.get('notes') or ''
|
||||||
|
if notes:
|
||||||
|
text += '0x1f\n\n' + notes
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
data.append((book_id, fmt, user_type, user, timestamp_in_secs, aid, atype, json.dumps(annot), text))
|
||||||
|
cursor.executemany(
|
||||||
|
'INSERT OR REPLACE INTO annotations (book, format, user_type, user, timestamp, annot_id, annot_type, annot_data, searchable_text)'
|
||||||
|
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', data)
|
||||||
|
cursor.execute('INSERT OR IGNORE INTO annotations_dirtied (book) VALUES (?)', (book_id,))
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
class Connection(apsw.Connection): # {{{
|
class Connection(apsw.Connection): # {{{
|
||||||
|
|
||||||
BUSY_TIMEOUT = 10000 # milliseconds
|
BUSY_TIMEOUT = 10000 # milliseconds
|
||||||
@ -1724,6 +1759,10 @@ class DB(object):
|
|||||||
def get_ids_for_custom_book_data(self, name):
|
def get_ids_for_custom_book_data(self, name):
|
||||||
return frozenset(r[0] for r in self.execute('SELECT book FROM books_plugin_data WHERE name=?', (name,)))
|
return frozenset(r[0] for r in self.execute('SELECT book FROM books_plugin_data WHERE name=?', (name,)))
|
||||||
|
|
||||||
|
def annotations_for_book(self, book_id, fmt, user_type, user):
|
||||||
|
for x in annotations_for_book(self.conn, book_id, fmt, user_type, user):
|
||||||
|
yield x
|
||||||
|
|
||||||
def conversion_options(self, book_id, fmt):
|
def conversion_options(self, book_id, fmt):
|
||||||
for (data,) in self.conn.get('SELECT data FROM conversion_options WHERE book=? AND format=?', (book_id, fmt.upper())):
|
for (data,) in self.conn.get('SELECT data FROM conversion_options WHERE book=? AND format=?', (book_id, fmt.upper())):
|
||||||
if data:
|
if data:
|
||||||
|
@ -2279,6 +2279,13 @@ class Cache(object):
|
|||||||
if progress is not None:
|
if progress is not None:
|
||||||
progress(_('Completed'), total, total)
|
progress(_('Completed'), total, total)
|
||||||
|
|
||||||
|
@read_api
|
||||||
|
def annotations_map_for_book(self, book_id, fmt, user_type='local', user='viewer'):
|
||||||
|
ans = {}
|
||||||
|
for annot in self.backend.annotations_for_book(book_id, fmt, user_type, user):
|
||||||
|
ans.setdefault(annot['type'], []).append(annot)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
def import_library(library_key, importer, library_path, progress=None, abort=None):
|
def import_library(library_key, importer, library_path, progress=None, abort=None):
|
||||||
from calibre.db.backend import DB
|
from calibre.db.backend import DB
|
||||||
|
@ -715,7 +715,7 @@ CREATE TABLE annotations ( id INTEGER PRIMARY KEY,
|
|||||||
annot_type TEXT NOT NULL,
|
annot_type TEXT NOT NULL,
|
||||||
annot_data TEXT NOT NULL,
|
annot_data TEXT NOT NULL,
|
||||||
searchable_text TEXT NOT NULL,
|
searchable_text TEXT NOT NULL,
|
||||||
UNIQUE(book, user_type, user, format, annot_id)
|
UNIQUE(book, user_type, user, format, annot_type, annot_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
DROP INDEX IF EXISTS annot_idx;
|
DROP INDEX IF EXISTS annot_idx;
|
||||||
|
@ -6,7 +6,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, time
|
import os, time, json
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt5.Qt import Qt, QAction, pyqtSignal
|
from PyQt5.Qt import Qt, QAction, pyqtSignal
|
||||||
@ -19,7 +19,7 @@ from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
|||||||
from calibre.utils.config import prefs, tweaks
|
from calibre.utils.config import prefs, tweaks
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
from polyglot.builtins import unicode_type
|
from polyglot.builtins import unicode_type, as_bytes
|
||||||
|
|
||||||
|
|
||||||
class HistoryAction(QAction):
|
class HistoryAction(QAction):
|
||||||
@ -99,13 +99,21 @@ class ViewAction(InterfaceAction):
|
|||||||
id_ = self.gui.library_view.model().id(row)
|
id_ = self.gui.library_view.model().id(row)
|
||||||
self.view_format_by_id(id_, format)
|
self.view_format_by_id(id_, format)
|
||||||
|
|
||||||
|
def calibre_book_data(self, book_id, fmt):
|
||||||
|
db = self.gui.current_db.new_api
|
||||||
|
annotations_map = db.annotations_map_for_book(book_id, fmt)
|
||||||
|
return {
|
||||||
|
'book_id': book_id, 'uuid': db.field_for('uuid', book_id), 'fmt': fmt.upper(),
|
||||||
|
'annotations_map': annotations_map,
|
||||||
|
}
|
||||||
|
|
||||||
def view_format_by_id(self, id_, format):
|
def view_format_by_id(self, id_, format):
|
||||||
db = self.gui.current_db
|
db = self.gui.current_db
|
||||||
fmt_path = db.format_abspath(id_, format,
|
fmt_path = db.format_abspath(id_, format,
|
||||||
index_is_id=True)
|
index_is_id=True)
|
||||||
if fmt_path:
|
if fmt_path:
|
||||||
title = db.title(id_, index_is_id=True)
|
title = db.title(id_, index_is_id=True)
|
||||||
self._view_file(fmt_path)
|
self._view_file(fmt_path, calibre_book_data=self.calibre_book_data(id_, format))
|
||||||
self.update_history([(id_, title)])
|
self.update_history([(id_, title)])
|
||||||
|
|
||||||
def book_downloaded_for_viewing(self, job):
|
def book_downloaded_for_viewing(self, job):
|
||||||
@ -114,15 +122,20 @@ class ViewAction(InterfaceAction):
|
|||||||
return
|
return
|
||||||
self._view_file(job.result)
|
self._view_file(job.result)
|
||||||
|
|
||||||
def _launch_viewer(self, name=None, viewer='ebook-viewer', internal=True):
|
def _launch_viewer(self, name=None, viewer='ebook-viewer', internal=True, calibre_book_data=None):
|
||||||
self.gui.setCursor(Qt.BusyCursor)
|
self.gui.setCursor(Qt.BusyCursor)
|
||||||
try:
|
try:
|
||||||
if internal:
|
if internal:
|
||||||
args = [viewer]
|
args = [viewer]
|
||||||
if isosx and 'ebook' in viewer:
|
if isosx and 'ebook' in viewer:
|
||||||
args.append('--raise-window')
|
args.append('--raise-window')
|
||||||
|
|
||||||
if name is not None:
|
if name is not None:
|
||||||
args.append(name)
|
args.append(name)
|
||||||
|
if calibre_book_data is not None:
|
||||||
|
with PersistentTemporaryFile('.json') as ptf:
|
||||||
|
ptf.write(as_bytes(json.dumps(calibre_book_data)))
|
||||||
|
args.append('--internal-book-data=' + ptf.name)
|
||||||
self.gui.job_manager.launch_gui_app(viewer,
|
self.gui.job_manager.launch_gui_app(viewer,
|
||||||
kwargs=dict(args=args))
|
kwargs=dict(args=args))
|
||||||
else:
|
else:
|
||||||
@ -149,12 +162,12 @@ class ViewAction(InterfaceAction):
|
|||||||
finally:
|
finally:
|
||||||
self.gui.unsetCursor()
|
self.gui.unsetCursor()
|
||||||
|
|
||||||
def _view_file(self, name):
|
def _view_file(self, name, calibre_book_data=None):
|
||||||
ext = os.path.splitext(name)[1].upper().replace('.',
|
ext = os.path.splitext(name)[1].upper().replace('.',
|
||||||
'').replace('ORIGINAL_', '')
|
'').replace('ORIGINAL_', '')
|
||||||
viewer = 'lrfviewer' if ext == 'LRF' else 'ebook-viewer'
|
viewer = 'lrfviewer' if ext == 'LRF' else 'ebook-viewer'
|
||||||
internal = self.force_internal_viewer or ext in config['internally_viewed_formats']
|
internal = self.force_internal_viewer or ext in config['internally_viewed_formats']
|
||||||
self._launch_viewer(name, viewer, internal)
|
self._launch_viewer(name, viewer, internal, calibre_book_data=calibre_book_data)
|
||||||
|
|
||||||
def view_specific_format(self, triggered):
|
def view_specific_format(self, triggered):
|
||||||
rows = list(self.gui.library_view.selectionModel().selectedRows())
|
rows = list(self.gui.library_view.selectionModel().selectedRows())
|
||||||
|
@ -3,17 +3,26 @@
|
|||||||
# License: GPL v3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from calibre.gui2.viewer.convert_book import update_book
|
||||||
|
from calibre.gui2.viewer.integration import save_annotations_list_to_library
|
||||||
|
from calibre.gui2.viewer.web_view import viewer_config_dir
|
||||||
from calibre.srv.render_book import (
|
from calibre.srv.render_book import (
|
||||||
EPUB_FILE_TYPE_MAGIC, parse_annotation, parse_annotations as _parse_annotations
|
EPUB_FILE_TYPE_MAGIC, parse_annotation, parse_annotations as _parse_annotations
|
||||||
)
|
)
|
||||||
|
from calibre.utils.date import EPOCH
|
||||||
from calibre.utils.serialize import json_dumps
|
from calibre.utils.serialize import json_dumps
|
||||||
from calibre.utils.zipfile import safe_replace
|
from calibre.utils.zipfile import safe_replace
|
||||||
from polyglot.binary import as_base64_bytes
|
from polyglot.binary import as_base64_bytes
|
||||||
from polyglot.builtins import iteritems, itervalues
|
from polyglot.builtins import iteritems, itervalues
|
||||||
|
from polyglot.queue import Queue
|
||||||
|
|
||||||
|
annotations_dir = os.path.join(viewer_config_dir, 'annots')
|
||||||
|
|
||||||
|
|
||||||
def parse_annotations(raw):
|
def parse_annotations(raw):
|
||||||
@ -53,14 +62,17 @@ def serialize_annotation(annot):
|
|||||||
return annot
|
return annot
|
||||||
|
|
||||||
|
|
||||||
def serialize_annotations(annots_map):
|
def annotations_as_copied_list(annots_map):
|
||||||
ans = []
|
|
||||||
for atype, annots in iteritems(annots_map):
|
for atype, annots in iteritems(annots_map):
|
||||||
for annot in annots:
|
for annot in annots:
|
||||||
|
ts = (annot['timestamp'] - EPOCH).total_seconds()
|
||||||
annot = serialize_annotation(annot)
|
annot = serialize_annotation(annot)
|
||||||
annot['type'] = atype
|
annot['type'] = atype
|
||||||
ans.append(annot)
|
yield annot, ts
|
||||||
return json_dumps(ans)
|
|
||||||
|
|
||||||
|
def annot_list_as_bytes(annots):
|
||||||
|
return json_dumps(tuple(annot for annot, seconds in annots))
|
||||||
|
|
||||||
|
|
||||||
def split_lines(chunk, length=80):
|
def split_lines(chunk, length=80):
|
||||||
@ -78,3 +90,56 @@ def save_annots_to_epub(path, serialized_annots):
|
|||||||
with zf:
|
with zf:
|
||||||
serialized_annots = EPUB_FILE_TYPE_MAGIC + b'\n'.join(split_lines(as_base64_bytes(serialized_annots)))
|
serialized_annots = EPUB_FILE_TYPE_MAGIC + b'\n'.join(split_lines(as_base64_bytes(serialized_annots)))
|
||||||
safe_replace(zf, 'META-INF/calibre_bookmarks.txt', BytesIO(serialized_annots), add_missing=True)
|
safe_replace(zf, 'META-INF/calibre_bookmarks.txt', BytesIO(serialized_annots), add_missing=True)
|
||||||
|
|
||||||
|
|
||||||
|
def save_annotations(annotations_list, annotations_path_key, bld, pathtoebook, in_book_file):
|
||||||
|
annots = annot_list_as_bytes(annotations_list)
|
||||||
|
with open(os.path.join(annotations_dir, annotations_path_key), 'wb') as f:
|
||||||
|
f.write(annots)
|
||||||
|
if in_book_file and os.access(pathtoebook, os.W_OK):
|
||||||
|
before_stat = os.stat(pathtoebook)
|
||||||
|
save_annots_to_epub(pathtoebook, annots)
|
||||||
|
update_book(pathtoebook, before_stat, {'calibre-book-annotations.json': annots})
|
||||||
|
if bld:
|
||||||
|
save_annotations_list_to_library(bld, annotations_list)
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationsSaveWorker(Thread):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
Thread.__init__(self, name='AnnotSaveWorker')
|
||||||
|
self.daemon = True
|
||||||
|
self.queue = Queue()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
if self.is_alive():
|
||||||
|
self.queue.put(None)
|
||||||
|
self.join()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while True:
|
||||||
|
x = self.queue.get()
|
||||||
|
if x is None:
|
||||||
|
return
|
||||||
|
annotations_list = x['annotations_list']
|
||||||
|
annotations_path_key = x['annotations_path_key']
|
||||||
|
bld = x['book_library_details']
|
||||||
|
pathtoebook = x['pathtoebook']
|
||||||
|
in_book_file = x['in_book_file']
|
||||||
|
try:
|
||||||
|
save_annotations(annotations_list, annotations_path_key, bld, pathtoebook, in_book_file)
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def save_annotations(self, current_book_data, in_book_file=True):
|
||||||
|
alist = tuple(annotations_as_copied_list(current_book_data['annotations_map']))
|
||||||
|
ebp = current_book_data['pathtoebook']
|
||||||
|
can_save_in_book_file = ebp.lower.endswith('.epub')
|
||||||
|
self.queue.put({
|
||||||
|
'annotations_list': alist,
|
||||||
|
'annotations_path_key': current_book_data['annotations_path_key'],
|
||||||
|
'book_library_details': current_book_data['book_library_details'],
|
||||||
|
'pathtoebook': current_book_data['pathtoebook'],
|
||||||
|
'in_book_file': in_book_file and can_save_in_book_file
|
||||||
|
})
|
||||||
|
@ -5,10 +5,7 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from polyglot.functools import lru_cache
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=2)
|
|
||||||
def get_book_library_details(absolute_path_to_ebook):
|
def get_book_library_details(absolute_path_to_ebook):
|
||||||
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)
|
||||||
@ -22,3 +19,37 @@ def get_book_library_details(absolute_path_to_ebook):
|
|||||||
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()}
|
||||||
|
|
||||||
|
|
||||||
|
def load_annotations_map_from_library(book_library_details):
|
||||||
|
import apsw
|
||||||
|
from calibre.db.backend import annotations_for_book, Connection
|
||||||
|
ans = {}
|
||||||
|
dbpath = book_library_details['dbpath']
|
||||||
|
try:
|
||||||
|
conn = apsw.Connection(dbpath, flags=apsw.SQLITE_OPEN_READONLY)
|
||||||
|
except Exception:
|
||||||
|
return ans
|
||||||
|
try:
|
||||||
|
conn.setbusytimeout(Connection.BUSY_TIMEOUT)
|
||||||
|
for annot in annotations_for_book(conn.cursor(), book_library_details['book_id'], book_library_details['fmt']):
|
||||||
|
ans.setdefault(annot['type'], []).append(annot)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
def save_annotations_list_to_library(book_library_details, alist):
|
||||||
|
import apsw
|
||||||
|
from calibre.db.backend import save_annotations_for_book, Connection
|
||||||
|
dbpath = book_library_details['dbpath']
|
||||||
|
try:
|
||||||
|
conn = apsw.Connection(dbpath, flags=apsw.SQLITE_OPEN_READWRITE)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
conn.setbusytimeout(Connection.BUSY_TIMEOUT)
|
||||||
|
with conn:
|
||||||
|
save_annotations_for_book(conn.cursor(), book_library_details['book_id'], book_library_details['fmt'], alist)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
@ -195,6 +196,23 @@ def main(args=sys.argv):
|
|||||||
scheme.setFlags(QWebEngineUrlScheme.SecureScheme)
|
scheme.setFlags(QWebEngineUrlScheme.SecureScheme)
|
||||||
QWebEngineUrlScheme.registerScheme(scheme)
|
QWebEngineUrlScheme.registerScheme(scheme)
|
||||||
override = 'calibre-ebook-viewer' if islinux else None
|
override = 'calibre-ebook-viewer' if islinux else None
|
||||||
|
processed_args = []
|
||||||
|
internal_book_data = None
|
||||||
|
for arg in args:
|
||||||
|
if arg.startswith('--internal-book-data='):
|
||||||
|
internal_book_data = arg.split('=', 1)[1]
|
||||||
|
continue
|
||||||
|
processed_args.append(arg)
|
||||||
|
if internal_book_data:
|
||||||
|
try:
|
||||||
|
with lopen(internal_book_data, 'rb') as f:
|
||||||
|
internal_book_data = json.load(f)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.remove(internal_book_data)
|
||||||
|
except EnvironmentError:
|
||||||
|
pass
|
||||||
|
args = processed_args
|
||||||
app = Application(args, override_program_name=override, windows_app_uid=VIEWER_APP_UID)
|
app = Application(args, override_program_name=override, windows_app_uid=VIEWER_APP_UID)
|
||||||
|
|
||||||
parser = option_parser()
|
parser = option_parser()
|
||||||
@ -219,7 +237,9 @@ def main(args=sys.argv):
|
|||||||
app.load_builtin_fonts()
|
app.load_builtin_fonts()
|
||||||
app.setWindowIcon(QIcon(I('viewer.png')))
|
app.setWindowIcon(QIcon(I('viewer.png')))
|
||||||
migrate_previous_viewer_prefs()
|
migrate_previous_viewer_prefs()
|
||||||
main = EbookViewer(open_at=opts.open_at, continue_reading=opts.continue_reading, force_reload=opts.force_reload)
|
main = EbookViewer(
|
||||||
|
open_at=opts.open_at, continue_reading=opts.continue_reading, force_reload=opts.force_reload,
|
||||||
|
calibre_book_data=internal_book_data)
|
||||||
main.set_exception_handler()
|
main.set_exception_handler()
|
||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
acc.events.append(os.path.abspath(args[-1]))
|
acc.events.append(os.path.abspath(args[-1]))
|
||||||
|
@ -24,22 +24,22 @@ 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.annotations import (
|
from calibre.gui2.viewer.annotations import (
|
||||||
merge_annotations, parse_annotations, save_annots_to_epub, serialize_annotation,
|
AnnotationsSaveWorker, annotations_dir, merge_annotations, parse_annotations,
|
||||||
serialize_annotations
|
serialize_annotation
|
||||||
)
|
)
|
||||||
from calibre.gui2.viewer.bookmarks import BookmarkManager
|
from calibre.gui2.viewer.bookmarks import BookmarkManager
|
||||||
from calibre.gui2.viewer.convert_book import (
|
from calibre.gui2.viewer.convert_book import clean_running_workers, prepare_book
|
||||||
clean_running_workers, prepare_book, update_book
|
|
||||||
)
|
|
||||||
from calibre.gui2.viewer.highlights import HighlightsPanel
|
from calibre.gui2.viewer.highlights import HighlightsPanel
|
||||||
|
from calibre.gui2.viewer.integration import (
|
||||||
|
get_book_library_details, load_annotations_map_from_library
|
||||||
|
)
|
||||||
from calibre.gui2.viewer.lookup import Lookup
|
from calibre.gui2.viewer.lookup import Lookup
|
||||||
from calibre.gui2.viewer.overlay import LoadingOverlay
|
from calibre.gui2.viewer.overlay import LoadingOverlay
|
||||||
from calibre.gui2.viewer.search import SearchPanel
|
from calibre.gui2.viewer.search import SearchPanel
|
||||||
from calibre.gui2.viewer.toc import TOC, TOCSearch, TOCView
|
from calibre.gui2.viewer.toc import TOC, TOCSearch, TOCView
|
||||||
from calibre.gui2.viewer.toolbars import ActionsToolBar
|
from calibre.gui2.viewer.toolbars import ActionsToolBar
|
||||||
from calibre.gui2.viewer.web_view import (
|
from calibre.gui2.viewer.web_view import (
|
||||||
WebView, get_path_for_name, get_session_pref, set_book_path, viewer_config_dir,
|
WebView, get_path_for_name, get_session_pref, set_book_path, vprefs
|
||||||
vprefs
|
|
||||||
)
|
)
|
||||||
from calibre.utils.date import utcnow
|
from calibre.utils.date import utcnow
|
||||||
from calibre.utils.img import image_from_path
|
from calibre.utils.img import image_from_path
|
||||||
@ -47,9 +47,7 @@ from calibre.utils.ipc.simple_worker import WorkerError
|
|||||||
from calibre.utils.iso8601 import parse_iso8601
|
from calibre.utils.iso8601 import parse_iso8601
|
||||||
from calibre.utils.monotonic import monotonic
|
from calibre.utils.monotonic import monotonic
|
||||||
from calibre.utils.serialize import json_loads
|
from calibre.utils.serialize import json_loads
|
||||||
from polyglot.builtins import as_bytes, iteritems, itervalues, as_unicode
|
from polyglot.builtins import as_bytes, as_unicode, iteritems, itervalues
|
||||||
|
|
||||||
annotations_dir = os.path.join(viewer_config_dir, 'annots')
|
|
||||||
|
|
||||||
|
|
||||||
def is_float(x):
|
def is_float(x):
|
||||||
@ -88,8 +86,10 @@ class EbookViewer(MainWindow):
|
|||||||
book_prepared = pyqtSignal(object, object)
|
book_prepared = pyqtSignal(object, object)
|
||||||
MAIN_WINDOW_STATE_VERSION = 1
|
MAIN_WINDOW_STATE_VERSION = 1
|
||||||
|
|
||||||
def __init__(self, open_at=None, continue_reading=None, force_reload=False):
|
def __init__(self, open_at=None, continue_reading=None, force_reload=False, calibre_book_data=None):
|
||||||
MainWindow.__init__(self, None)
|
MainWindow.__init__(self, None)
|
||||||
|
self.annotations_saver = None
|
||||||
|
self.calibre_book_data_for_first_book = calibre_book_data
|
||||||
self.shutting_down = self.close_forced = False
|
self.shutting_down = self.close_forced = False
|
||||||
self.force_reload = force_reload
|
self.force_reload = force_reload
|
||||||
connect_lambda(self.book_preparation_started, self, lambda self: self.loading_overlay(_(
|
connect_lambda(self.book_preparation_started, self, lambda self: self.loading_overlay(_(
|
||||||
@ -479,6 +479,8 @@ class EbookViewer(MainWindow):
|
|||||||
self.book_preparation_started.emit()
|
self.book_preparation_started.emit()
|
||||||
|
|
||||||
def load_finished(self, ok, data):
|
def load_finished(self, ok, data):
|
||||||
|
cbd = self.calibre_book_data_for_first_book
|
||||||
|
self.calibre_book_data_for_first_book = None
|
||||||
if self.shutting_down:
|
if self.shutting_down:
|
||||||
return
|
return
|
||||||
open_at, self.pending_open_at = self.pending_open_at, None
|
open_at, self.pending_open_at = self.pending_open_at, None
|
||||||
@ -507,7 +509,7 @@ class EbookViewer(MainWindow):
|
|||||||
self.current_book_data = data
|
self.current_book_data = 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()
|
self.load_book_data(cbd)
|
||||||
self.update_window_title()
|
self.update_window_title()
|
||||||
initial_cfi = self.initial_cfi_for_current_book()
|
initial_cfi = self.initial_cfi_for_current_book()
|
||||||
initial_position = {'type': 'cfi', 'data': initial_cfi} if initial_cfi else None
|
initial_position = {'type': 'cfi', 'data': initial_cfi} if initial_cfi else None
|
||||||
@ -534,8 +536,13 @@ class EbookViewer(MainWindow):
|
|||||||
highlights=list(map(serialize_annotation, highlights))
|
highlights=list(map(serialize_annotation, highlights))
|
||||||
)
|
)
|
||||||
|
|
||||||
def load_book_data(self):
|
def load_book_data(self, calibre_book_data=None):
|
||||||
self.load_book_annotations()
|
self.current_book_data['book_library_details'] = get_book_library_details(self.current_book_data['pathtoebook'])
|
||||||
|
if calibre_book_data is not None:
|
||||||
|
self.current_book_data['calibre_book_id'] = calibre_book_data['book_id']
|
||||||
|
self.current_book_data['calibre_book_uuid'] = calibre_book_data['uuid']
|
||||||
|
self.current_book_data['calibre_book_fmt'] = calibre_book_data['fmt']
|
||||||
|
self.load_book_annotations(calibre_book_data)
|
||||||
path = os.path.join(self.current_book_data['base'], 'calibre-book-manifest.json')
|
path = os.path.join(self.current_book_data['base'], 'calibre-book-manifest.json')
|
||||||
with open(path, 'rb') as f:
|
with open(path, 'rb') as f:
|
||||||
raw = f.read()
|
raw = f.read()
|
||||||
@ -547,7 +554,7 @@ class EbookViewer(MainWindow):
|
|||||||
self.current_book_data['metadata'] = set_book_path.parsed_metadata
|
self.current_book_data['metadata'] = set_book_path.parsed_metadata
|
||||||
self.current_book_data['manifest'] = set_book_path.parsed_manifest
|
self.current_book_data['manifest'] = set_book_path.parsed_manifest
|
||||||
|
|
||||||
def load_book_annotations(self):
|
def load_book_annotations(self, calibre_book_data=None):
|
||||||
amap = self.current_book_data['annotations_map']
|
amap = self.current_book_data['annotations_map']
|
||||||
path = os.path.join(self.current_book_data['base'], 'calibre-book-annotations.json')
|
path = os.path.join(self.current_book_data['base'], 'calibre-book-annotations.json')
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
@ -559,6 +566,16 @@ class EbookViewer(MainWindow):
|
|||||||
with open(path, 'rb') as f:
|
with open(path, 'rb') as f:
|
||||||
raw = f.read()
|
raw = f.read()
|
||||||
merge_annotations(parse_annotations(raw), amap)
|
merge_annotations(parse_annotations(raw), amap)
|
||||||
|
if calibre_book_data is None:
|
||||||
|
bld = self.current_book_data['book_library_details']
|
||||||
|
if bld is not None:
|
||||||
|
amap = load_annotations_map_from_library(bld)
|
||||||
|
if amap:
|
||||||
|
for annot_type, annots in iteritems(self.calibre_book_data_for_first_book['annotations_map']):
|
||||||
|
merge_annotations(annots, amap)
|
||||||
|
else:
|
||||||
|
for annot_type, annots in iteritems(calibre_book_data['annotations_map']):
|
||||||
|
merge_annotations(annots, amap)
|
||||||
|
|
||||||
def update_window_title(self):
|
def update_window_title(self):
|
||||||
try:
|
try:
|
||||||
@ -590,17 +607,10 @@ class EbookViewer(MainWindow):
|
|||||||
def save_annotations(self, in_book_file=True):
|
def save_annotations(self, in_book_file=True):
|
||||||
if not self.current_book_data:
|
if not self.current_book_data:
|
||||||
return
|
return
|
||||||
amap = self.current_book_data['annotations_map']
|
if self.annotations_saver is None:
|
||||||
annots = as_bytes(serialize_annotations(amap))
|
self.annotations_saver = AnnotationsSaveWorker()
|
||||||
with open(os.path.join(annotations_dir, self.current_book_data['annotations_path_key']), 'wb') as f:
|
self.annotations_saver.start()
|
||||||
f.write(annots)
|
self.annotations_saver.save_annotations(self.current_book_data, in_book_file and get_session_pref('save_annotations_in_ebook', default=True))
|
||||||
if in_book_file and self.current_book_data.get('pathtoebook', '').lower().endswith(
|
|
||||||
'.epub') and get_session_pref('save_annotations_in_ebook', default=True):
|
|
||||||
path = self.current_book_data['pathtoebook']
|
|
||||||
if os.access(path, os.W_OK):
|
|
||||||
before_stat = os.stat(path)
|
|
||||||
save_annots_to_epub(path, annots)
|
|
||||||
update_book(path, before_stat, {'calibre-book-annotations.json': annots})
|
|
||||||
|
|
||||||
def highlights_changed(self, highlights):
|
def highlights_changed(self, highlights):
|
||||||
if not self.current_book_data:
|
if not self.current_book_data:
|
||||||
@ -649,11 +659,16 @@ class EbookViewer(MainWindow):
|
|||||||
QTimer.singleShot(2000, self.force_close)
|
QTimer.singleShot(2000, self.force_close)
|
||||||
self.web_view.prepare_for_close()
|
self.web_view.prepare_for_close()
|
||||||
return
|
return
|
||||||
|
if self.shutting_down:
|
||||||
|
return
|
||||||
self.shutting_down = True
|
self.shutting_down = True
|
||||||
self.search_widget.shutdown()
|
self.search_widget.shutdown()
|
||||||
try:
|
try:
|
||||||
self.save_annotations()
|
|
||||||
self.save_state()
|
self.save_state()
|
||||||
|
self.save_annotations()
|
||||||
|
if self.annotations_saver is not None:
|
||||||
|
self.annotations_saver.shutdown()
|
||||||
|
self.annotations_saver = None
|
||||||
except Exception:
|
except Exception:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user