fix lịch ghi hình và ngày lưu
This commit is contained in:
@@ -21,6 +21,7 @@ from .models import (
|
|||||||
MediaMTXAddCameraRequest,
|
MediaMTXAddCameraRequest,
|
||||||
MediaMTXCamera,
|
MediaMTXCamera,
|
||||||
MediaMTXConfigView,
|
MediaMTXConfigView,
|
||||||
|
RecordDeleteAfterUpdate,
|
||||||
RecordingToggle,
|
RecordingToggle,
|
||||||
ScheduleUpdate,
|
ScheduleUpdate,
|
||||||
SchedulerEnabled,
|
SchedulerEnabled,
|
||||||
@@ -111,9 +112,35 @@ def _set_recording_in_mediamtx_yml(enabled: bool) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_record_delete_after_days(value: object) -> Optional[int]:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return None
|
||||||
|
text = value.strip().lower()
|
||||||
|
if text == "24h":
|
||||||
|
return 1
|
||||||
|
if text == "72h":
|
||||||
|
return 3
|
||||||
|
if text == "168h":
|
||||||
|
return 7
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _set_record_delete_after_days_in_mediamtx_yml(days: int) -> None:
|
||||||
|
if days not in {1, 3, 7}:
|
||||||
|
raise HTTPException(status_code=400, detail="invalid_record_delete_after_days")
|
||||||
|
data = _load_mediamtx_yml()
|
||||||
|
path_defaults = data.setdefault("pathDefaults", {})
|
||||||
|
if not isinstance(path_defaults, dict):
|
||||||
|
raise HTTPException(status_code=400, detail="invalid_mediamtx_path_defaults")
|
||||||
|
path_defaults["recordDeleteAfter"] = f"{days * 24}h"
|
||||||
|
data["pathDefaults"] = path_defaults
|
||||||
|
_save_mediamtx_yml(data)
|
||||||
|
|
||||||
|
|
||||||
def _build_mediamtx_view(data: dict, cfg: AppConfig) -> MediaMTXConfigView:
|
def _build_mediamtx_view(data: dict, cfg: AppConfig) -> MediaMTXConfigView:
|
||||||
path_defaults = data.get("pathDefaults") or {}
|
path_defaults = data.get("pathDefaults") or {}
|
||||||
record_enabled = bool(path_defaults.get("record", False))
|
record_enabled = bool(path_defaults.get("record", False))
|
||||||
|
record_delete_after_days = _parse_record_delete_after_days(path_defaults.get("recordDeleteAfter"))
|
||||||
|
|
||||||
cameras: list[MediaMTXCamera] = []
|
cameras: list[MediaMTXCamera] = []
|
||||||
paths = data.get("paths") or {}
|
paths = data.get("paths") or {}
|
||||||
@@ -130,6 +157,7 @@ def _build_mediamtx_view(data: dict, cfg: AppConfig) -> MediaMTXConfigView:
|
|||||||
api_url=cfg.mediamtx_api_url,
|
api_url=cfg.mediamtx_api_url,
|
||||||
webrtc_url=cfg.mediamtx_webrtc_url,
|
webrtc_url=cfg.mediamtx_webrtc_url,
|
||||||
record_enabled=record_enabled,
|
record_enabled=record_enabled,
|
||||||
|
record_delete_after_days=record_delete_after_days,
|
||||||
cameras=cameras,
|
cameras=cameras,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -314,6 +342,14 @@ async def set_mediamtx_recording(data: RecordingToggle) -> MediaMTXConfigView:
|
|||||||
return _build_mediamtx_view(payload, cfg)
|
return _build_mediamtx_view(payload, cfg)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/mediamtx/record-delete-after")
|
||||||
|
async def set_record_delete_after(data: RecordDeleteAfterUpdate) -> MediaMTXConfigView:
|
||||||
|
cfg = await store.load()
|
||||||
|
_set_record_delete_after_days_in_mediamtx_yml(data.days)
|
||||||
|
payload = _load_mediamtx_yml()
|
||||||
|
return _build_mediamtx_view(payload, cfg)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/mediamtx/restart")
|
@app.post("/api/mediamtx/restart")
|
||||||
async def restart_mediamtx() -> dict:
|
async def restart_mediamtx() -> dict:
|
||||||
return _restart_mediamtx()
|
return _restart_mediamtx()
|
||||||
|
|||||||
@@ -61,8 +61,13 @@ class MediaMTXConfigView(BaseModel):
|
|||||||
api_url: str
|
api_url: str
|
||||||
webrtc_url: str
|
webrtc_url: str
|
||||||
record_enabled: bool
|
record_enabled: bool
|
||||||
|
record_delete_after_days: Optional[int] = None
|
||||||
cameras: list[MediaMTXCamera]
|
cameras: list[MediaMTXCamera]
|
||||||
|
|
||||||
|
|
||||||
class MediaMTXAddCameraRequest(BaseModel):
|
class MediaMTXAddCameraRequest(BaseModel):
|
||||||
rtsp_url: str = Field(min_length=1, max_length=2048)
|
rtsp_url: str = Field(min_length=1, max_length=2048)
|
||||||
|
|
||||||
|
|
||||||
|
class RecordDeleteAfterUpdate(BaseModel):
|
||||||
|
days: int = Field(ge=1, le=7)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class Scheduler:
|
|||||||
async def tick(self, schedule: Schedule) -> None:
|
async def tick(self, schedule: Schedule) -> None:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
state = should_record_now(now, schedule)
|
state = should_record_now(now, schedule)
|
||||||
if self._last_state != state:
|
# Always enforce desired state to recover from external/manual drift.
|
||||||
await self.apply(state)
|
await self.apply(state)
|
||||||
self._last_state = state
|
self._last_state = state
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default function Settings() {
|
|||||||
const [mediamtxApiPass, setMediamtxApiPass] = useState("");
|
const [mediamtxApiPass, setMediamtxApiPass] = useState("");
|
||||||
const [recordingsDir, setRecordingsDir] = useState("");
|
const [recordingsDir, setRecordingsDir] = useState("");
|
||||||
const [apiPort, setApiPort] = useState("8008");
|
const [apiPort, setApiPort] = useState("8008");
|
||||||
|
const [recordDeleteAfterDays, setRecordDeleteAfterDays] = useState("7");
|
||||||
|
|
||||||
const loadMtx = async () => {
|
const loadMtx = async () => {
|
||||||
setMtxBusy(true);
|
setMtxBusy(true);
|
||||||
@@ -37,6 +38,7 @@ export default function Settings() {
|
|||||||
try {
|
try {
|
||||||
const data = await apiJson<MediaMtxConfigView>("/mediamtx/config", { method: "GET" });
|
const data = await apiJson<MediaMtxConfigView>("/mediamtx/config", { method: "GET" });
|
||||||
setMtx(data);
|
setMtx(data);
|
||||||
|
setRecordDeleteAfterDays(String(data.record_delete_after_days ?? 7));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (typeof e === "object" && e && "status" in e) {
|
if (typeof e === "object" && e && "status" in e) {
|
||||||
const ex = e as { status: number; bodyText?: string };
|
const ex = e as { status: number; bodyText?: string };
|
||||||
@@ -180,6 +182,28 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onChangeRecordDeleteAfter = async (days: number) => {
|
||||||
|
setMtxBusy(true);
|
||||||
|
setMtxError(null);
|
||||||
|
try {
|
||||||
|
const data = await apiJson<MediaMtxConfigView>("/mediamtx/record-delete-after", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ days }),
|
||||||
|
});
|
||||||
|
setMtx(data);
|
||||||
|
setRecordDeleteAfterDays(String(data.record_delete_after_days ?? days));
|
||||||
|
} catch (e) {
|
||||||
|
if (typeof e === "object" && e && "status" in e) {
|
||||||
|
const ex = e as { status: number; bodyText?: string };
|
||||||
|
setMtxError(`http_${ex.status}${ex.bodyText ? `: ${ex.bodyText}` : ""}`);
|
||||||
|
} else {
|
||||||
|
setMtxError("failed_to_update_record_delete_after");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setMtxBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onRestartDocker = async () => {
|
const onRestartDocker = async () => {
|
||||||
setMtxBusy(true);
|
setMtxBusy(true);
|
||||||
setRestartMsg(null);
|
setRestartMsg(null);
|
||||||
@@ -365,6 +389,20 @@ export default function Settings() {
|
|||||||
MediaMTX record (pathDefaults.record)
|
MediaMTX record (pathDefaults.record)
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-zinc-400">recordDeleteAfter</label>
|
||||||
|
<select
|
||||||
|
value={String(mtx?.record_delete_after_days ?? recordDeleteAfterDays)}
|
||||||
|
onChange={(e) => void onChangeRecordDeleteAfter(Number(e.target.value))}
|
||||||
|
disabled={mtxBusy}
|
||||||
|
className="mt-1 h-9 w-full rounded-md border border-zinc-800 bg-zinc-950 px-3 text-sm text-zinc-100 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
<option value="1">1 ngày</option>
|
||||||
|
<option value="3">3 ngày</option>
|
||||||
|
<option value="7">7 ngày</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label className="flex items-center gap-2 text-sm text-zinc-200">
|
<label className="flex items-center gap-2 text-sm text-zinc-200">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@@ -52,5 +52,6 @@ export type MediaMtxConfigView = {
|
|||||||
api_url: string;
|
api_url: string;
|
||||||
webrtc_url: string;
|
webrtc_url: string;
|
||||||
record_enabled: boolean;
|
record_enabled: boolean;
|
||||||
|
record_delete_after_days?: number | null;
|
||||||
cameras: Camera[];
|
cameras: Camera[];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user