Wire up notification of changes for the embedded server

This commit is contained in:
Kovid Goyal 2017-05-03 10:10:20 +05:30
parent 345842825b
commit 8535c6a1d8
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
11 changed files with 163 additions and 30 deletions

View File

@ -436,11 +436,12 @@ class AddAction(InterfaceAction):
Adder(paths, db=None if to_device else self.gui.current_db, Adder(paths, db=None if to_device else self.gui.current_db,
parent=self.gui, callback=partial(self._files_added, on_card=on_card), pool=self.gui.spare_pool()) parent=self.gui, callback=partial(self._files_added, on_card=on_card), pool=self.gui.spare_pool())
def refresh_gui(self, num, set_current_row=-1): def refresh_gui(self, num, set_current_row=-1, recount=True):
self.gui.library_view.model().books_added(num) self.gui.library_view.model().books_added(num)
if set_current_row > -1: if set_current_row > -1:
self.gui.library_view.set_current_row(0) self.gui.library_view.set_current_row(0)
self.gui.refresh_cover_browser() self.gui.refresh_cover_browser()
if recount:
self.gui.tags_view.recount() self.gui.tags_view.recount()
def _files_added(self, adder, on_card=None): def _files_added(self, adder, on_card=None):

View File

@ -0,0 +1,59 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
from __future__ import absolute_import, division, print_function, unicode_literals
from calibre.srv.changes import (
BooksAdded, BooksDeleted, FormatsAdded, FormatsRemoved, MetadataChanged,
SavedSearchesChanged
)
def handle_changes(changes, gui=None):
if not changes:
return
if gui is None:
from calibre.gui2.ui import get_gui
gui = get_gui()
if gui is None:
return
refresh_ids = set()
added, removed = set(), set()
ss_changed = False
for change in changes:
if isinstance(change, (FormatsAdded, FormatsRemoved, MetadataChanged)):
refresh_ids |= change.book_ids
elif isinstance(change, BooksAdded):
added |= change.book_ids
elif isinstance(change, BooksDeleted):
removed |= change.book_ids
elif isinstance(change, SavedSearchesChanged):
ss_changed = True
if added and removed:
gui.refresh_all()
return
refresh_ids -= added | removed
orig = gui.tags_view.disable_recounting, gui.disable_cover_browser_refresh
gui.tags_view.disable_recounting = gui.disable_cover_browser_refresh = True
if added:
gui.current_db.data.books_added(added)
gui.iactions['Add Books'].refresh_gui(len(added), recount=False)
if removed:
next_id = gui.current_view().next_id
m = gui.library_view.model()
m.ids_deleted(removed)
gui.iactions['Remove Books'].library_ids_deleted2(removed, next_id=next_id)
# current = gui.library_view.currentIndex()
# if current.isValid():
# m.current_changed(current, current)
# else:
# gui.book_details.reset_info()
if refresh_ids:
gui.iactions['Edit Metadata'].refresh_books_after_metadata_edit(refresh_ids)
if ss_changed:
gui.saved_searches_changed(recount=False)
gui.tags_view.disable_recounting = gui.disable_cover_browser_refresh = False
gui.tags_view.recount(), gui.refresh_cover_browser()
gui.tags_view.disable_recounting, gui.disable_cover_browser_refresh = orig

View File

@ -292,6 +292,8 @@ class CBDialog(QDialog):
class CoverFlowMixin(object): class CoverFlowMixin(object):
disable_cover_browser_refresh = False
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
pass pass
@ -405,6 +407,8 @@ class CoverFlowMixin(object):
return not self.cb_splitter.is_side_index_hidden return not self.cb_splitter.is_side_index_hidden
def refresh_cover_browser(self): def refresh_cover_browser(self):
if self.disable_cover_browser_refresh:
return
try: try:
if self.is_cover_browser_visible() and not isinstance(self.cover_flow, QLabel): if self.is_cover_browser_visible() and not isinstance(self.cover_flow, QLabel):
self.db_images.ignore_image_requests = False self.db_images.ignore_image_requests = False
@ -466,6 +470,7 @@ def test():
def main(args=sys.argv): def main(args=sys.argv):
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':
from PyQt5.Qt import QApplication, QMainWindow from PyQt5.Qt import QApplication, QMainWindow
app = QApplication([]) app = QApplication([])

View File

@ -385,6 +385,9 @@ class BooksModel(QAbstractTableModel): # {{{
def delete_books_by_id(self, ids, permanent=False): def delete_books_by_id(self, ids, permanent=False):
self.db.new_api.remove_books(ids, permanent=permanent) self.db.new_api.remove_books(ids, permanent=permanent)
self.ids_deleted(ids)
def ids_deleted(self, ids):
self.db.data.books_deleted(tuple(ids)) self.db.data.books_deleted(tuple(ids))
self.db.notify('delete', list(ids)) self.db.notify('delete', list(ids))
self.books_deleted() self.books_deleted()

View File

@ -475,7 +475,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def start_content_server(self, check_started=True): def start_content_server(self, check_started=True):
from calibre.srv.embedded import Server from calibre.srv.embedded import Server
self.content_server = Server(self.library_broker, self.handle_changes_from_server) self.content_server = Server(self.library_broker, Dispatcher(self.handle_changes_from_server))
self.content_server.state_callback = Dispatcher( self.content_server.state_callback = Dispatcher(
self.iactions['Connect Share'].content_server_state_changed) self.iactions['Connect Share'].content_server_state_changed)
if check_started: if check_started:
@ -484,8 +484,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.content_server.start() self.content_server.start()
def handle_changes_from_server(self, library_path, change_event): def handle_changes_from_server(self, library_path, change_event):
if DEBUG:
prints('Received server change event: {} for {}'.format(change_event, library_path))
if self.library_broker.is_gui_library(library_path): if self.library_broker.is_gui_library(library_path):
self.server_changes.push((library_path, change_event)) self.server_changes.put((library_path, change_event))
self.server_change_notification_timer.start() self.server_change_notification_timer.start()
def handle_changes_from_server_debounced(self): def handle_changes_from_server_debounced(self):
@ -500,7 +502,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
if self.library_broker.is_gui_library(library_path): if self.library_broker.is_gui_library(library_path):
changes.append(change_event) changes.append(change_event)
if changes: if changes:
handle_changes(self, changes) handle_changes(changes, self)
def content_server_start_failed(self, msg): def content_server_start_failed(self, msg):
error_dialog(self, _('Failed to start Content server'), error_dialog(self, _('Failed to start Content server'),
@ -587,6 +589,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def refresh_all(self): def refresh_all(self):
m = self.library_view.model() m = self.library_view.model()
m.db.data.refresh(clear_caches=False, do_search=False) m.db.data.refresh(clear_caches=False, do_search=False)
self.saved_searches_changed(recount=False)
m.resort() m.resort()
m.research() m.research()
self.tags_view.recount() self.tags_view.recount()

View File

@ -1,4 +1,3 @@
Wire up cdb notify_changes for the embeded server
Prevent standalone and embedded servers from running simultaneously Prevent standalone and embedded servers from running simultaneously
Prevent more than a single instance of the standalone server from running Prevent more than a single instance of the standalone server from running

View File

@ -4,12 +4,14 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
from functools import partial
from calibre import as_unicode from calibre import as_unicode
from calibre.db.cli import module_for_cmd from calibre.db.cli import module_for_cmd
from calibre.srv.errors import HTTPNotFound, HTTPBadRequest from calibre.srv.errors import HTTPBadRequest, HTTPNotFound
from calibre.srv.routes import endpoint, msgpack_or_json from calibre.srv.routes import endpoint, msgpack_or_json
from calibre.srv.utils import get_library_data from calibre.srv.utils import get_library_data
from calibre.utils.serialize import MSGPACK_MIME, msgpack_loads, json_loads from calibre.utils.serialize import MSGPACK_MIME, json_loads, msgpack_loads
receive_data_methods = {'GET', 'POST'} receive_data_methods = {'GET', 'POST'}
@ -39,7 +41,7 @@ def cdb_run(ctx, rd, which, version):
if getattr(m, 'needs_srv_ctx', False): if getattr(m, 'needs_srv_ctx', False):
args = [ctx] + list(args) args = [ctx] + list(args)
try: try:
result = m.implementation(db, ctx.notify_changes, *args) result = m.implementation(db, partial(ctx.notify_changes, db.backend.library_path), *args)
except Exception as err: except Exception as err:
import traceback import traceback
return {'err': as_unicode(err), 'tb': traceback.format_exc()} return {'err': as_unicode(err), 'tb': traceback.format_exc()}

View File

@ -3,29 +3,80 @@
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net> # License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
from future_builtins import map
def books_added(book_ids): class ChangeEvent(object):
def __init__(self):
pass pass
def __repr__(self):
def formats_added(formats_map): return '{}(book_ids={})'.format(
# formats_map: {book_id:(fmt1, fmt2)} self.__class__.__name__, ','.join(sorted(map(str, self.book_ids)))
pass )
def formats_removed(formats_map): class BooksAdded(ChangeEvent):
# formats_map: {book_id:(fmt1, fmt2)}
pass def __init__(self, book_ids):
ChangeEvent.__init__(self)
self.book_ids = frozenset(book_ids)
def books_deleted(book_ids): class BooksDeleted(ChangeEvent):
pass
def __init__(self, book_ids):
ChangeEvent.__init__(self)
self.book_ids = frozenset(book_ids)
def metadata(book_ids): class FormatsAdded(ChangeEvent):
pass
def __init__(self, formats_map):
ChangeEvent.__init__(self)
self.formats_map = formats_map
@property
def book_ids(self):
return frozenset(self.formats_map)
def saved_searches(added=(), removed=()): class FormatsRemoved(ChangeEvent):
pass
def __init__(self, formats_map):
ChangeEvent.__init__(self)
self.formats_map = formats_map
@property
def book_ids(self):
return frozenset(self.formats_map)
class MetadataChanged(ChangeEvent):
def __init__(self, book_ids):
ChangeEvent.__init__(self)
self.book_ids = frozenset(book_ids)
class SavedSearchesChanged(ChangeEvent):
def __init__(self, added=(), removed=()):
ChangeEvent.__init__(self)
self.added = frozenset(added)
self.removed = frozenset(removed)
def __repr__(self):
return '{}(added={}, removed={})'.format(
self.__class__.__name__,
sorted(map(str, self.added)), sorted(map(str, self.removed))
)
books_added = BooksAdded
formats_added = FormatsAdded
formats_removed = FormatsRemoved
books_deleted = BooksDeleted
metadata = MetadataChanged
saved_searches = SavedSearchesChanged

View File

@ -27,13 +27,13 @@ class Server(object):
loop = current_thread = exception = None loop = current_thread = exception = None
state_callback = start_failure_callback = None state_callback = start_failure_callback = None
def __init__(self, library_broker): def __init__(self, library_broker, notify_changes):
opts = server_config() opts = server_config()
lp, lap = log_paths() lp, lap = log_paths()
log_size = opts.max_log_size * 1024 * 1024 log_size = opts.max_log_size * 1024 * 1024
log = RotatingLog(lp, max_size=log_size) log = RotatingLog(lp, max_size=log_size)
access_log = RotatingLog(lap, max_size=log_size) access_log = RotatingLog(lap, max_size=log_size)
self.handler = Handler(library_broker, opts) self.handler = Handler(library_broker, opts, notify_changes=notify_changes)
plugins = self.plugins = [] plugins = self.plugins = []
if opts.use_bonjour: if opts.use_bonjour:
plugins.append(BonJour()) plugins.append(BonJour())

View File

@ -24,7 +24,7 @@ class Context(object):
CATEGORY_CACHE_SIZE = 25 CATEGORY_CACHE_SIZE = 25
SEARCH_CACHE_SIZE = 100 SEARCH_CACHE_SIZE = 100
def __init__(self, libraries, opts, testing=False): def __init__(self, libraries, opts, testing=False, notify_changes=None):
self.opts = opts self.opts = opts
self.library_broker = libraries if isinstance(libraries, LibraryBroker) else LibraryBroker(libraries) self.library_broker = libraries if isinstance(libraries, LibraryBroker) else LibraryBroker(libraries)
self.testing = testing self.testing = testing
@ -32,7 +32,11 @@ class Context(object):
self.user_manager = UserManager(opts.userdb) self.user_manager = UserManager(opts.userdb)
self.ignored_fields = frozenset(filter(None, (x.strip() for x in (opts.ignored_fields or '').split(',')))) self.ignored_fields = frozenset(filter(None, (x.strip() for x in (opts.ignored_fields or '').split(','))))
self.displayed_fields = frozenset(filter(None, (x.strip() for x in (opts.displayed_fields or '').split(',')))) self.displayed_fields = frozenset(filter(None, (x.strip() for x in (opts.displayed_fields or '').split(','))))
self.notify_changes = lambda *a: None self._notify_changes = notify_changes
def notify_changes(self, library_path, change_event):
if self._notify_changes is not None:
self._notify_changes(library_path, change_event)
def start_job(self, name, module, func, args=(), kwargs=None, job_done_callback=None, job_data=None): def start_job(self, name, module, func, args=(), kwargs=None, job_done_callback=None, job_data=None):
return self.jobs_manager.start_job(name, module, func, args, kwargs, job_done_callback, job_data) return self.jobs_manager.start_job(name, module, func, args, kwargs, job_done_callback, job_data)
@ -133,8 +137,8 @@ class Context(object):
class Handler(object): class Handler(object):
def __init__(self, libraries, opts, testing=False): def __init__(self, libraries, opts, testing=False, notify_changes=None):
ctx = Context(libraries, opts, testing=testing) ctx = Context(libraries, opts, testing=testing, notify_changes=notify_changes)
self.auth_controller = None self.auth_controller = None
if opts.auth: if opts.auth:
has_ssl = opts.ssl_certfile is not None and opts.ssl_keyfile is not None has_ssl = opts.ssl_certfile is not None and opts.ssl_keyfile is not None

View File

@ -6,7 +6,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
import os import os
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from threading import Lock from threading import RLock as Lock
from calibre import filesystem_encoding from calibre import filesystem_encoding
from calibre.db.cache import Cache from calibre.db.cache import Cache
@ -189,6 +189,12 @@ class GuiLibraryBroker(LibraryBroker):
olddb.close(), olddb.break_cycles() olddb.close(), olddb.break_cycles()
self._prune_loaded_dbs() self._prune_loaded_dbs()
def is_gui_library(self, library_path):
with self:
if self.gui_library_id and self.gui_library_id in self.lmap:
return samefile(library_path, self.lmap[self.gui_library_id])
return False
def _prune_loaded_dbs(self): def _prune_loaded_dbs(self):
now = monotonic() now = monotonic()
for library_id in tuple(self.loaded_dbs): for library_id in tuple(self.loaded_dbs):