From 5764eaddaa2c4d7184a99e3139c55189e50382b5 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:08:56 -0500 Subject: [PATCH] miscellaneous fixes - keep base path in recording share links - reject non-positive scale on recordings/unavailable, which looped forever on the event loop - reject negative vod event padding, which cleared has_clip - clamp negative preview gif/mp4 offsets - don't delete exports that are still in progress - hash on unknown login usernames so timing doesn't reveal valid users - include the first minute of the events time-of-day filter - show an error when the push subscription is refused - don't wipe an unnamed known plates entry when Add is pressed again - use elapsed hours for export last N hours so DST doesn't shift the range --- docs/static/frigate-api.yaml | 2 ++ frigate/api/auth.py | 4 +++ .../defs/query/recordings_query_parameters.py | 4 +-- frigate/api/event.py | 8 ++--- frigate/api/export.py | 2 +- frigate/api/media.py | 6 ++-- frigate/test/http_api/test_http_event.py | 10 ++++-- frigate/test/http_api/test_http_export.py | 24 ++++++++++++++ frigate/test/http_api/test_http_media.py | 4 +++ .../NotificationsSettingsExtras.tsx | 7 ++++ .../theme/fields/KnownPlatesField.tsx | 3 ++ web/src/components/overlay/ExportDialog.tsx | 32 ++----------------- web/src/utils/recordingReviewUrl.ts | 3 +- 13 files changed, 65 insertions(+), 44 deletions(-) diff --git a/docs/static/frigate-api.yaml b/docs/static/frigate-api.yaml index 4d61778cf8..ca73bf24f3 100644 --- a/docs/static/frigate-api.yaml +++ b/docs/static/frigate-api.yaml @@ -6371,6 +6371,7 @@ paths: required: false schema: type: integer + minimum: 0 description: Padding to apply to the vod. default: 0 title: Padding @@ -7576,6 +7577,7 @@ paths: required: false schema: type: integer + minimum: 1 default: 30 title: Scale responses: diff --git a/frigate/api/auth.py b/frigate/api/auth.py index 49fd641c33..c712ec925e 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -966,6 +966,10 @@ def login(request: Request, body: AppPostLoginBody): try: db_user: User = User.get_by_id(user) except DoesNotExist: + # Hash anyway so response timing doesn't reveal which usernames exist + hash_password( + password, iterations=request.app.frigate_config.auth.hash_iterations + ) logger.warning(f"Login failed for unknown user '{user}' from {remote_addr}") failed_logins.record(user, time.time()) return JSONResponse(content={"message": "Login failed"}, status_code=401) diff --git a/frigate/api/defs/query/recordings_query_parameters.py b/frigate/api/defs/query/recordings_query_parameters.py index cd7b4221c2..d10db010fb 100644 --- a/frigate/api/defs/query/recordings_query_parameters.py +++ b/frigate/api/defs/query/recordings_query_parameters.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel +from pydantic import BaseModel, Field from pydantic.json_schema import SkipJsonSchema @@ -11,7 +11,7 @@ class MediaRecordingsAvailabilityQueryParams(BaseModel): cameras: str = "all" before: float | SkipJsonSchema[None] = None after: float | SkipJsonSchema[None] = None - scale: int = 30 + scale: int = Field(default=30, ge=1) class RecordingsDeleteQueryParams(BaseModel): diff --git a/frigate/api/event.py b/frigate/api/event.py index 3137b124e6..7640e979c9 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -291,12 +291,12 @@ def events( clauses.append( reduce( operator.or_, - [(start_hour_fun > time_after), (start_hour_fun < time_before)], + [(start_hour_fun >= time_after), (start_hour_fun < time_before)], ) ) # all other cases should be and operator else: - clauses.append(start_hour_fun > time_after) + clauses.append(start_hour_fun >= time_after) clauses.append(start_hour_fun < time_before) if has_clip is not None: @@ -749,12 +749,12 @@ def events_search( event_filters.append( reduce( operator.or_, - [(start_hour_fun > time_after), (start_hour_fun < time_before)], + [(start_hour_fun >= time_after), (start_hour_fun < time_before)], ) ) # all other cases should be and operator else: - event_filters.append(start_hour_fun > time_after) + event_filters.append(start_hour_fun >= time_after) event_filters.append(start_hour_fun < time_before) # Perform semantic search diff --git a/frigate/api/export.py b/frigate/api/export.py index 5873a565c8..4d1017fc88 100644 --- a/frigate/api/export.py +++ b/frigate/api/export.py @@ -1227,7 +1227,7 @@ def bulk_delete_exports(body: ExportBulkDeleteBody): files_in_use = _get_files_in_use() for export in exports: - if export.video_path.split("/")[-1] in files_in_use: + if export.in_progress or export.video_path.split("/")[-1] in files_in_use: return JSONResponse( content={ "success": False, diff --git a/frigate/api/media.py b/frigate/api/media.py index 34448bddc0..f1797658af 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -862,7 +862,7 @@ async def vod_hour( async def vod_event( request: Request, event_id: str, - padding: int = Query(0, description="Padding to apply to the vod."), + padding: int = Query(0, ge=0, description="Padding to apply to the vod."), ): try: event: Event = Event.get(Event.id == event_id) @@ -1530,7 +1530,7 @@ async def preview_gif( status_code=404, ) - diff = start_ts - preview.start_time + diff = max(start_ts - preview.start_time, 0) minutes = int(diff / 60) seconds = int(diff % 60) config: FrigateConfig = request.app.frigate_config @@ -1721,7 +1721,7 @@ async def preview_mp4( status_code=404, ) - diff = start_ts - preview.start_time + diff = max(start_ts - preview.start_time, 0) minutes = int(diff / 60) seconds = int(diff % 60) config: FrigateConfig = request.app.frigate_config diff --git a/frigate/test/http_api/test_http_event.py b/frigate/test/http_api/test_http_event.py index 8de5eb9dbc..f6b128b5fa 100644 --- a/frigate/test/http_api/test_http_event.py +++ b/frigate/test/http_api/test_http_event.py @@ -393,8 +393,8 @@ class TestHttpApp(BaseTestHttp): def test_event_time_filtering(self): morning_id = "123456.random" evening_id = "654321.random" - morning = 1656590400 # 06/30/2022 6 am (GMT) - evening = 1656633600 # 06/30/2022 6 pm (GMT) + morning = 1656568800 # 06/30/2022 6 am (GMT) + evening = 1656612000 # 06/30/2022 6 pm (GMT) with AuthTestClient(self.app) as client: super().insert_mock_event(morning_id, morning) @@ -417,6 +417,12 @@ class TestHttpApp(BaseTestHttp): ).json() assert events assert len(events) == 1 + # an event in the first minute of the range is included + events = client.get( + "/events", + params={"time_range": "06:00,24:00"}, + ).json() + assert len(events) == 2 def test_set_delete_sub_label(self): mock_event_updater = Mock(spec=EventMetadataPublisher) diff --git a/frigate/test/http_api/test_http_export.py b/frigate/test/http_api/test_http_export.py index 13f9422610..ae46ce52be 100644 --- a/frigate/test/http_api/test_http_export.py +++ b/frigate/test/http_api/test_http_export.py @@ -427,6 +427,30 @@ class TestHttpExport(BaseTestHttp): assert response.status_code == 400 assert Export.get(Export.id == "front_door_running").video_path == video + def test_delete_export_rejected_while_in_progress(self): + with tempfile.TemporaryDirectory() as tmpdir: + video = os.path.join(tmpdir, "front_door_abc123.mp4") + with open(video, "wb") as handle: + handle.write(b"video") + + Export.create( + id="front_door_running", + camera="front_door", + name="front door export", + date=100, + video_path=video, + thumb_path=os.path.join(tmpdir, "t.webp"), + in_progress=True, + ) + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/delete", json={"ids": ["front_door_running"]} + ) + + assert response.status_code == 400 + assert os.path.exists(video) + def test_rename_export_missing_file_leaves_the_row_alone(self): with tempfile.TemporaryDirectory() as tmpdir: video = os.path.join(tmpdir, "front_door_gone_abc123.mp4") diff --git a/frigate/test/http_api/test_http_media.py b/frigate/test/http_api/test_http_media.py index 047d7de01a..ba7c7babae 100644 --- a/frigate/test/http_api/test_http_media.py +++ b/frigate/test/http_api/test_http_media.py @@ -584,6 +584,10 @@ class TestHttpMedia(BaseTestHttp): {"start_time": 1160, "end_time": 1300}, ] + # a non-positive scale would never advance the gap scan + response = client.get("/recordings/unavailable", params={"scale": 0}) + assert response.status_code == 422 + def test_recordings_summary_day_attribution_by_start_time(self): """ A recording spanning midnight marks only its start day. diff --git a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx index 3db2a8203b..b1ddee22b1 100644 --- a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx +++ b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx @@ -416,6 +416,13 @@ export default function NotificationsSettingsExtras({ workerRegistration.unregister(); setRegistration(null); }); + }) + .catch(() => { + toast.error(t("notification.toast.error.registerFailed"), { + position: "top-center", + }); + workerRegistration.unregister(); + setRegistration(null); }); }, [addMessage, publicKey, t], diff --git a/web/src/components/config-form/theme/fields/KnownPlatesField.tsx b/web/src/components/config-form/theme/fields/KnownPlatesField.tsx index 7b5d81cc8a..b0d753b462 100644 --- a/web/src/components/config-form/theme/fields/KnownPlatesField.tsx +++ b/web/src/components/config-form/theme/fields/KnownPlatesField.tsx @@ -307,6 +307,9 @@ export function KnownPlatesField(props: FieldProps) { ); const handleAddEntry = useCallback(() => { + if ("" in data) { + return; + } const next = { ...data, "": [""] }; onChange(next, fieldPath); }, [data, fieldPath, onChange]); diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index e53190b160..73ee6b375c 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -690,36 +690,8 @@ export function ExportContent({ (option: ExportOption) => { setSelectedOption(option); - const now = new Date(latestTime * 1000); - let start: number; - - switch (option) { - case "1": - now.setHours(now.getHours() - 1); - start = now.getTime() / 1000; - break; - case "4": - now.setHours(now.getHours() - 4); - start = now.getTime() / 1000; - break; - case "8": - now.setHours(now.getHours() - 8); - start = now.getTime() / 1000; - break; - case "12": - now.setHours(now.getHours() - 12); - start = now.getTime() / 1000; - break; - case "24": - now.setHours(now.getHours() - 24); - start = now.getTime() / 1000; - break; - case "custom": - start = latestTime - 3600; - break; - default: - start = latestTime - 3600; - } + const hours = Number(option); + const start = latestTime - (Number.isNaN(hours) ? 1 : hours) * 3600; setRange({ before: latestTime, diff --git a/web/src/utils/recordingReviewUrl.ts b/web/src/utils/recordingReviewUrl.ts index 0bc8960d07..e9f80c1cbf 100644 --- a/web/src/utils/recordingReviewUrl.ts +++ b/web/src/utils/recordingReviewUrl.ts @@ -45,8 +45,7 @@ export function createRecordingReviewUrl( pathname: string, state: RecordingReviewLinkState, ): string { - const url = new URL(baseUrl); - url.pathname = pathname.startsWith("/") ? pathname : `/${pathname}`; + const url = new URL(pathname.replace(/^\//, ""), baseUrl); url.searchParams.set( RECORDING_REVIEW_LINK_PARAM, `${state.camera}_${Math.floor(state.timestamp)}`,