feat: Enhance scheduler and watcher with improved logging and cron trigger helpers

This commit is contained in:
2025-11-29 12:56:18 +09:00
parent fe4061d1a1
commit 559342fae7
3 changed files with 138 additions and 70 deletions
+49 -25
View File
@@ -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()