import asyncio import logging import uuid from pathlib import Path from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler, FileCreatedEvent, FileMovedEvent from ..database import AsyncSessionLocal from ..models.library import Library from ..models.session import ScanJob from sqlalchemy import select logger = logging.getLogger(__name__) AUDIO_EXTENSIONS = {".mp3", ".wav", ".m4a", ".flac", ".ogg", ".aac", ".m4b", ".opus"} _observer: Observer | None = None _scan_debounce: dict[str, asyncio.TimerHandle] = {} class AudioFileHandler(FileSystemEventHandler): def __init__(self, library_id: str, loop: asyncio.AbstractEventLoop): self.library_id = library_id self.loop = loop def _schedule_scan(self): key = self.library_id if key in _scan_debounce: _scan_debounce[key].cancel() handle = self.loop.call_later( 30.0, # 30s Debounce — nicht bei jeder Datei sofort scannen lambda: asyncio.run_coroutine_threadsafe( _trigger_scan(self.library_id), self.loop ), ) _scan_debounce[key] = handle def on_created(self, event): if not event.is_directory: ext = Path(event.src_path).suffix.lower() if ext in AUDIO_EXTENSIONS: logger.info(f"Neue Audiodatei erkannt: {event.src_path}") self._schedule_scan() def on_moved(self, event): if not event.is_directory: ext = Path(event.dest_path).suffix.lower() if ext in AUDIO_EXTENSIONS: logger.info(f"Audiodatei verschoben: {event.dest_path}") self._schedule_scan() async def _trigger_scan(library_id: str): from ..services.scanner import scan_library_task async with AsyncSessionLocal() as db: job = ScanJob( id=str(uuid.uuid4()), library_id=library_id, status="queued", ) db.add(job) await db.commit() await db.refresh(job) asyncio.create_task(scan_library_task(library_id, job.id)) async def start_file_watcher(): global _observer loop = asyncio.get_event_loop() async with AsyncSessionLocal() as db: result = await db.execute(select(Library)) libraries = result.scalars().all() observer = Observer() for lib in libraries: for folder_info in (lib.folders or []): folder_path = folder_info.get("fullPath", folder_info.get("full_path", "")) if folder_path and Path(folder_path).exists(): handler = AudioFileHandler(lib.id, loop) observer.schedule(handler, folder_path, recursive=True) logger.info(f"Watching: {folder_path} (Library: {lib.name})") observer.start() _observer = observer logger.info("File Watcher gestartet.") def stop_file_watcher(): global _observer if _observer: _observer.stop() _observer.join() _observer = None logger.info("File Watcher gestoppt.")