116 lines
4.2 KiB
Python
116 lines
4.2 KiB
Python
import os
|
|
import threading
|
|
from typing import Optional
|
|
from watchdog.observers.polling import PollingObserver as Observer
|
|
from watchdog.events import FileSystemEventHandler, FileSystemEvent
|
|
from app.utils.logger import logger
|
|
from app.utils.sync_manager import sync_manager
|
|
|
|
class PlaylistEventHandler(FileSystemEventHandler):
|
|
"""
|
|
Handles file system events for the playlist directory.
|
|
Triggers a sync operation when changes are detected, with debouncing.
|
|
"""
|
|
def __init__(self):
|
|
self.debounce_timer: Optional[threading.Timer] = None
|
|
self.debounce_interval = 5.0 # Seconds
|
|
|
|
def on_any_event(self, event: FileSystemEvent):
|
|
# Log all events at DEBUG level to avoid cluttering INFO logs
|
|
logger.debug(f"[Watcher] Event detected: {event.event_type} {event.src_path}")
|
|
|
|
if event.is_directory:
|
|
return
|
|
|
|
# Filter out noisy events. Only listen to actual changes.
|
|
# 'opened' and 'closed' (without write) are read events and should be ignored.
|
|
if event.event_type not in ['created', 'modified', 'deleted', 'moved']:
|
|
return
|
|
|
|
# Ignore temporary files or hidden files
|
|
filename = os.path.basename(event.src_path)
|
|
if filename.startswith('.'):
|
|
return
|
|
|
|
# Prevent feedback loops: if sync is in progress, ignore events
|
|
if sync_manager.is_syncing:
|
|
logger.debug(f"[Watcher] Ignoring event {event.event_type} on {event.src_path} because sync is in progress.")
|
|
return
|
|
|
|
logger.info(f"[Watcher] Accepted file change: {event.event_type} {event.src_path}")
|
|
self.trigger_sync()
|
|
|
|
def trigger_sync(self):
|
|
"""
|
|
Triggers the sync process after a debounce interval.
|
|
"""
|
|
if self.debounce_timer:
|
|
self.debounce_timer.cancel()
|
|
|
|
logger.debug(f"[Watcher] Debouncing sync for {self.debounce_interval} seconds...")
|
|
self.debounce_timer = threading.Timer(self.debounce_interval, self.run_sync)
|
|
self.debounce_timer.start()
|
|
|
|
def run_sync(self):
|
|
"""
|
|
Executes the sync via SyncManager.
|
|
"""
|
|
logger.info("[Watcher] Debounce timer expired. Triggering sync due to file changes.")
|
|
try:
|
|
sync_manager.run_sync(trigger_source="watcher", wait=False)
|
|
except Exception as e:
|
|
logger.error(f"[Watcher] Failed to trigger sync: {e}", exc_info=True)
|
|
|
|
class WatcherManager:
|
|
"""
|
|
Manages the lifecycle of the file watcher.
|
|
"""
|
|
def __init__(self):
|
|
self.observer: Optional[Observer] = None
|
|
self.handler: Optional[PlaylistEventHandler] = None
|
|
self.current_path: Optional[str] = None
|
|
|
|
def start(self, path: str):
|
|
"""
|
|
Starts watching the specified directory.
|
|
"""
|
|
# If already watching the same path, do nothing
|
|
if self.observer and self.observer.is_alive() and self.current_path == path:
|
|
logger.info(f"[Watcher] Already running on {path}")
|
|
return
|
|
|
|
self.stop()
|
|
|
|
if not os.path.exists(path):
|
|
logger.warning(f"[Watcher] Cannot watch path {path}: Directory does not exist.")
|
|
return
|
|
|
|
logger.info(f"[Watcher] Starting file watcher on: {path}")
|
|
try:
|
|
files = os.listdir(path)
|
|
logger.debug(f"[Watcher] Initial files in watch directory: {files}")
|
|
except Exception as e:
|
|
logger.error(f"[Watcher] Failed to list files in watch directory: {e}")
|
|
|
|
self.handler = PlaylistEventHandler()
|
|
# Explicitly set timeout for PollingObserver
|
|
self.observer = Observer(timeout=1.0)
|
|
self.observer.schedule(self.handler, path, recursive=True)
|
|
self.observer.start()
|
|
self.current_path = path
|
|
logger.info("[Watcher] Watcher started successfully.")
|
|
|
|
def stop(self):
|
|
"""
|
|
Stops the file watcher.
|
|
"""
|
|
if self.observer:
|
|
logger.info("[Watcher] Stopping file watcher...")
|
|
self.observer.stop()
|
|
self.observer.join()
|
|
self.observer = None
|
|
self.current_path = None
|
|
logger.info("[Watcher] Watcher stopped.")
|
|
|
|
watcher_manager = WatcherManager()
|