Files
CoRE.Vision/backend/app/recorder/supervisor.py
T
git_admin 8e4844111b storage: per-camera target + date subfolders; move active-storage widget
- Camera.storage_id: each camera chooses its storage ("" = active/default).
  supervisor resolves per-camera (storage_by_id → active fallback); cameras
  API (body/payload) + /toggle accept storage_id and repoint the recorder.
- Recordings now go to <root>/<cam>/<YYYY-MM-DD>/<file>.mp4. This ffmpeg build
  lacks -strftime_mkdir, so recorder_task pre-creates today+tomorrow dirs at
  start and the indexer re-ensures them each tick (survives midnight).
- indexer: scan date subdirs + legacy flat files; sort by basename so the
  in-progress segment is detected correctly when both layouts coexist.
- frontend: per-camera "Хранилище" dropdown in the camera table (inline) and
  in the add/edit + bulk forms. Move the active-storage summary widget from
  the Система tab into the Хранилища tab.

VERSION 0.0.129.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 19:47:44 +05:00

86 lines
3.3 KiB
Python

"""Супервизор: владеет набором RecorderTask по всем включённым камерам."""
from __future__ import annotations
import logging
from ..config import Camera, Config
from ..models import CameraStatus
from ..services import go2rtc
from .recorder_task import RecorderTask
log = logging.getLogger("nvr.supervisor")
class Supervisor:
def __init__(self, config: Config, on_status=None):
self.config = config
self.on_status = on_status
self.tasks: dict[str, RecorderTask] = {}
def _input_url(self, cam: Camera) -> str:
# запись основного потока идёт через рестрим go2rtc (одна сессия к камере)
return go2rtc.restream_main_url(self.config, cam.id)
def _new_task(self, cam: Camera) -> RecorderTask:
# хранилище камеры: её storage_id, иначе активное (по умолчанию). Несуществующий
# id (удалённое хранилище) → тоже откат на активное.
store = self.config.storage_by_id(cam.storage_id) or self.config.active_storage()
return RecorderTask(cam, store, self.config.storage.segment_seconds,
on_status=self.on_status, input_url=self._input_url(cam))
def start_all(self) -> None:
for cam in self.config.cameras:
task = self._new_task(cam)
self.tasks[cam.id] = task
if cam.enabled and cam.record:
task.start()
log.info("recorder started: %s", cam.id)
else:
log.info("recorder skipped (enabled=%s record=%s): %s", cam.enabled, cam.record, cam.id)
async def stop_all(self) -> None:
for task in self.tasks.values():
await task.stop()
def statuses(self) -> list[CameraStatus]:
return [t.status for t in self.tasks.values()]
def status(self, cam_id: str) -> CameraStatus | None:
task = self.tasks.get(cam_id)
return task.status if task else None
def log_tail(self, cam_id: str) -> list[str]:
task = self.tasks.get(cam_id)
return task.log_tail if task else []
async def restart(self, cam_id: str) -> bool:
task = self.tasks.get(cam_id)
if not task:
return False
await task.stop()
task.start()
return True
def add_camera(self, cam: Camera) -> None:
"""Создаёт задачу записи для новой камеры и запускает (если включена)."""
task = self._new_task(cam)
self.tasks[cam.id] = task
if cam.enabled and cam.record:
task.start()
log.info("recorder added: %s", cam.id)
async def remove_camera(self, cam_id: str) -> None:
task = self.tasks.pop(cam_id, None)
if task:
await task.stop()
log.info("recorder removed: %s", cam_id)
async def replace_camera(self, cam: Camera) -> None:
await self.remove_camera(cam.id)
self.add_camera(cam)
async def restart_all(self) -> None:
"""Пересоздаёт все задачи (например, после смены настроек хранилища)."""
for cam in list(self.config.cameras):
await self.replace_camera(cam)