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:
2026-06-01 19:47:44 +05:00
parent a64749ccc8
commit 8e4844111b
9 changed files with 114 additions and 40 deletions
+4 -3
View File
@@ -22,9 +22,10 @@ class Supervisor:
return go2rtc.restream_main_url(self.config, cam.id)
def _new_task(self, cam: Camera) -> RecorderTask:
# пишем в активное хранилище; смена активного → restart_all репойнтит все задачи
active = self.config.active_storage()
return RecorderTask(cam, active, self.config.storage.segment_seconds,
# хранилище камеры: её 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: