"""Daemon worker: config-driven channel scan + video processing.""" from __future__ import annotations import logging import time from datetime import datetime, timedelta from core.db import conn from core.youtube import scan_all_channels from core.pipeline import process_pending logger = logging.getLogger(__name__) CHECK_INTERVAL = 30 # seconds between config checks def _get_config() -> dict | None: """Read daemon config from DB.""" try: with conn() as c: cur = c.cursor() cur.execute( "SELECT scan_enabled, scan_interval_min, process_enabled, " "process_interval_min, process_limit, last_scan_at, last_process_at " "FROM daemon_config WHERE id = 1" ) row = cur.fetchone() if not row: return None return { "scan_enabled": bool(row[0]), "scan_interval_min": row[1], "process_enabled": bool(row[2]), "process_interval_min": row[3], "process_limit": row[4], "last_scan_at": row[5], "last_process_at": row[6], } except Exception as e: logger.error("Failed to read daemon config: %s", e) return None def _should_run(last_at: datetime | None, interval_min: int) -> bool: """Check if enough time has passed since last run.""" if last_at is None: return True now = datetime.utcnow() # Oracle TIMESTAMP comes as datetime return now - last_at >= timedelta(minutes=interval_min) def _update_last(field: str) -> None: """Update last_scan_at or last_process_at.""" with conn() as c: c.cursor().execute( f"UPDATE daemon_config SET {field} = SYSTIMESTAMP WHERE id = 1" ) def run_once_if_due() -> None: """Check config and run tasks if their schedule is due.""" cfg = _get_config() if not cfg: return if cfg["scan_enabled"] and _should_run(cfg["last_scan_at"], cfg["scan_interval_min"]): logger.info("=== Scheduled scan start ===") try: new_count = scan_all_channels() logger.info("Scan complete: %d new videos", new_count) _update_last("last_scan_at") except Exception as e: logger.error("Channel scan failed: %s", e) if cfg["process_enabled"] and _should_run(cfg["last_process_at"], cfg["process_interval_min"]): logger.info("=== Scheduled processing start ===") try: rest_count = process_pending(limit=cfg["process_limit"]) logger.info("Processing complete: %d restaurants extracted", rest_count) _update_last("last_process_at") except Exception as e: logger.error("Video processing failed: %s", e) def run_loop() -> None: """Run daemon loop, checking config every CHECK_INTERVAL seconds.""" logger.info("Daemon started (config-driven, check every %ds)", CHECK_INTERVAL) while True: run_once_if_due() time.sleep(CHECK_INTERVAL)