mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
E-book viewer: Use IPC to update annotations when calibre is running. Avoids possible loss of annotations due to db being locked. See #2103990 (Problems with the Annotations Browser)
This commit is contained in:
parent
47d18ccfec
commit
d6a1cbf72d
@ -5,6 +5,8 @@ from collections import defaultdict
|
|||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
from calibre.ebooks.epub.cfi.parse import cfi_sort_key
|
from calibre.ebooks.epub.cfi.parse import cfi_sort_key
|
||||||
|
from calibre.utils.date import EPOCH
|
||||||
|
from calibre.utils.iso8601 import parse_iso8601
|
||||||
from polyglot.builtins import itervalues
|
from polyglot.builtins import itervalues
|
||||||
|
|
||||||
no_cfi_sort_key = cfi_sort_key('/99999999')
|
no_cfi_sort_key = cfi_sort_key('/99999999')
|
||||||
@ -30,6 +32,15 @@ def highlight_sort_key(hl):
|
|||||||
return no_cfi_sort_key
|
return no_cfi_sort_key
|
||||||
|
|
||||||
|
|
||||||
|
def annotations_as_copied_list(annots_map):
|
||||||
|
for atype, annots in annots_map.items():
|
||||||
|
for annot in annots:
|
||||||
|
ts = (parse_iso8601(annot['timestamp'], assume_utc=True) - EPOCH).total_seconds()
|
||||||
|
annot = annot.copy()
|
||||||
|
annot['type'] = atype
|
||||||
|
yield annot, ts
|
||||||
|
|
||||||
|
|
||||||
def sort_annot_list_by_position_in_book(annots, annot_type):
|
def sort_annot_list_by_position_in_book(annots, annot_type):
|
||||||
annots.sort(key={'bookmark': bookmark_sort_key, 'highlight': highlight_sort_key}[annot_type])
|
annots.sort(key={'bookmark': bookmark_sort_key, 'highlight': highlight_sort_key}[annot_type])
|
||||||
|
|
||||||
|
@ -338,6 +338,26 @@ def save_annotations_for_book(cursor, book_id, fmt, annots_list, user_type='loca
|
|||||||
cursor.executemany(
|
cursor.executemany(
|
||||||
'INSERT OR REPLACE INTO annotations (book, format, user_type, user, timestamp, annot_id, annot_type, annot_data, searchable_text)'
|
'INSERT OR REPLACE INTO annotations (book, format, user_type, user, timestamp, annot_id, annot_type, annot_data, searchable_text)'
|
||||||
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', data)
|
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', data)
|
||||||
|
|
||||||
|
|
||||||
|
def save_annotations_list_to_cursor(cursor, alist, sync_annots_user, book_id, book_fmt):
|
||||||
|
from calibre.db.annotations import annotations_as_copied_list, merge_annotations
|
||||||
|
book_fmt = book_fmt.upper()
|
||||||
|
amap = {}
|
||||||
|
for annot in annotations_for_book(cursor, book_id, book_fmt):
|
||||||
|
amap.setdefault(annot['type'], []).append(annot)
|
||||||
|
merge_annotations((x[0] for x in alist), amap)
|
||||||
|
if sync_annots_user:
|
||||||
|
other_amap = {}
|
||||||
|
for annot in annotations_for_book(cursor, book_id, book_fmt, user_type='web', user=sync_annots_user):
|
||||||
|
other_amap.setdefault(annot['type'], []).append(annot)
|
||||||
|
merge_annotations(amap, other_amap)
|
||||||
|
alist = tuple(annotations_as_copied_list(amap))
|
||||||
|
save_annotations_for_book(cursor, book_id, book_fmt, alist)
|
||||||
|
if sync_annots_user:
|
||||||
|
alist = tuple(annotations_as_copied_list(other_amap))
|
||||||
|
save_annotations_for_book(cursor, book_id, book_fmt, alist, user_type='web', user=sync_annots_user)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
@ -2400,6 +2420,11 @@ class DB:
|
|||||||
def annotations_for_book(self, book_id, fmt, user_type, user):
|
def annotations_for_book(self, book_id, fmt, user_type, user):
|
||||||
yield from annotations_for_book(self.conn, book_id, fmt, user_type, user)
|
yield from annotations_for_book(self.conn, book_id, fmt, user_type, user)
|
||||||
|
|
||||||
|
def save_annotations_list(self, book_id, book_fmt, sync_annots_user, alist):
|
||||||
|
conn = self.conn
|
||||||
|
with conn:
|
||||||
|
save_annotations_list_to_cursor(conn.cursor(), alist, sync_annots_user, book_id, book_fmt)
|
||||||
|
|
||||||
def search_annotations(self,
|
def search_annotations(self,
|
||||||
fts_engine_query, use_stemming, highlight_start, highlight_end, snippet_size, annotation_type,
|
fts_engine_query, use_stemming, highlight_start, highlight_end, snippet_size, annotation_type,
|
||||||
restrict_to_book_ids, restrict_to_user, ignore_removed=False
|
restrict_to_book_ids, restrict_to_user, ignore_removed=False
|
||||||
|
@ -3307,6 +3307,10 @@ class Cache:
|
|||||||
alist.append((annot, ts))
|
alist.append((annot, ts))
|
||||||
self._set_annotations_for_book(book_id, fmt, alist, user_type=user_type, user=user)
|
self._set_annotations_for_book(book_id, fmt, alist, user_type=user_type, user=user)
|
||||||
|
|
||||||
|
@write_api
|
||||||
|
def save_annotations_list(self, book_id: int, book_fmt: str, sync_annots_user: str, alist: list[dict]) -> None:
|
||||||
|
self.backend.save_annotations_list(book_id, book_fmt, sync_annots_user, alist)
|
||||||
|
|
||||||
@write_api
|
@write_api
|
||||||
def reindex_annotations(self):
|
def reindex_annotations(self):
|
||||||
self.backend.reindex_annotations()
|
self.backend.reindex_annotations()
|
||||||
|
@ -136,7 +136,8 @@ class ViewAction(InterfaceAction):
|
|||||||
merge_annotations(other_annotations_map, annotations_map, merge_last_read=False)
|
merge_annotations(other_annotations_map, annotations_map, merge_last_read=False)
|
||||||
return {
|
return {
|
||||||
'book_id': book_id, 'uuid': db.field_for('uuid', book_id), 'fmt': fmt.upper(),
|
'book_id': book_id, 'uuid': db.field_for('uuid', book_id), 'fmt': fmt.upper(),
|
||||||
'annotations_map': annotations_map, 'library_id': getattr(self.gui.current_db.new_api, 'server_library_id', None)
|
'annotations_map': annotations_map, 'library_id': getattr(self.gui.current_db.new_api, 'server_library_id', None),
|
||||||
|
'calibre_pid': os.getpid(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def view_format_by_id(self, id_, format, open_at=None):
|
def view_format_by_id(self, id_, format, open_at=None):
|
||||||
|
@ -889,6 +889,15 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
self.activateWindow()
|
self.activateWindow()
|
||||||
elif msg.startswith('shutdown:'):
|
elif msg.startswith('shutdown:'):
|
||||||
self.quit(confirm_quit=False)
|
self.quit(confirm_quit=False)
|
||||||
|
elif msg.startswith('save-annotations:'):
|
||||||
|
from calibre.gui2.viewer.integration import save_annotations_in_gui
|
||||||
|
try:
|
||||||
|
if not save_annotations_in_gui(self.library_broker, msg[len('save-annotations:'):]):
|
||||||
|
print('Failed to update annotations for book from viewer, book or library not found.', file=sys.stderr)
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
error_dialog(self, _('Failed to update annotations'), _(
|
||||||
|
'Failed to update annotations in the database for the book being currently viewed.'), det_msg=traceback.format_exc(), show=True)
|
||||||
elif msg.startswith('bookedited:'):
|
elif msg.startswith('bookedited:'):
|
||||||
parts = msg.split(':')[1:]
|
parts = msg.split(':')[1:]
|
||||||
try:
|
try:
|
||||||
|
@ -7,32 +7,20 @@ from io import BytesIO
|
|||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from calibre.db.annotations import merge_annot_lists
|
from calibre.db.annotations import annotations_as_copied_list, merge_annot_lists
|
||||||
from calibre.gui2.viewer.convert_book import update_book
|
from calibre.gui2.viewer.convert_book import update_book
|
||||||
from calibre.gui2.viewer.integration import save_annotations_list_to_library
|
from calibre.gui2.viewer.integration import save_annotations_list_to_library
|
||||||
from calibre.gui2.viewer.web_view import viewer_config_dir
|
from calibre.gui2.viewer.web_view import viewer_config_dir
|
||||||
from calibre.srv.render_book import EPUB_FILE_TYPE_MAGIC
|
from calibre.srv.render_book import EPUB_FILE_TYPE_MAGIC
|
||||||
from calibre.utils.date import EPOCH
|
|
||||||
from calibre.utils.iso8601 import parse_iso8601
|
|
||||||
from calibre.utils.serialize import json_dumps, json_loads
|
from calibre.utils.serialize import json_dumps, json_loads
|
||||||
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
|
|
||||||
from polyglot.queue import Queue
|
from polyglot.queue import Queue
|
||||||
|
|
||||||
annotations_dir = os.path.join(viewer_config_dir, 'annots')
|
annotations_dir = os.path.join(viewer_config_dir, 'annots')
|
||||||
parse_annotations = json_loads
|
parse_annotations = json_loads
|
||||||
|
|
||||||
|
|
||||||
def annotations_as_copied_list(annots_map):
|
|
||||||
for atype, annots in iteritems(annots_map):
|
|
||||||
for annot in annots:
|
|
||||||
ts = (parse_iso8601(annot['timestamp'], assume_utc=True) - EPOCH).total_seconds()
|
|
||||||
annot = annot.copy()
|
|
||||||
annot['type'] = atype
|
|
||||||
yield annot, ts
|
|
||||||
|
|
||||||
|
|
||||||
def annot_list_as_bytes(annots):
|
def annot_list_as_bytes(annots):
|
||||||
return json_dumps(tuple(annot for annot, seconds in annots))
|
return json_dumps(tuple(annot for annot, seconds in annots))
|
||||||
|
|
||||||
@ -54,7 +42,7 @@ def save_annots_to_epub(path, 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, sync_annots_user):
|
def save_annotations(annotations_list, annotations_path_key, bld, pathtoebook, in_book_file, sync_annots_user, calibre_data):
|
||||||
annots = annot_list_as_bytes(annotations_list)
|
annots = annot_list_as_bytes(annotations_list)
|
||||||
with open(os.path.join(annotations_dir, annotations_path_key), 'wb') as f:
|
with open(os.path.join(annotations_dir, annotations_path_key), 'wb') as f:
|
||||||
f.write(annots)
|
f.write(annots)
|
||||||
@ -63,7 +51,7 @@ def save_annotations(annotations_list, annotations_path_key, bld, pathtoebook, i
|
|||||||
save_annots_to_epub(pathtoebook, annots)
|
save_annots_to_epub(pathtoebook, annots)
|
||||||
update_book(pathtoebook, before_stat, {'calibre-book-annotations.json': annots})
|
update_book(pathtoebook, before_stat, {'calibre-book-annotations.json': annots})
|
||||||
if bld:
|
if bld:
|
||||||
save_annotations_list_to_library(bld, annotations_list, sync_annots_user)
|
save_annotations_list_to_library(bld, annotations_list, sync_annots_user, calibre_data=calibre_data)
|
||||||
|
|
||||||
|
|
||||||
class AnnotationsSaveWorker(Thread):
|
class AnnotationsSaveWorker(Thread):
|
||||||
@ -90,7 +78,7 @@ class AnnotationsSaveWorker(Thread):
|
|||||||
in_book_file = x['in_book_file']
|
in_book_file = x['in_book_file']
|
||||||
sync_annots_user = x['sync_annots_user']
|
sync_annots_user = x['sync_annots_user']
|
||||||
try:
|
try:
|
||||||
save_annotations(annotations_list, annotations_path_key, bld, pathtoebook, in_book_file, sync_annots_user)
|
save_annotations(annotations_list, annotations_path_key, bld, pathtoebook, in_book_file, sync_annots_user, x['calibre_data'])
|
||||||
except Exception:
|
except Exception:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -107,6 +95,11 @@ class AnnotationsSaveWorker(Thread):
|
|||||||
'pathtoebook': current_book_data['pathtoebook'],
|
'pathtoebook': current_book_data['pathtoebook'],
|
||||||
'in_book_file': in_book_file and can_save_in_book_file,
|
'in_book_file': in_book_file and can_save_in_book_file,
|
||||||
'sync_annots_user': sync_annots_user,
|
'sync_annots_user': sync_annots_user,
|
||||||
|
'calibre_data': {
|
||||||
|
'library_id': current_book_data.get('calibre_library_id'),
|
||||||
|
'book_id': current_book_data.get('calibre_book_id'),
|
||||||
|
'book_fmt': current_book_data.get('calibre_book_fmt'),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,12 +52,48 @@ def load_annotations_map_from_library(book_library_details, user_type='local', u
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
def save_annotations_list_to_library(book_library_details, alist, sync_annots_user=''):
|
def send_msg_to_calibre(alist, sync_annots_user, library_id, book_id, book_fmt) -> bool:
|
||||||
|
import json
|
||||||
|
|
||||||
|
from calibre.gui2.listener import send_message_in_process
|
||||||
|
|
||||||
|
packet = json.dumps({
|
||||||
|
'alist': alist,
|
||||||
|
'sync_annots_user': sync_annots_user,
|
||||||
|
'library_id': library_id,
|
||||||
|
'book_id': book_id,
|
||||||
|
'book_fmt': book_fmt,
|
||||||
|
})
|
||||||
|
msg = 'save-annotations:' + packet
|
||||||
|
try:
|
||||||
|
send_message_in_process(msg)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def save_annotations_in_gui(library_broker, msg) -> bool:
|
||||||
|
import json
|
||||||
|
data = json.loads(msg)
|
||||||
|
db = library_broker.get(data['library_id'])
|
||||||
|
if db:
|
||||||
|
db = db.new_api
|
||||||
|
with db.write_lock:
|
||||||
|
if db._has_format(data['book_id'], data['book_fmt']):
|
||||||
|
db._save_annotations_list(int(data['book_id']), data['book_fmt'].upper(), data['sync_annots_user'], data['alist'])
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def save_annotations_list_to_library(book_library_details, alist, sync_annots_user='', calibre_data=None):
|
||||||
|
calibre_data = calibre_data or {}
|
||||||
|
if calibre_data.get('library_id') and calibre_data.get('book_id') and send_msg_to_calibre(
|
||||||
|
alist, sync_annots_user, calibre_data['library_id'], calibre_data['book_id'], calibre_data['book_fmt']):
|
||||||
|
return
|
||||||
|
|
||||||
import apsw
|
import apsw
|
||||||
|
|
||||||
from calibre.db.annotations import merge_annotations
|
from calibre.db.backend import Connection, save_annotations_list_to_cursor
|
||||||
from calibre.db.backend import Connection, annotations_for_book, save_annotations_for_book
|
|
||||||
from calibre.gui2.viewer.annotations import annotations_as_copied_list
|
|
||||||
dbpath = book_library_details['dbpath']
|
dbpath = book_library_details['dbpath']
|
||||||
try:
|
try:
|
||||||
conn = apsw.Connection(dbpath, flags=apsw.SQLITE_OPEN_READWRITE)
|
conn = apsw.Connection(dbpath, flags=apsw.SQLITE_OPEN_READWRITE)
|
||||||
@ -67,21 +103,7 @@ def save_annotations_list_to_library(book_library_details, alist, sync_annots_us
|
|||||||
conn.setbusytimeout(Connection.BUSY_TIMEOUT)
|
conn.setbusytimeout(Connection.BUSY_TIMEOUT)
|
||||||
if not database_has_annotations_support(conn.cursor()):
|
if not database_has_annotations_support(conn.cursor()):
|
||||||
return
|
return
|
||||||
amap = {}
|
|
||||||
with conn:
|
with conn:
|
||||||
cursor = conn.cursor()
|
save_annotations_list_to_cursor(conn.cursor(), alist, sync_annots_user, book_library_details['book_id'], book_library_details['fmt'])
|
||||||
for annot in annotations_for_book(cursor, book_library_details['book_id'], book_library_details['fmt']):
|
|
||||||
amap.setdefault(annot['type'], []).append(annot)
|
|
||||||
merge_annotations((x[0] for x in alist), amap)
|
|
||||||
if sync_annots_user:
|
|
||||||
other_amap = {}
|
|
||||||
for annot in annotations_for_book(cursor, book_library_details['book_id'], book_library_details['fmt'], user_type='web', user=sync_annots_user):
|
|
||||||
other_amap.setdefault(annot['type'], []).append(annot)
|
|
||||||
merge_annotations(amap, other_amap)
|
|
||||||
alist = tuple(annotations_as_copied_list(amap))
|
|
||||||
save_annotations_for_book(cursor, book_library_details['book_id'], book_library_details['fmt'], alist)
|
|
||||||
if sync_annots_user:
|
|
||||||
alist = tuple(annotations_as_copied_list(other_amap))
|
|
||||||
save_annotations_for_book(cursor, book_library_details['book_id'], book_library_details['fmt'], alist, user_type='web', user=sync_annots_user)
|
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user