This commit is contained in:
Ignacio Serantes
2026-04-12 08:39:07 +02:00
parent 07afab6ca3
commit 1508e629c0
7 changed files with 249 additions and 42 deletions

View File

@@ -1,3 +1,14 @@
"""
File System Watcher Module for Bagheera Image Viewer.
This module provides functionality to monitor file system changes in real-time
using the watchdog library. It notifies the application about new, deleted, or
modified image files within watched directories, handling debouncing to ensure
stability during rapid file operations.
Classes:
FileSystemWatcher: Coordinates file system monitoring and emits Qt signals.
"""
import os
try:
from watchdog.observers import Observer
@@ -14,6 +25,10 @@ class FileSystemWatcher(QObject):
Monitors file system events (created, deleted, modified) for specified directories.
Emits signals to notify the main application thread of changes.
"""
# Signals emitted to the rest of the application
# ---------------------------------------------
file_created = Signal(str)
file_deleted = Signal(str)
file_modified = Signal(str)
@@ -24,10 +39,18 @@ class FileSystemWatcher(QObject):
directory_modified = Signal(str) # For changes that might not be specific files
_modified_events_queue = {} # {path: QTimer}
"""Queue to manage debouncing of modification events."""
def __init__(self, parent=None):
"""
Initializes the FileSystemWatcher.
Args:
parent (QObject, optional): The parent object. Defaults to None.
"""
super().__init__(parent)
self._watched_directories = set()
self._debounce_interval = 500 # milliseconds
if HAVE_WATCHDOG:
self._observer = Observer()
@@ -36,16 +59,21 @@ class FileSystemWatcher(QObject):
else:
self._observer = None # Keep observer as None if watchdog is not available
# Debounce timer for modified events to avoid multiple signals for a single save
self._debounce_interval = 500 # milliseconds
# Connect the internal signal to the debouncing slot
if HAVE_WATCHDOG:
self._file_modified_from_handler.connect(self._on_file_modified_debounced)
def _on_file_modified_debounced(self, path):
"""Slot to handle modified events from the watchdog thread, debounced in the
main thread."""
"""
Slot to handle modified events from the watchdog thread.
Implements a debouncing mechanism: if multiple modification events
arrive for the same path within the interval, previous timers are
reset to avoid redundant UI updates or heavy disk operations.
Args:
path (str): The path of the modified file.
"""
# Debounce timer for modified events to avoid multiple signals for a single save
if path in self._modified_events_queue:
self._modified_events_queue[path].stop()
@@ -59,7 +87,12 @@ class FileSystemWatcher(QObject):
self._modified_events_queue[path].start()
def _emit_modified_after_debounce(self, path):
"""Emits the file_modified signal after the debounce period."""
"""
Emits the file_modified signal after the debounce period.
Args:
path (str): The path of the modified file.
"""
self.file_modified.emit(path)
if path in self._modified_events_queue:
# Safely delete the QTimer object when done
@@ -67,7 +100,16 @@ class FileSystemWatcher(QObject):
del self._modified_events_queue[path]
def add_path(self, path):
"""Adds a directory to be monitored."""
"""
Adds a directory to be monitored.
This method ensures that redundant watches are avoided by checking if
the path is already covered by an existing watch or if it should
consolidate multiple sub-watches into a single parent watch.
Args:
path (str): The directory path to monitor.
"""
if not HAVE_WATCHDOG or self._observer is None:
return
@@ -111,7 +153,12 @@ class FileSystemWatcher(QObject):
self.monitoring_status_changed.emit(True)
def remove_path(self, path):
"""Removes a directory from monitoring."""
"""
Removes a directory from monitoring.
Args:
path (str): The directory path to stop monitoring.
"""
if not HAVE_WATCHDOG or self._observer is None:
return
abs_path = os.path.normpath(os.path.abspath(os.path.expanduser(path)))
@@ -138,7 +185,9 @@ class FileSystemWatcher(QObject):
self.monitoring_status_changed.emit(False)
def stop(self):
"""Stops the file system observer."""
"""
Stops the file system observer and cleans up active timers.
"""
if HAVE_WATCHDOG and self._observer:
self._observer.stop()
self._observer.join()
@@ -150,10 +199,19 @@ class FileSystemWatcher(QObject):
if HAVE_WATCHDOG:
class _Handler(FileSystemEventHandler):
"""
Custom event handler for watchdog events.
Translates low-level file system events into high-level application
signals, filtering for supported image types.
"""
# Signal to communicate to main thread
file_modified_from_thread = Signal(str)
"""Custom event handler for watchdog events."""
def __init__(self, watcher):
"""
Initializes the handler with a reference to the main watcher.
"""
super().__init__()
self.watcher = watcher
@@ -199,11 +257,21 @@ class FileSystemWatcher(QObject):
self.watcher._file_modified_from_handler.emit(event.src_path)
def _emit_modified(self, path):
"""Internal helper to emit the modified signal."""
"""
Internal helper to emit the modified signal.
Args:
path (str): The modified path.
"""
self.watcher.file_modified.emit(path)
if path in self.watcher._modified_events_queue:
del self.watcher._modified_events_queue[path]
def _is_image_file(self, path):
"""Checks if a given path has a supported image extension."""
"""
Checks if a given path has a supported image extension.
Args:
path (str): The file path to check.
"""
return os.path.splitext(path)[1].lower() in IMAGE_EXTENSIONS