From 9fca3f276d059214e89ca3bc0305c40f86b50156 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:29:16 -0600 Subject: [PATCH] address review feedback - Discard the export row when an export fails before or during ffmpeg, so it doesn't stay in progress until a restart - Skip the empty thumbnail path when discarding a failed export - Measure preview clip duration from the clamped start - Default live player volume to 1 when the group has no saved setting - Clear the unsaved registration message when notification registration fails - Keep a highlight timer per trigger - Remove the severity order_by in /review, which the start_time order_by replaced --- frigate/api/media.py | 10 ++++++---- frigate/api/review.py | 3 +-- frigate/record/export.py | 7 ++++++- .../sectionExtras/NotificationsSettingsExtras.tsx | 4 +++- web/src/views/live/DraggableGridLayout.tsx | 4 ++-- web/src/views/live/LiveDashboardView.tsx | 2 +- web/src/views/settings/TriggerView.tsx | 13 ++++++++----- 7 files changed, 27 insertions(+), 16 deletions(-) diff --git a/frigate/api/media.py b/frigate/api/media.py index f1797658af..c3b5d4f6cf 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -1530,7 +1530,8 @@ async def preview_gif( status_code=404, ) - diff = max(start_ts - preview.start_time, 0) + clip_start = max(start_ts, preview.start_time) + diff = clip_start - preview.start_time minutes = int(diff / 60) seconds = int(diff % 60) config: FrigateConfig = request.app.frigate_config @@ -1542,7 +1543,7 @@ async def preview_gif( "-ss", f"00:{minutes}:{seconds}", "-t", - f"{end_ts - start_ts}", + f"{end_ts - clip_start}", "-i", preview.path, "-r", @@ -1721,7 +1722,8 @@ async def preview_mp4( status_code=404, ) - diff = max(start_ts - preview.start_time, 0) + clip_start = max(start_ts, preview.start_time) + diff = clip_start - preview.start_time minutes = int(diff / 60) seconds = int(diff % 60) config: FrigateConfig = request.app.frigate_config @@ -1734,7 +1736,7 @@ async def preview_mp4( "-ss", f"00:{minutes}:{seconds}", "-t", - f"{end_ts - start_ts}", + f"{end_ts - clip_start}", "-i", preview.path, "-r", diff --git a/frigate/api/review.py b/frigate/api/review.py index 14f9bb169c..0a0c758436 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -163,8 +163,7 @@ async def review( # Apply ordering and limit review_query = ( - review_query.order_by(ReviewSegment.severity.asc()) - .order_by(ReviewSegment.start_time.desc()) + review_query.order_by(ReviewSegment.start_time.desc()) .limit(limit) .dicts() .iterator() diff --git a/frigate/record/export.py b/frigate/record/export.py index 5efa4914fa..9bfe64e4ac 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -1470,6 +1470,9 @@ class RecordingExporter(threading.Thread): try: self._run_export(video_path, thumb_path) + except Exception: + self._discard_failed_export(video_path, thumb_path) + raise finally: # staged runs hold a full copy of the export, so they must not # survive a failure the way the small chapter file could @@ -1479,7 +1482,8 @@ class RecordingExporter(threading.Thread): """Drop the partial output and the row that promised it.""" Path(video_path).unlink(missing_ok=True) Export.delete().where(Export.id == self.export_id).execute() - Path(thumb_path).unlink(missing_ok=True) + if thumb_path: + Path(thumb_path).unlink(missing_ok=True) def _run_export(self, video_path: str, thumb_path: str) -> None: try: @@ -1498,6 +1502,7 @@ class RecordingExporter(threading.Thread): else: ffmpeg_cmd, playlist_lines = self.get_preview_export_command(video_path) except DoesNotExist: + self._discard_failed_export(video_path, thumb_path) return # When neither custom ffmpeg arg is set the default path uses diff --git a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx index b1ddee22b1..6f38611742 100644 --- a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx +++ b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx @@ -415,6 +415,7 @@ export default function NotificationsSettingsExtras({ pushSubscription.unsubscribe(); workerRegistration.unregister(); setRegistration(null); + removeMessage("notification_settings", "registration"); }); }) .catch(() => { @@ -423,9 +424,10 @@ export default function NotificationsSettingsExtras({ }); workerRegistration.unregister(); setRegistration(null); + removeMessage("notification_settings", "registration"); }); }, - [addMessage, publicKey, t], + [addMessage, removeMessage, publicKey, t], ); useEffect(() => { diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index 4599e6a41f..f1bcb0d097 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -794,7 +794,7 @@ export default function DraggableGridLayout({ toggleAudio={() => toggleAudio(camera.name)} statsState={statsStates[camera.name]} toggleStats={() => toggleStats(camera.name)} - volumeState={volumeStates[camera.name]} + volumeState={volumeStates[camera.name] ?? 1} setVolumeState={(value) => setVolumeStates((prev) => ({ ...prev, @@ -839,7 +839,7 @@ export default function DraggableGridLayout({ onError={(e) => handleError(camera.name, e)} onResetLiveMode={() => resetPreferredLiveMode(camera.name)} playAudio={audioStates[camera.name]} - volume={volumeStates[camera.name]} + volume={volumeStates[camera.name] ?? 1} onLiveAspectChange={liveAspectHandlers[camera.name]} /> diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index fd804becdf..de9396f009 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -629,7 +629,7 @@ export default function LiveDashboardView({ resetPreferredLiveMode(camera.name) } playAudio={audioStates[camera.name] ?? false} - volume={volumeStates[camera.name]} + volume={volumeStates[camera.name] ?? 1} /> ); diff --git a/web/src/views/settings/TriggerView.tsx b/web/src/views/settings/TriggerView.tsx index 9910e43020..73a0b17a8b 100644 --- a/web/src/views/settings/TriggerView.tsx +++ b/web/src/views/settings/TriggerView.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import useSWR from "swr"; @@ -133,6 +133,9 @@ export default function TriggerView({ // watch websocket for updates const { payload: triggers_status_ws } = useTriggers(); + const highlightTimers = useRef>>( + {}, + ); useEffect(() => { if (!triggers_status_ws) return; @@ -148,11 +151,11 @@ export default function TriggerView({ return current; }); - // not cleared on the next payload, or a second trigger firing within - // 3s would leave this one highlighted - setTimeout(() => { + const name = triggers_status_ws.name; + clearTimeout(highlightTimers.current[name]); + highlightTimers.current[name] = setTimeout(() => { setTriggeredTrigger((prev) => - (prev || []).filter((name) => name !== triggers_status_ws.name), + (prev || []).filter((triggered) => triggered !== name), ); }, 3000); }, [triggers_status_ws, selectedCamera, mutate]);