diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 9b0da5d4e5..2a00538e78 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -436,12 +436,13 @@ class AddAction(InterfaceAction): 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()) - 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) if set_current_row > -1: self.gui.library_view.set_current_row(0) self.gui.refresh_cover_browser() - self.gui.tags_view.recount() + if recount: + self.gui.tags_view.recount() def _files_added(self, adder, on_card=None): if adder.items: diff --git a/src/calibre/gui2/changes.py b/src/calibre/gui2/changes.py new file mode 100644 index 0000000000..9cf2ea6d2e --- /dev/null +++ b/src/calibre/gui2/changes.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2017, Kovid Goyal + +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 diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py index 431916f605..1ada278cbe 100644 --- a/src/calibre/gui2/cover_flow.py +++ b/src/calibre/gui2/cover_flow.py @@ -292,6 +292,8 @@ class CBDialog(QDialog): class CoverFlowMixin(object): + disable_cover_browser_refresh = False + def __init__(self, *args, **kwargs): pass @@ -405,6 +407,8 @@ class CoverFlowMixin(object): return not self.cb_splitter.is_side_index_hidden def refresh_cover_browser(self): + if self.disable_cover_browser_refresh: + return try: if self.is_cover_browser_visible() and not isinstance(self.cover_flow, QLabel): self.db_images.ignore_image_requests = False @@ -466,6 +470,7 @@ def test(): def main(args=sys.argv): return 0 + if __name__ == '__main__': from PyQt5.Qt import QApplication, QMainWindow app = QApplication([]) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 70997ebd4d..ca84833f03 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -385,6 +385,9 @@ class BooksModel(QAbstractTableModel): # {{{ def delete_books_by_id(self, ids, permanent=False): 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.notify('delete', list(ids)) self.books_deleted() diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 7c9a69f8c2..5ca5415530 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -475,7 +475,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ def start_content_server(self, check_started=True): 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.iactions['Connect Share'].content_server_state_changed) if check_started: @@ -484,8 +484,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.content_server.start() 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): - self.server_changes.push((library_path, change_event)) + self.server_changes.put((library_path, change_event)) self.server_change_notification_timer.start() 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): changes.append(change_event) if changes: - handle_changes(self, changes) + handle_changes(changes, self) def content_server_start_failed(self, msg): error_dialog(self, _('Failed to start Content server'), @@ -587,6 +589,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ def refresh_all(self): m = self.library_view.model() m.db.data.refresh(clear_caches=False, do_search=False) + self.saved_searches_changed(recount=False) m.resort() m.research() self.tags_view.recount() diff --git a/src/calibre/srv/TODO b/src/calibre/srv/TODO index 6313205f64..11e2e6e4fc 100644 --- a/src/calibre/srv/TODO +++ b/src/calibre/srv/TODO @@ -1,4 +1,3 @@ -Wire up cdb notify_changes for the embeded server Prevent standalone and embedded servers from running simultaneously Prevent more than a single instance of the standalone server from running diff --git a/src/calibre/srv/cdb.py b/src/calibre/srv/cdb.py index 2a97f200b5..56e4b20ab3 100644 --- a/src/calibre/srv/cdb.py +++ b/src/calibre/srv/cdb.py @@ -4,12 +4,14 @@ from __future__ import absolute_import, division, print_function, unicode_literals +from functools import partial + from calibre import as_unicode 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.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'} @@ -39,7 +41,7 @@ def cdb_run(ctx, rd, which, version): if getattr(m, 'needs_srv_ctx', False): args = [ctx] + list(args) 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: import traceback return {'err': as_unicode(err), 'tb': traceback.format_exc()} diff --git a/src/calibre/srv/changes.py b/src/calibre/srv/changes.py index 49b7276a19..a97db6c707 100644 --- a/src/calibre/srv/changes.py +++ b/src/calibre/srv/changes.py @@ -3,29 +3,80 @@ # License: GPLv3 Copyright: 2017, Kovid Goyal from __future__ import absolute_import, division, print_function, unicode_literals +from future_builtins import map -def books_added(book_ids): - pass +class ChangeEvent(object): + + def __init__(self): + pass + + def __repr__(self): + return '{}(book_ids={})'.format( + self.__class__.__name__, ','.join(sorted(map(str, self.book_ids))) + ) -def formats_added(formats_map): - # formats_map: {book_id:(fmt1, fmt2)} - pass +class BooksAdded(ChangeEvent): + + def __init__(self, book_ids): + ChangeEvent.__init__(self) + self.book_ids = frozenset(book_ids) -def formats_removed(formats_map): - # formats_map: {book_id:(fmt1, fmt2)} - pass +class BooksDeleted(ChangeEvent): + + def __init__(self, book_ids): + ChangeEvent.__init__(self) + self.book_ids = frozenset(book_ids) -def books_deleted(book_ids): - pass +class FormatsAdded(ChangeEvent): + + def __init__(self, formats_map): + ChangeEvent.__init__(self) + self.formats_map = formats_map + + @property + def book_ids(self): + return frozenset(self.formats_map) -def metadata(book_ids): - pass +class FormatsRemoved(ChangeEvent): + + 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=()): - pass +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 diff --git a/src/calibre/srv/embedded.py b/src/calibre/srv/embedded.py index f40ad3c933..e6a5c4e925 100644 --- a/src/calibre/srv/embedded.py +++ b/src/calibre/srv/embedded.py @@ -27,13 +27,13 @@ class Server(object): loop = current_thread = exception = None state_callback = start_failure_callback = None - def __init__(self, library_broker): + def __init__(self, library_broker, notify_changes): opts = server_config() lp, lap = log_paths() log_size = opts.max_log_size * 1024 * 1024 log = RotatingLog(lp, 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 = [] if opts.use_bonjour: plugins.append(BonJour()) diff --git a/src/calibre/srv/handler.py b/src/calibre/srv/handler.py index 0f997b24f1..5d95e259b2 100644 --- a/src/calibre/srv/handler.py +++ b/src/calibre/srv/handler.py @@ -24,7 +24,7 @@ class Context(object): CATEGORY_CACHE_SIZE = 25 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.library_broker = libraries if isinstance(libraries, LibraryBroker) else LibraryBroker(libraries) self.testing = testing @@ -32,7 +32,11 @@ class Context(object): self.user_manager = UserManager(opts.userdb) 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.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): 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): - def __init__(self, libraries, opts, testing=False): - ctx = Context(libraries, opts, testing=testing) + def __init__(self, libraries, opts, testing=False, notify_changes=None): + ctx = Context(libraries, opts, testing=testing, notify_changes=notify_changes) self.auth_controller = None if opts.auth: has_ssl = opts.ssl_certfile is not None and opts.ssl_keyfile is not None diff --git a/src/calibre/srv/library_broker.py b/src/calibre/srv/library_broker.py index 9a7e0560e7..4ad6d218d5 100644 --- a/src/calibre/srv/library_broker.py +++ b/src/calibre/srv/library_broker.py @@ -6,7 +6,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera import os from collections import OrderedDict, defaultdict -from threading import Lock +from threading import RLock as Lock from calibre import filesystem_encoding from calibre.db.cache import Cache @@ -189,6 +189,12 @@ class GuiLibraryBroker(LibraryBroker): olddb.close(), olddb.break_cycles() 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): now = monotonic() for library_id in tuple(self.loaded_dbs):