diff --git a/docs/static/frigate-api.yaml b/docs/static/frigate-api.yaml index 0bfadd8331..6a63bc98bf 100644 --- a/docs/static/frigate-api.yaml +++ b/docs/static/frigate-api.yaml @@ -7197,13 +7197,17 @@ paths: in: query required: false schema: - type: number + anyOf: + - type: number + - type: 'null' title: After - name: before in: query required: false schema: - type: number + anyOf: + - type: number + - type: 'null' title: Before responses: '200': diff --git a/frigate/api/record.py b/frigate/api/record.py index 9a59e0ccb0..8862e1cd6d 100644 --- a/frigate/api/record.py +++ b/frigate/api/record.py @@ -358,10 +358,13 @@ async def recordings_coverage( @router.get("/{camera_name}/recordings", dependencies=[Depends(require_camera_access)]) async def recordings( camera_name: str, - after: float = (datetime.now() - timedelta(hours=1)).timestamp(), - before: float = datetime.now().timestamp(), + after: float | None = None, + before: float | None = None, ): """Return specific camera recordings between the given 'after'/'end' times. If not provided the last hour will be used""" + now = datetime.now() + after = after if after is not None else (now - timedelta(hours=1)).timestamp() + before = before if before is not None else now.timestamp() recordings = ( Recordings.select( Recordings.id, diff --git a/frigate/test/http_api/test_http_media.py b/frigate/test/http_api/test_http_media.py index 0d385978a2..fd5b6b53a0 100644 --- a/frigate/test/http_api/test_http_media.py +++ b/frigate/test/http_api/test_http_media.py @@ -1,7 +1,8 @@ """Unit tests for recordings/media API endpoints.""" from dataclasses import dataclass -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta +from unittest.mock import patch import pytz from fastapi import Request @@ -1838,6 +1839,27 @@ class TestHttpMedia(BaseTestHttp): recording["id"] for recording in response.json() ] == expected_ids + def test_recordings_default_range_follows_the_request_clock(self): + """Omitted bounds resolve at request time, not at module import time.""" + uptime = timedelta(hours=2) + recorded_at = (datetime.now() + uptime).timestamp() + + class UptimeDatetime(datetime): + """Stands in for a process that booted `uptime` ago.""" + + @classmethod + def now(cls, tz=None): + return datetime.now(tz) + uptime + + with AuthTestClient(self.app) as client: + self._insert_recording("after_boot", recorded_at - 20, recorded_at - 10) + + with patch("frigate.api.record.datetime", UptimeDatetime): + response = client.get("/front_door/recordings") + + assert response.status_code == 200 + assert [recording["id"] for recording in response.json()] == ["after_boot"] + def test_vod_handles_all_range_relations(self): """VOD clips every interval relation with positive playback duration.""" with AuthTestClient(self.app) as client: diff --git a/generate_api_auth_spec.py b/generate_api_auth_spec.py index 43b4ababae..c1d82ab155 100644 --- a/generate_api_auth_spec.py +++ b/generate_api_auth_spec.py @@ -410,11 +410,10 @@ def enrich(spec: dict, access_map: dict) -> tuple[dict, list, list]: # Numeric defaults at or above this magnitude are treated as live Unix -# timestamps baked into the schema at import time (e.g. the /{camera_name} -# /recordings after/before params default to datetime.now()). They make the -# export non-deterministic and document a meaningless frozen epoch, so they are -# stripped. The proper fix is to default those route params to None and resolve -# "now" inside the handler. +# timestamps baked into the schema at import time. They make the export +# non-deterministic and document a meaningless frozen epoch, so they are +# stripped. No route defaults this way today, and route params that need "now" +# resolve it inside the handler. Kept as a guard against the pattern returning. VOLATILE_DEFAULT_THRESHOLD = 1_000_000_000