Allow syninc annots from viewer to specific content server user

This commit is contained in:
Kovid Goyal 2020-07-11 20:42:26 +05:30
parent d11b9b6f29
commit 18c779a1a8
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 77 additions and 23 deletions

View File

@ -74,25 +74,29 @@ def merge_annot_lists(a, b, annot_type):
return c return c
def merge_annotations(annots, annots_map): def merge_annotations(annots, annots_map, merge_last_read=True):
# If you make changes to this algorithm also update the # If you make changes to this algorithm also update the
# implementation in read_book.annotations # implementation in read_book.annotations
amap = defaultdict(list) if isinstance(annots, dict):
for annot in annots: amap = annots
amap[annot['type']].append(annot) else:
amap = defaultdict(list)
for annot in annots:
amap[annot['type']].append(annot)
lr = amap.get('last-read') if merge_last_read:
if lr: lr = amap.get('last-read')
existing = annots_map.get('last-read')
if existing:
lr = existing + lr
if lr: if lr:
lr.sort(key=itemgetter('timestamp'), reverse=True) existing = annots_map.get('last-read')
annots_map['last-read'] = [lr[0]] if existing:
lr = existing + lr
if lr:
lr.sort(key=itemgetter('timestamp'), reverse=True)
annots_map['last-read'] = [lr[0]]
for annot_type, field in merge_field_map.items(): for annot_type, field in merge_field_map.items():
a = annots_map.get(annot_type) a = annots_map.get(annot_type)
b = amap[annot_type] b = amap.get(annot_type)
if not b: if not b:
continue continue
changed, annots_map[annot_type] = merge_annots_with_identical_field(a or [], b, field=field) changed, annots_map[annot_type] = merge_annots_with_identical_field(a or [], b, field=field)

View File

@ -100,8 +100,16 @@ class ViewAction(InterfaceAction):
self.view_format_by_id(id_, format) self.view_format_by_id(id_, format)
def calibre_book_data(self, book_id, fmt): def calibre_book_data(self, book_id, fmt):
from calibre.gui2.viewer.config import vprefs, get_session_pref
from calibre.db.annotations import merge_annotations
vprefs.refresh()
sync_annots_user = get_session_pref('sync_annots_user', default='')
db = self.gui.current_db.new_api db = self.gui.current_db.new_api
annotations_map = db.annotations_map_for_book(book_id, fmt) annotations_map = db.annotations_map_for_book(book_id, fmt)
if sync_annots_user:
other_annotations_map = db.annotations_map_for_book(book_id, fmt, user_type='web', user=sync_annots_user)
if other_annotations_map:
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, 'annotations_map': annotations_map,

View File

@ -55,7 +55,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): def save_annotations(annotations_list, annotations_path_key, bld, pathtoebook, in_book_file, sync_annots_user):
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)
@ -64,7 +64,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) save_annotations_list_to_library(bld, annotations_list, sync_annots_user)
class AnnotationsSaveWorker(Thread): class AnnotationsSaveWorker(Thread):
@ -89,13 +89,14 @@ class AnnotationsSaveWorker(Thread):
bld = x['book_library_details'] bld = x['book_library_details']
pathtoebook = x['pathtoebook'] pathtoebook = x['pathtoebook']
in_book_file = x['in_book_file'] in_book_file = x['in_book_file']
sync_annots_user = x['sync_annots_user']
try: try:
save_annotations(annotations_list, annotations_path_key, bld, pathtoebook, in_book_file) save_annotations(annotations_list, annotations_path_key, bld, pathtoebook, in_book_file, sync_annots_user)
except Exception: except Exception:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
def save_annotations(self, current_book_data, in_book_file=True): def save_annotations(self, current_book_data, in_book_file=True, sync_annots_user=''):
alist = tuple(annotations_as_copied_list(current_book_data['annotations_map'])) alist = tuple(annotations_as_copied_list(current_book_data['annotations_map']))
ebp = current_book_data['pathtoebook'] ebp = current_book_data['pathtoebook']
can_save_in_book_file = ebp.lower().endswith('.epub') can_save_in_book_file = ebp.lower().endswith('.epub')
@ -104,7 +105,8 @@ class AnnotationsSaveWorker(Thread):
'annotations_path_key': current_book_data['annotations_path_key'], 'annotations_path_key': current_book_data['annotations_path_key'],
'book_library_details': current_book_data['book_library_details'], 'book_library_details': current_book_data['book_library_details'],
'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,
}) })

View File

@ -25,7 +25,7 @@ def database_has_annotations_support(cursor):
return next(cursor.execute('pragma user_version;'))[0] > 23 return next(cursor.execute('pragma user_version;'))[0] > 23
def load_annotations_map_from_library(book_library_details): def load_annotations_map_from_library(book_library_details, user_type='local', user='viewer'):
import apsw import apsw
from calibre.db.backend import annotations_for_book, Connection from calibre.db.backend import annotations_for_book, Connection
ans = {} ans = {}
@ -39,14 +39,17 @@ def load_annotations_map_from_library(book_library_details):
cursor = conn.cursor() cursor = conn.cursor()
if not database_has_annotations_support(cursor): if not database_has_annotations_support(cursor):
return ans return ans
for annot in annotations_for_book(cursor, 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'],
user_type=user_type, user=user
):
ans.setdefault(annot['type'], []).append(annot) ans.setdefault(annot['type'], []).append(annot)
finally: finally:
conn.close() conn.close()
return ans return ans
def save_annotations_list_to_library(book_library_details, alist): def save_annotations_list_to_library(book_library_details, alist, sync_annots_user=''):
import apsw import apsw
from calibre.db.backend import save_annotations_for_book, Connection, annotations_for_book from calibre.db.backend import save_annotations_for_book, Connection, annotations_for_book
from calibre.gui2.viewer.annotations import annotations_as_copied_list from calibre.gui2.viewer.annotations import annotations_as_copied_list
@ -66,7 +69,15 @@ def save_annotations_list_to_library(book_library_details, alist):
for annot in annotations_for_book(cursor, 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) amap.setdefault(annot['type'], []).append(annot)
merge_annotations((x[0] for x in alist), amap) 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)) alist = tuple(annotations_as_copied_list(amap))
save_annotations_for_book(cursor, book_library_details['book_id'], book_library_details['fmt'], alist) 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()

View File

@ -564,6 +564,11 @@ class EbookViewer(MainWindow):
bld = self.current_book_data['book_library_details'] bld = self.current_book_data['book_library_details']
if bld is not None: if bld is not None:
lib_amap = load_annotations_map_from_library(bld) lib_amap = load_annotations_map_from_library(bld)
sau = get_session_pref('sync_annots_user', default='')
if sau:
other_amap = load_annotations_map_from_library(bld, user_type='web', user=sau)
if other_amap:
merge_annotations(other_amap, lib_amap)
if lib_amap: if lib_amap:
for annot_type, annots in iteritems(lib_amap): for annot_type, annots in iteritems(lib_amap):
merge_annotations(annots, amap) merge_annotations(annots, amap)
@ -604,7 +609,11 @@ class EbookViewer(MainWindow):
if self.annotations_saver is None: if self.annotations_saver is None:
self.annotations_saver = AnnotationsSaveWorker() self.annotations_saver = AnnotationsSaveWorker()
self.annotations_saver.start() self.annotations_saver.start()
self.annotations_saver.save_annotations(self.current_book_data, in_book_file and get_session_pref('save_annotations_in_ebook', default=True)) self.annotations_saver.save_annotations(
self.current_book_data,
in_book_file and get_session_pref('save_annotations_in_ebook', default=True),
get_session_pref('sync_annots_user', default='')
)
def highlights_changed(self, highlights): def highlights_changed(self, highlights):
if not self.current_book_data: if not self.current_book_data:

View File

@ -19,6 +19,7 @@ DEFAULTS = {
'show_actions_toolbar': False, 'show_actions_toolbar': False,
'show_actions_toolbar_in_fullscreen': False, 'show_actions_toolbar_in_fullscreen': False,
'save_annotations_in_ebook': True, 'save_annotations_in_ebook': True,
'sync_annots_user': '',
'singleinstance': False, 'singleinstance': False,
} }
@ -26,7 +27,11 @@ DEFAULTS = {
def restore_defaults(): def restore_defaults():
container = get_container() container = get_container()
for q in Object.keys(DEFAULTS): for q in Object.keys(DEFAULTS):
container.querySelector(f'[name={q}]').checked = DEFAULTS[q] control = container.querySelector(f'[name={q}]')
if jstype(DEFAULTS[q]) is 'boolean':
control.checked = DEFAULTS[q]
else:
control.value = DEFAULTS[q]
container.querySelector(f'[name=hide_tooltips]').checked = defaults.hide_tooltips container.querySelector(f'[name=hide_tooltips]').checked = defaults.hide_tooltips
@ -46,6 +51,16 @@ def create_misc_panel(container, apply_func, cancel_func):
ans.checked = settings[name] if jstype(settings[name]) is 'boolean' else DEFAULTS[name] ans.checked = settings[name] if jstype(settings[name]) is 'boolean' else DEFAULTS[name]
return E.div(style='margin-top:1ex', E.label(ans, '\xa0' + text)) return E.div(style='margin-top:1ex', E.label(ans, '\xa0' + text))
sai = E.input(name='sync_annots_user', title=_(
'The username of a Content server user that you want all annotations synced with.'
' Use the special value * to sync with anonymous users'
))
sai.value = settings.sync_annots_user if jstype(settings.sync_annots_user) is 'string' else DEFAULTS.sync_annots_user
sync_annots = E.div(
style='margin-top: 1ex; margin-left: 3px',
E.label(_('Sync bookmarks/highlights with Content server user:') + '\xa0', sai)
)
container.append(cb('remember_window_geometry', _('Remember last used window size and position'))) container.append(cb('remember_window_geometry', _('Remember last used window size and position')))
container.append(cb('show_actions_toolbar', _('Show a toolbar with the most useful actions'))) container.append(cb('show_actions_toolbar', _('Show a toolbar with the most useful actions')))
container.lastChild.append(E.span('\xa0')) container.lastChild.append(E.span('\xa0'))
@ -54,6 +69,7 @@ def create_misc_panel(container, apply_func, cancel_func):
container.append(cb('show_actions_toolbar_in_fullscreen', _('Keep the toolbar in full screen mode (needs restart)'))) container.append(cb('show_actions_toolbar_in_fullscreen', _('Keep the toolbar in full screen mode (needs restart)')))
container.append(cb('remember_last_read', _('Remember current page when quitting'))) container.append(cb('remember_last_read', _('Remember current page when quitting')))
container.append(cb('save_annotations_in_ebook', _('Keep a copy of annotations/bookmarks in the e-book file, for easy sharing'))) container.append(cb('save_annotations_in_ebook', _('Keep a copy of annotations/bookmarks in the e-book file, for easy sharing')))
container.append(sync_annots)
container.append(cb('singleinstance', _('Allow only a single instance of the viewer (needs restart)'))) container.append(cb('singleinstance', _('Allow only a single instance of the viewer (needs restart)')))
container.append(cb('hide_tooltips', _('Hide mouse-over tooltips in the book text'))) container.append(cb('hide_tooltips', _('Hide mouse-over tooltips in the book text')))
@ -68,7 +84,11 @@ def commit_misc(onchange):
container = get_container() container = get_container()
vals = {} vals = {}
for q in Object.keys(DEFAULTS): for q in Object.keys(DEFAULTS):
val = container.querySelector(f'[name={q}]').checked control = container.querySelector(f'[name={q}]')
if jstype(DEFAULTS[q]) is 'boolean':
val = control.checked
else:
val = control.value.strip()
if val is not DEFAULTS[q]: if val is not DEFAULTS[q]:
vals[q] = val vals[q] = val
sd.set('standalone_misc_settings', vals) sd.set('standalone_misc_settings', vals)