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>
This commit is contained in:
@@ -10,7 +10,7 @@ import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from ..config import Config
|
||||
from ..db import Database
|
||||
@@ -32,7 +32,11 @@ def _parse_started_at(path: str) -> int:
|
||||
|
||||
|
||||
def _scan_segments(root: str) -> dict[str, list[str]]:
|
||||
"""Возвращает {camera_id: [пути mp4, отсортированы]}."""
|
||||
"""Возвращает {camera_id: [пути mp4, отсортированы]}.
|
||||
|
||||
Файлы лежат в подпапке даты <cam>/<YYYY-MM-DD>/*.mp4; старые «плоские»
|
||||
<cam>/*.mp4 тоже подхватываем (обратная совместимость). Сортировка по полному
|
||||
пути == хронологическая (ISO-даты и в каталоге, и в имени)."""
|
||||
result: dict[str, list[str]] = {}
|
||||
if not os.path.isdir(root):
|
||||
return result
|
||||
@@ -40,12 +44,19 @@ def _scan_segments(root: str) -> dict[str, list[str]]:
|
||||
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()
|
||||
files: list[str] = []
|
||||
for entry in os.listdir(cam_dir):
|
||||
p = os.path.join(cam_dir, entry)
|
||||
if entry.endswith(".mp4") and os.path.isfile(p):
|
||||
files.append(p) # старый плоский формат
|
||||
elif os.path.isdir(p): # подпапка даты
|
||||
for fn in os.listdir(p):
|
||||
if fn.endswith(".mp4"):
|
||||
files.append(os.path.join(p, fn))
|
||||
# сортируем по ИМЕНИ файла (YYYY-MM-DD_HH-MM-SS) — хронологически независимо от того,
|
||||
# лежит файл плоско или в подпапке даты (полный путь сортировал бы '/' < '_' неверно,
|
||||
# из-за чего пишущийся сейчас файл в подпапке не оказывался бы последним)
|
||||
files.sort(key=os.path.basename)
|
||||
if files:
|
||||
result[cam_id] = files
|
||||
return result
|
||||
@@ -74,11 +85,29 @@ class Indexer:
|
||||
async def _loop(self) -> None:
|
||||
while not self._stopping:
|
||||
try:
|
||||
self._ensure_date_dirs()
|
||||
await self.scan_once()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("indexer scan failed")
|
||||
await asyncio.sleep(SCAN_INTERVAL)
|
||||
|
||||
def _ensure_date_dirs(self) -> None:
|
||||
"""Создаёт каталоги даты (сегодня+завтра) для пишущих камер заранее: ffmpeg
|
||||
не умеет -strftime_mkdir, без готового каталога запись встала бы в полночь."""
|
||||
today = datetime.now()
|
||||
days = [today.strftime("%Y-%m-%d"), (today + timedelta(days=1)).strftime("%Y-%m-%d")]
|
||||
for cam in self.config.cameras:
|
||||
if not (cam.enabled and cam.record):
|
||||
continue
|
||||
store = self.config.storage_by_id(cam.storage_id) or self.config.active_storage()
|
||||
if store.type == "nas" and not os.path.ismount(store.path):
|
||||
continue
|
||||
for d in days:
|
||||
try:
|
||||
os.makedirs(os.path.join(store.path, cam.id, d), exist_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
async def scan_once(self) -> int:
|
||||
active_path = self.config.active_storage().path
|
||||
known = await self.db.known_paths()
|
||||
|
||||
Reference in New Issue
Block a user