init
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""Индексатор архива: периодически сканирует /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:
|
||||
"""Время начала сегмента из имени <cam>/<YYYY-MM-DD_HH-MM-SS>.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
|
||||
Reference in New Issue
Block a user