Scan opened book/data folders for changes to extra files for a few minutes after they are opened, updating caches and the GUI as changes are detected

This commit is contained in:
Kovid Goyal 2023-04-24 22:37:54 +05:30
parent 79cd85af35
commit 300c68b1e4
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 101 additions and 11 deletions

View File

@ -272,24 +272,23 @@ class ViewAction(InterfaceAction):
return return
if not self._view_check(len(rows), max_=10, skip_dialog_name='open-folder-many-check'): if not self._view_check(len(rows), max_=10, skip_dialog_name='open-folder-many-check'):
return return
db = self.gui.current_db
for i, row in enumerate(rows): for i, row in enumerate(rows):
db = self.gui.current_db self.gui.extra_files_watcher.watch_book(db.id(row.row()))
db.new_api.clear_extra_files_cache(self.gui.library_view.model().id(row))
path = db.abspath(row.row()) path = db.abspath(row.row())
open_local_file(path) if path:
if ismacos and i < len(rows) - 1: open_local_file(path)
time.sleep(0.1) # Finder cannot handle multiple folder opens if ismacos and i < len(rows) - 1:
time.sleep(0.1) # Finder cannot handle multiple folder opens
def view_folder_for_id(self, id_): def view_folder_for_id(self, id_):
db = self.gui.current_db self.gui.extra_files_watcher.watch_book(id_)
db.new_api.clear_extra_files_cache(id_) path = self.gui.current_db.abspath(id_, index_is_id=True)
path = db.abspath(id_, index_is_id=True)
open_local_file(path) open_local_file(path)
def view_data_folder_for_id(self, id_): def view_data_folder_for_id(self, id_):
db = self.gui.current_db self.gui.extra_files_watcher.watch_book(id_)
db.new_api.clear_extra_files_cache(id_) path = self.gui.current_db.abspath(id_, index_is_id=True)
path = db.abspath(id_, index_is_id=True)
open_local_file(os.path.join(path, DATA_DIR_NAME)) open_local_file(os.path.join(path, DATA_DIR_NAME))
def view_book(self, triggered): def view_book(self, triggered):

View File

@ -0,0 +1,87 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2023, Kovid Goyal <kovid at kovidgoyal.net>
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())

View File

@ -30,6 +30,7 @@ from calibre.constants import (
from calibre.customize import PluginInstallationType from calibre.customize import PluginInstallationType
from calibre.customize.ui import available_store_plugins, interface_actions from calibre.customize.ui import available_store_plugins, interface_actions
from calibre.db.legacy import LibraryDatabase from calibre.db.legacy import LibraryDatabase
from calibre.gui2.extra_files_watcher import ExtraFilesWatcher
from calibre.gui2 import ( from calibre.gui2 import (
Dispatcher, GetMetadata, config, error_dialog, gprefs, info_dialog, Dispatcher, GetMetadata, config, error_dialog, gprefs, info_dialog,
max_available_height, open_url, question_dialog, warning_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): def __init__(self, opts, parent=None, gui_debug=None):
MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True) MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
self.setWindowIcon(QApplication.instance().windowIcon()) self.setWindowIcon(QApplication.instance().windowIcon())
self.extra_files_watcher = ExtraFilesWatcher(self)
self.jobs_pointer = Pointer(self) self.jobs_pointer = Pointer(self)
self.proceed_requested.connect(self.do_proceed, self.proceed_requested.connect(self.do_proceed,
type=Qt.ConnectionType.QueuedConnection) type=Qt.ConnectionType.QueuedConnection)
@ -914,6 +916,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
import traceback import traceback
traceback.print_exc() traceback.print_exc()
self.library_path = newloc self.library_path = newloc
self.extra_files_watcher.clear()
prefs['library_path'] = self.library_path prefs['library_path'] = self.library_path
self.book_on_device(None, reset=True) self.book_on_device(None, reset=True)
db.set_book_on_device_func(self.book_on_device) 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.shutdown_started.emit()
self.show_shutdown_message() self.show_shutdown_message()
self.server_change_notification_timer.stop() self.server_change_notification_timer.stop()
self.extra_files_watcher.clear()
try: try:
self.event_in_db.disconnect() self.event_in_db.disconnect()
except Exception: except Exception: