Dockerized media library scanner for NAS drives. Identifies movies and series from filenames, fetches posters via the public IMDB suggestion API (no key required), and exposes a dark-themed web UI with multilingual search support. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
96 lines
2.7 KiB
Python
96 lines
2.7 KiB
Python
"""
|
|
scheduler.py — Background scan scheduler using APScheduler.
|
|
"""
|
|
|
|
import logging
|
|
import threading
|
|
|
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
|
|
from config import Settings
|
|
from scanner import (
|
|
create_scan_record,
|
|
has_any_scan_completed,
|
|
run_full_scan,
|
|
run_incremental_scan,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_scheduler: BackgroundScheduler | None = None
|
|
_scan_lock = threading.Lock()
|
|
_active_scan_id: int | None = None
|
|
|
|
|
|
def get_active_scan_id() -> int | None:
|
|
return _active_scan_id
|
|
|
|
|
|
def is_scan_running() -> bool:
|
|
return _active_scan_id is not None
|
|
|
|
|
|
def _run_scan_protected(scan_type: str, settings: Settings) -> int | None:
|
|
"""Try to acquire lock and run a scan. Returns scan_id or None if already running."""
|
|
global _active_scan_id
|
|
if not _scan_lock.acquire(blocking=False):
|
|
logger.warning("Scan already in progress — skipping %s scan request", scan_type)
|
|
return None
|
|
|
|
scan_id = create_scan_record(scan_type)
|
|
_active_scan_id = scan_id
|
|
logger.info("Scan %s started (id=%d)", scan_type, scan_id)
|
|
|
|
def _run():
|
|
global _active_scan_id
|
|
try:
|
|
if scan_type == "full":
|
|
run_full_scan(settings, scan_id)
|
|
else:
|
|
run_incremental_scan(settings, scan_id)
|
|
finally:
|
|
_active_scan_id = None
|
|
_scan_lock.release()
|
|
logger.info("Scan %s finished (id=%d)", scan_type, scan_id)
|
|
|
|
thread = threading.Thread(target=_run, daemon=True, name=f"scan-{scan_type}-{scan_id}")
|
|
thread.start()
|
|
return scan_id
|
|
|
|
|
|
def trigger_full_scan(settings: Settings) -> int | None:
|
|
return _run_scan_protected("full", settings)
|
|
|
|
|
|
def trigger_incremental_scan(settings: Settings) -> int | None:
|
|
return _run_scan_protected("incremental", settings)
|
|
|
|
|
|
def start_scheduler(settings: Settings) -> None:
|
|
global _scheduler
|
|
|
|
_scheduler = BackgroundScheduler(timezone="UTC")
|
|
_scheduler.add_job(
|
|
func=lambda: trigger_incremental_scan(settings),
|
|
trigger="interval",
|
|
hours=settings.scan_interval_hours,
|
|
id="incremental_scan",
|
|
replace_existing=True,
|
|
)
|
|
_scheduler.start()
|
|
logger.info(
|
|
"Scheduler started — incremental scan every %d hour(s)",
|
|
settings.scan_interval_hours,
|
|
)
|
|
|
|
# Trigger initial full scan if the database has never been scanned
|
|
if not has_any_scan_completed():
|
|
logger.info("No prior completed scan found — launching initial full scan")
|
|
trigger_full_scan(settings)
|
|
|
|
|
|
def stop_scheduler() -> None:
|
|
if _scheduler and _scheduler.running:
|
|
_scheduler.shutdown(wait=False)
|
|
logger.info("Scheduler stopped")
|