feat: Enhance scheduler and watcher with improved logging and cron trigger helpers
This commit is contained in:
+49
-25
@@ -1,19 +1,23 @@
|
||||
import os
|
||||
import threading
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
from watchdog.observers.polling import PollingObserver as Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.events import FileSystemEventHandler, FileSystemEvent
|
||||
from app.utils.logger import logger
|
||||
from app.utils.config import server_config
|
||||
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 = None
|
||||
self.debounce_timer: Optional[threading.Timer] = None
|
||||
self.debounce_interval = 5.0 # Seconds
|
||||
|
||||
def on_any_event(self, event):
|
||||
# Log all events for debugging (using INFO temporarily to ensure visibility)
|
||||
logger.info(f"[WATCHER-DEBUG] Event detected: {event.event_type} {event.src_path}")
|
||||
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
|
||||
@@ -23,55 +27,70 @@ class PlaylistEventHandler(FileSystemEventHandler):
|
||||
if event.event_type not in ['created', 'modified', 'deleted', 'moved']:
|
||||
return
|
||||
|
||||
# Ignore temporary files or hidden files if necessary
|
||||
# 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 (likely caused by the sync itself)
|
||||
# Prevent feedback loops: if sync is in progress, ignore events
|
||||
if sync_manager.is_syncing:
|
||||
logger.info(f"[WATCHER-DEBUG] Ignoring event {event.event_type} on {event.src_path} because sync is in progress.")
|
||||
logger.debug(f"[Watcher] Ignoring event {event.event_type} on {event.src_path} because sync is in progress.")
|
||||
return
|
||||
|
||||
logger.info(f"File system event detected and accepted: {event.event_type} {event.src_path}")
|
||||
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()
|
||||
|
||||
# Debounce for 5 seconds to allow multiple file operations to complete
|
||||
self.debounce_timer = threading.Timer(5.0, self.run_sync)
|
||||
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):
|
||||
logger.info("Triggering sync due to file change...")
|
||||
sync_manager.run_sync(trigger_source="watcher", wait=False)
|
||||
"""
|
||||
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 = None
|
||||
self.handler = None
|
||||
self.current_path = None
|
||||
self.observer: Optional[Observer] = None
|
||||
self.handler: Optional[PlaylistEventHandler] = None
|
||||
self.current_path: Optional[str] = None
|
||||
|
||||
def start(self, path):
|
||||
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}")
|
||||
logger.info(f"[Watcher] Already running on {path}")
|
||||
return
|
||||
|
||||
self.stop()
|
||||
|
||||
if not os.path.exists(path):
|
||||
logger.warning(f"Cannot watch path {path}: Directory does not exist.")
|
||||
logger.warning(f"[Watcher] Cannot watch path {path}: Directory does not exist.")
|
||||
return
|
||||
|
||||
logger.info(f"Starting file watcher on: {path}")
|
||||
logger.info(f"[Watcher] Starting file watcher on: {path}")
|
||||
try:
|
||||
files = os.listdir(path)
|
||||
logger.info(f"Files currently in watch directory: {files}")
|
||||
logger.debug(f"[Watcher] Initial files in watch directory: {files}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list files in watch directory: {e}")
|
||||
logger.error(f"[Watcher] Failed to list files in watch directory: {e}")
|
||||
|
||||
self.handler = PlaylistEventHandler()
|
||||
# Explicitly set timeout for PollingObserver
|
||||
@@ -79,13 +98,18 @@ class WatcherManager:
|
||||
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("Stopping file watcher...")
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user