diff --git a/frigate/api/export.py b/frigate/api/export.py index c88e0146a..1f4e8e417 100644 --- a/frigate/api/export.py +++ b/frigate/api/export.py @@ -4,6 +4,7 @@ import logging from pathlib import Path from typing import Optional +import psutil from flask import ( Blueprint, current_app, @@ -14,6 +15,7 @@ from flask import ( from peewee import DoesNotExist from werkzeug.utils import secure_filename +from frigate.const import EXPORT_DIR from frigate.models import Export, Recordings from frigate.record.export import PlaybackFactorEnum, RecordingExporter @@ -140,6 +142,27 @@ def export_delete(id: str): 404, ) + files_in_use = [] + for process in psutil.process_iter(): + try: + if process.name() != "ffmpeg": + continue + flist = process.open_files() + if flist: + for nt in flist: + if nt.path.startswith(EXPORT_DIR): + files_in_use.append(nt.path.split("/")[-1]) + except psutil.Error: + continue + + if export.video_path.split("/")[-1] in files_in_use: + return make_response( + jsonify( + {"success": False, "message": "Can not delete in progress export."} + ), + 400, + ) + Path(export.video_path).unlink(missing_ok=True) if export.thumb_path: diff --git a/frigate/record/export.py b/frigate/record/export.py index efc155838..6e30656d4 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -11,6 +11,8 @@ import threading from enum import Enum from pathlib import Path +from peewee import DoesNotExist + from frigate.config import FrigateConfig from frigate.const import ( CACHE_DIR, @@ -72,30 +74,32 @@ class RecordingExporter(threading.Thread): if datetime.datetime.fromtimestamp( self.start_time - ) < datetime.datetime.now().replace(minute=0, second=0): + ) < datetime.datetime.now().astimezone(datetime.timezone.dst).replace( + minute=0, second=0, microsecond=0 + ): # has preview mp4 - preview: Previews = ( - Previews.select( - Previews.camera, - Previews.path, - Previews.duration, - Previews.start_time, - Previews.end_time, - ) - .where( - Previews.start_time.between(self.start_time, self.end_time) - | Previews.end_time.between(self.start_time, self.end_time) - | ( - (self.start_time > Previews.start_time) - & (self.end_time < Previews.end_time) + try: + preview: Previews = ( + Previews.select( + Previews.camera, + Previews.path, + Previews.duration, + Previews.start_time, + Previews.end_time, ) + .where( + Previews.start_time.between(self.start_time, self.end_time) + | Previews.end_time.between(self.start_time, self.end_time) + | ( + (self.start_time > Previews.start_time) + & (self.end_time < Previews.end_time) + ) + ) + .where(Previews.camera == self.camera) + .limit(1) + .get() ) - .where(Previews.camera == self.camera) - .limit(1) - .get() - ) - - if not preview: + except DoesNotExist: return "" diff = self.start_time - preview.start_time diff --git a/web/src/components/card/AnimatedEventCard.tsx b/web/src/components/card/AnimatedEventCard.tsx index a032ecda6..acacf4707 100644 --- a/web/src/components/card/AnimatedEventCard.tsx +++ b/web/src/components/card/AnimatedEventCard.tsx @@ -14,8 +14,12 @@ import { baseUrl } from "@/api/baseUrl"; type AnimatedEventCardProps = { event: ReviewSegment; + selectedGroup?: string; }; -export function AnimatedEventCard({ event }: AnimatedEventCardProps) { +export function AnimatedEventCard({ + event, + selectedGroup, +}: AnimatedEventCardProps) { const { data: config } = useSWR("config"); const currentHour = useMemo(() => isCurrentHour(event.start_time), [event]); @@ -53,7 +57,8 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) { const navigate = useNavigate(); const onOpenReview = useCallback(() => { - navigate("review", { + const url = selectedGroup ? `review?group=${selectedGroup}` : "review"; + navigate(url, { state: { severity: event.severity, recording: { @@ -64,7 +69,7 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) { }, }); axios.post(`reviews/viewed`, { ids: [event.id] }); - }, [navigate, event]); + }, [navigate, selectedGroup, event]); // image behavior diff --git a/web/src/components/card/ExportCard.tsx b/web/src/components/card/ExportCard.tsx index ec305648f..ce21113e7 100644 --- a/web/src/components/card/ExportCard.tsx +++ b/web/src/components/card/ExportCard.tsx @@ -109,43 +109,38 @@ export default function ExportCard({ "relative flex aspect-video items-center justify-center rounded-lg bg-black md:rounded-2xl", className, )} - onMouseEnter={ - isDesktop && !exportedRecording.in_progress - ? () => setHovered(true) - : undefined - } - onMouseLeave={ - isDesktop && !exportedRecording.in_progress - ? () => setHovered(false) - : undefined - } - onClick={ - isDesktop || exportedRecording.in_progress - ? undefined - : () => setHovered(!hovered) - } + onMouseEnter={isDesktop ? () => setHovered(true) : undefined} + onMouseLeave={isDesktop ? () => setHovered(false) : undefined} + onClick={isDesktop ? undefined : () => setHovered(!hovered)} > {hovered && ( <>
- - - + {!exportedRecording.in_progress && ( + + + + + + )} + {!exportedRecording.in_progress && ( + + setEditName({ + original: exportedRecording.name, + update: "", + }) + } + > + - - - setEditName({ original: exportedRecording.name, update: "" }) - } - > - - + )} @@ -159,15 +154,17 @@ export default function ExportCard({
- + {!exportedRecording.in_progress && ( + + )} )} {exportedRecording.in_progress ? ( diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index 8c9debc64..be7e253a8 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -68,6 +68,19 @@ export default function Events() { const [reviewFilter, setReviewFilter, reviewSearchParams] = useApiFilter(); + useSearchEffect("group", (reviewGroup) => { + if (config && reviewGroup) { + const group = config.camera_groups[reviewGroup]; + + if (group) { + setReviewFilter({ + ...reviewFilter, + cameras: group.cameras, + }); + } + } + }); + const onUpdateFilter = useCallback( (newFilter: ReviewFilter) => { setReviewFilter(newFilter); diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index c4bd1857f..c8871500f 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -668,27 +668,32 @@ function Timeline({ ) ) : ( -
- {mainCameraReviewItems.map((review) => { - if (review.severity == "significant_motion") { - return; - } +
+
+ {mainCameraReviewItems.map((review) => { + if (review.severity == "significant_motion") { + return; + } - return ( - { - setScrubbing(true); - setCurrentTime(review.start_time - REVIEW_PADDING); - setScrubbing(false); - }} - /> - ); - })} + return ( + { + setScrubbing(true); + setCurrentTime(review.start_time - REVIEW_PADDING); + setScrubbing(false); + }} + /> + ); + })} +
)}
diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index 29130013a..a65591923 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -91,8 +91,16 @@ export default function DraggableGridLayout({ ); }, [config]); + // editing + const [editGroup, setEditGroup] = useState(false); + useEffect(() => { + setEditGroup(false); + }, [cameraGroup]); + + // camera state + const [currentCameras, setCurrentCameras] = useState(); const [currentIncludeBirdseye, setCurrentIncludeBirdseye] = useState(); diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index fe61c45c0..6e847036e 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -224,7 +224,13 @@ export default function LiveDashboardView({
{events.map((event) => { - return ; + return ( + + ); })}