Add ability to name exports

This commit is contained in:
Nicolas Mowen 2024-03-26 13:04:38 -06:00
parent 63df9d4338
commit 549b40d7c7
3 changed files with 27 additions and 9 deletions

View File

@ -8,6 +8,7 @@ import re
import subprocess as sp import subprocess as sp
import time import time
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional
from urllib.parse import unquote from urllib.parse import unquote
import cv2 import cv2
@ -618,6 +619,7 @@ def export_recording(camera_name: str, start_time, end_time):
json: dict[str, any] = request.get_json(silent=True) or {} json: dict[str, any] = request.get_json(silent=True) or {}
playback_factor = json.get("playback", "realtime") playback_factor = json.get("playback", "realtime")
name: Optional[str] = json.get("name")
recordings_count = ( recordings_count = (
Recordings.select() Recordings.select()
@ -641,6 +643,7 @@ def export_recording(camera_name: str, start_time, end_time):
exporter = RecordingExporter( exporter = RecordingExporter(
current_app.frigate_config, current_app.frigate_config,
camera_name, camera_name,
secure_filename(name.replace(" ", "_")) if name else None,
int(start_time), int(start_time),
int(end_time), int(end_time),
( (

View File

@ -38,6 +38,7 @@ class RecordingExporter(threading.Thread):
self, self,
config: FrigateConfig, config: FrigateConfig,
camera: str, camera: str,
name: str,
start_time: int, start_time: int,
end_time: int, end_time: int,
playback_factor: PlaybackFactorEnum, playback_factor: PlaybackFactorEnum,
@ -45,6 +46,7 @@ class RecordingExporter(threading.Thread):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.config = config self.config = config
self.camera = camera self.camera = camera
self.user_provided_name = name
self.start_time = start_time self.start_time = start_time
self.end_time = end_time self.end_time = end_time
self.playback_factor = playback_factor self.playback_factor = playback_factor
@ -57,8 +59,12 @@ class RecordingExporter(threading.Thread):
logger.debug( logger.debug(
f"Beginning export for {self.camera} from {self.start_time} to {self.end_time}" f"Beginning export for {self.camera} from {self.start_time} to {self.end_time}"
) )
file_name = f"{EXPORT_DIR}/in_progress.{self.camera}@{self.get_datetime_from_timestamp(self.start_time)}__{self.get_datetime_from_timestamp(self.end_time)}.mp4" file_name = (
final_file_name = f"{EXPORT_DIR}/{self.camera}_{self.get_datetime_from_timestamp(self.start_time)}__{self.get_datetime_from_timestamp(self.end_time)}.mp4" self.user_provided_name
or f"{self.camera}@{self.get_datetime_from_timestamp(self.start_time)}__{self.get_datetime_from_timestamp(self.end_time)}"
)
file_path = f"{EXPORT_DIR}/in_progress.{file_name}.mp4"
final_file_path = f"{EXPORT_DIR}/{file_name}.mp4"
if (self.end_time - self.start_time) <= MAX_PLAYLIST_SECONDS: if (self.end_time - self.start_time) <= MAX_PLAYLIST_SECONDS:
playlist_lines = f"http://127.0.0.1:5000/vod/{self.camera}/start/{self.start_time}/end/{self.end_time}/index.m3u8" playlist_lines = f"http://127.0.0.1:5000/vod/{self.camera}/start/{self.start_time}/end/{self.end_time}/index.m3u8"
@ -97,14 +103,14 @@ class RecordingExporter(threading.Thread):
if self.playback_factor == PlaybackFactorEnum.realtime: if self.playback_factor == PlaybackFactorEnum.realtime:
ffmpeg_cmd = ( ffmpeg_cmd = (
f"ffmpeg -hide_banner {ffmpeg_input} -c copy {file_name}" f"ffmpeg -hide_banner {ffmpeg_input} -c copy {file_path}"
).split(" ") ).split(" ")
elif self.playback_factor == PlaybackFactorEnum.timelapse_25x: elif self.playback_factor == PlaybackFactorEnum.timelapse_25x:
ffmpeg_cmd = ( ffmpeg_cmd = (
parse_preset_hardware_acceleration_encode( parse_preset_hardware_acceleration_encode(
self.config.ffmpeg.hwaccel_args, self.config.ffmpeg.hwaccel_args,
f"{TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}", f"{TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}",
f"{self.config.cameras[self.camera].record.export.timelapse_args} {file_name}", f"{self.config.cameras[self.camera].record.export.timelapse_args} {file_path}",
EncodeTypeEnum.timelapse, EncodeTypeEnum.timelapse,
) )
).split(" ") ).split(" ")
@ -122,9 +128,9 @@ class RecordingExporter(threading.Thread):
f"Failed to export recording for command {' '.join(ffmpeg_cmd)}" f"Failed to export recording for command {' '.join(ffmpeg_cmd)}"
) )
logger.error(p.stderr) logger.error(p.stderr)
Path(file_name).unlink(missing_ok=True) Path(file_path).unlink(missing_ok=True)
return return
logger.debug(f"Updating finalized export {file_name}") logger.debug(f"Updating finalized export {file_path}")
os.rename(file_name, final_file_name) os.rename(file_path, final_file_path)
logger.debug(f"Finished exporting {file_name}") logger.debug(f"Finished exporting {file_path}")

View File

@ -15,6 +15,7 @@ import { ExportMode } from "@/types/filter";
import { FaArrowDown } from "react-icons/fa"; import { FaArrowDown } from "react-icons/fa";
import axios from "axios"; import axios from "axios";
import { toast } from "sonner"; import { toast } from "sonner";
import { Input } from "../ui/input";
const EXPORT_OPTIONS = [ const EXPORT_OPTIONS = [
"1", "1",
@ -38,6 +39,7 @@ export default function ExportDialog({
setMode, setMode,
}: ExportDialogProps) { }: ExportDialogProps) {
const [selectedOption, setSelectedOption] = useState<ExportOption>("1"); const [selectedOption, setSelectedOption] = useState<ExportOption>("1");
const [name, setName] = useState("");
const onStartExport = useCallback(() => { const onStartExport = useCallback(() => {
const now = new Date(); const now = new Date();
@ -73,6 +75,7 @@ export default function ExportDialog({
axios axios
.post(`export/${camera}/start/${start}/end/${end}`, { .post(`export/${camera}/start/${start}/end/${end}`, {
playback: "realtime", playback: "realtime",
name,
}) })
.then((response) => { .then((response) => {
if (response.status == 200) { if (response.status == 200) {
@ -94,7 +97,7 @@ export default function ExportDialog({
}); });
} }
}); });
}, [camera, selectedOption]); }, [camera, name, selectedOption]);
return ( return (
<Dialog open={mode == "select"}> <Dialog open={mode == "select"}>
@ -138,6 +141,12 @@ export default function ExportDialog({
); );
})} })}
</RadioGroup> </RadioGroup>
<Input
type="search"
placeholder="Name the Export"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<DialogFooter> <DialogFooter>
<DialogClose onClick={() => setMode("none")}>Cancel</DialogClose> <DialogClose onClick={() => setMode("none")}>Cancel</DialogClose>
<Button <Button