Files
Audiolib/backend/app/services/file_watcher.py
Audiolib 14ffee3051 Initial commit: Phase 1 – Projektstruktur, DB-Schema, Core-API
- FastAPI-Backend mit vollständiger ABS v2.x API-Kompatibilität
- SQLAlchemy-Models: User, Library, LibraryItem, BookFile, Chapter,
  Podcast, PodcastEpisode, MediaProgress, Bookmark, PlaybackSession
- Auth: JWT-Login (/login, /logout, /api/authorize)
- Library + Items Endpoints inkl. camelCase ABS-Response-Format
- HLS-Streaming via FFmpeg (POST /api/items/:id/play, Session-Sync)
- Me/Progress Endpoints + Lesezeichen
- User-Management + Server-Settings (Admin)
- Library-Scanner (MP3/WAV Discovery, Hintergrund-Task)
- File Watcher (watchdog, 30s Debounce)
- Matching-Skelett (MusicBrainz, OpenLibrary, Google Books – Phase 5)
- Docker-Setup: backend (Python 3.12+FFmpeg), frontend (React/Vite),
  nginx Reverse-Proxy auf Port 3000
- setup.sh: Installiert Docker auf Debian/Ubuntu, richtet .env ein

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:43:35 +02:00

95 lines
3.0 KiB
Python

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.")