"""Индексатор архива: периодически сканирует /records, заносит новые сегменты в БД. Логика: самый свежий сегмент каждой камеры ещё пишется ffmpeg-ом — его пропускаем. Все остальные mp4, которых нет в БД, пробуем через ffprobe и добавляем. Параллельно убираем из БД записи о пропавших файлах. """ from __future__ import annotations import asyncio import logging import os import time from datetime import datetime from ..config import Config from ..db import Database from .ffmpeg import probe_duration log = logging.getLogger("nvr.indexer") SCAN_INTERVAL = 30 # сек def _parse_started_at(path: str) -> int: """Время начала сегмента из имени /.mp4.""" try: stem = os.path.splitext(os.path.basename(path))[0] dt = datetime.strptime(stem, "%Y-%m-%d_%H-%M-%S") return int(dt.timestamp()) except ValueError: return int(os.path.getmtime(path)) def _scan_segments(root: str) -> dict[str, list[str]]: """Возвращает {camera_id: [пути mp4, отсортированы]}.""" result: dict[str, list[str]] = {} if not os.path.isdir(root): return result for cam_id in os.listdir(root): cam_dir = os.path.join(root, cam_id) if not os.path.isdir(cam_dir): continue files = [ os.path.join(cam_dir, fn) for fn in os.listdir(cam_dir) if fn.endswith(".mp4") ] files.sort() if files: result[cam_id] = files return result class Indexer: def __init__(self, config: Config, db: Database): self.config = config self.db = db self._task: asyncio.Task | None = None self._stopping = False def start(self) -> None: self._stopping = False self._task = asyncio.create_task(self._loop(), name="indexer") async def stop(self) -> None: self._stopping = True if self._task: self._task.cancel() try: await self._task except asyncio.CancelledError: pass async def _loop(self) -> None: while not self._stopping: try: await self.scan_once() except Exception: # noqa: BLE001 log.exception("indexer scan failed") await asyncio.sleep(SCAN_INTERVAL) async def scan_once(self) -> int: root = self.config.storage.path by_cam = _scan_segments(root) known = await self.db.known_paths() added = 0 for cam_id, files in by_cam.items(): # последний файл может писаться прямо сейчас — не индексируем complete = files[:-1] if len(files) > 1 else [] for path in complete: if path in known: continue duration, size = await probe_duration(path) if size is None or size == 0: continue await self.db.add_recording( cam_id, path, _parse_started_at(path), duration, size ) added += 1 # уборка пропавших файлов for path in known: if not os.path.exists(path): await self.db.delete_missing(path) if added: log.info("indexer: добавлено сегментов %d", added) return added