diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py index 7b85dda42f..db28358fb3 100644 --- a/src/calibre/gui2/actions/view.py +++ b/src/calibre/gui2/actions/view.py @@ -272,24 +272,23 @@ class ViewAction(InterfaceAction): return if not self._view_check(len(rows), max_=10, skip_dialog_name='open-folder-many-check'): return + db = self.gui.current_db for i, row in enumerate(rows): - db = self.gui.current_db - db.new_api.clear_extra_files_cache(self.gui.library_view.model().id(row)) + self.gui.extra_files_watcher.watch_book(db.id(row.row())) path = db.abspath(row.row()) - open_local_file(path) - if ismacos and i < len(rows) - 1: - time.sleep(0.1) # Finder cannot handle multiple folder opens + if path: + open_local_file(path) + if ismacos and i < len(rows) - 1: + time.sleep(0.1) # Finder cannot handle multiple folder opens def view_folder_for_id(self, id_): - db = self.gui.current_db - db.new_api.clear_extra_files_cache(id_) - path = db.abspath(id_, index_is_id=True) + self.gui.extra_files_watcher.watch_book(id_) + path = self.gui.current_db.abspath(id_, index_is_id=True) open_local_file(path) def view_data_folder_for_id(self, id_): - db = self.gui.current_db - db.new_api.clear_extra_files_cache(id_) - path = db.abspath(id_, index_is_id=True) + self.gui.extra_files_watcher.watch_book(id_) + path = self.gui.current_db.abspath(id_, index_is_id=True) open_local_file(os.path.join(path, DATA_DIR_NAME)) def view_book(self, triggered): diff --git a/src/calibre/gui2/extra_files_watcher.py b/src/calibre/gui2/extra_files_watcher.py new file mode 100644 index 0000000000..f5b94c01f4 --- /dev/null +++ b/src/calibre/gui2/extra_files_watcher.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2023, Kovid Goyal + + +from qt.core import QObject, QTimer +from time import monotonic +from typing import NamedTuple, Tuple + +from calibre.db.constants import DATA_FILE_PATTERN + + +class ExtraFile(NamedTuple): + relpath: str + mtime: float + size: int + + +class ExtraFiles(NamedTuple): + last_changed_at: float + files: Tuple[ExtraFile, ...] + + +class ExtraFilesWatcher(QObject): + + WATCH_FOR = 300 # seconds + TICK_INTERVAL = 1 # seconds + + def __init__(self, parent=None): + super().__init__(parent) + self.watched_book_ids = {} + self.timer = QTimer(self) + self.timer.setInterval(int(self.TICK_INTERVAL * 1000)) + self.timer.timeout.connect(self.check_registered_books) + + def clear(self): + self.watched_book_ids.clear() + self.timer.stop() + + def watch_book(self, book_id): + if book_id not in self.watched_book_ids: + try: + self.watched_book_ids[book_id] = ExtraFiles(monotonic(), self.get_extra_files(book_id)) + except Exception: + import traceback + traceback.print_exc() + return + self.timer.start() + + @property + def gui(self): + ans = self.parent() + if hasattr(ans, 'current_db'): + return ans + from calibre.gui2.ui import get_gui + return get_gui() + + def get_extra_files(self, book_id): + db = self.gui.current_db.new_api + return tuple(ExtraFile(relpath, stat_result.st_mtime, stat_result.st_size) for + relpath, file_path, stat_result in db.list_extra_files(book_id, pattern=DATA_FILE_PATTERN)) + + def check_registered_books(self): + changed = {} + remove = set() + now = monotonic() + for book_id, extra_files in self.watched_book_ids.items(): + try: + ef = self.get_extra_files(book_id) + except Exception: + # book probably deleted + remove.add(book_id) + continue + if ef != extra_files.files: + changed[book_id] = ef + elif now - extra_files.last_changed_at > self.WATCH_FOR: + remove.add(book_id) + if changed: + self.refresh_gui(changed) + for book_id, files in changed.items(): + self.watched_book_ids[book_id] = self.watched_book_ids[book_id]._replace(files=files, last_changed_at=now) + for book_id in remove: + self.watched_book_ids.pop(book_id, None) + if not self.watched_book_ids: + self.timer.stop() + + def refresh_gui(self, book_ids): + self.gui.library_view.model().refresh_ids(frozenset(book_ids), current_row=self.gui.library_view.currentIndex().row()) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index efceb8843f..49f6c3e156 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -30,6 +30,7 @@ from calibre.constants import ( from calibre.customize import PluginInstallationType from calibre.customize.ui import available_store_plugins, interface_actions from calibre.db.legacy import LibraryDatabase +from calibre.gui2.extra_files_watcher import ExtraFilesWatcher from calibre.gui2 import ( Dispatcher, GetMetadata, config, error_dialog, gprefs, info_dialog, max_available_height, open_url, question_dialog, warning_dialog, @@ -118,6 +119,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ def __init__(self, opts, parent=None, gui_debug=None): MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True) self.setWindowIcon(QApplication.instance().windowIcon()) + self.extra_files_watcher = ExtraFilesWatcher(self) self.jobs_pointer = Pointer(self) self.proceed_requested.connect(self.do_proceed, type=Qt.ConnectionType.QueuedConnection) @@ -914,6 +916,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ import traceback traceback.print_exc() self.library_path = newloc + self.extra_files_watcher.clear() prefs['library_path'] = self.library_path self.book_on_device(None, reset=True) db.set_book_on_device_func(self.book_on_device) @@ -1148,6 +1151,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.shutdown_started.emit() self.show_shutdown_message() self.server_change_notification_timer.stop() + self.extra_files_watcher.clear() try: self.event_in_db.disconnect() except Exception: