mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-04 22:06:51 +03:00
fix frozen time bounds on the recordings API (#24121)
`after` and `before` defaulted to `datetime.now()` in the function signature, so they were evaluated once at import. Requests that omitted them got a window ending at process start. They now resolve in the handler.
This commit is contained in:
Vendored
+6
-2
@@ -7197,13 +7197,17 @@ paths:
|
|||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: number
|
anyOf:
|
||||||
|
- type: number
|
||||||
|
- type: 'null'
|
||||||
title: After
|
title: After
|
||||||
- name: before
|
- name: before
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: number
|
anyOf:
|
||||||
|
- type: number
|
||||||
|
- type: 'null'
|
||||||
title: Before
|
title: Before
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
|
|||||||
@@ -358,10 +358,13 @@ async def recordings_coverage(
|
|||||||
@router.get("/{camera_name}/recordings", dependencies=[Depends(require_camera_access)])
|
@router.get("/{camera_name}/recordings", dependencies=[Depends(require_camera_access)])
|
||||||
async def recordings(
|
async def recordings(
|
||||||
camera_name: str,
|
camera_name: str,
|
||||||
after: float = (datetime.now() - timedelta(hours=1)).timestamp(),
|
after: float | None = None,
|
||||||
before: float = datetime.now().timestamp(),
|
before: float | None = None,
|
||||||
):
|
):
|
||||||
"""Return specific camera recordings between the given 'after'/'end' times. If not provided the last hour will be used"""
|
"""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 = (
|
||||||
Recordings.select(
|
Recordings.select(
|
||||||
Recordings.id,
|
Recordings.id,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"""Unit tests for recordings/media API endpoints."""
|
"""Unit tests for recordings/media API endpoints."""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime, timedelta
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
@@ -1838,6 +1839,27 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
recording["id"] for recording in response.json()
|
recording["id"] for recording in response.json()
|
||||||
] == expected_ids
|
] == 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):
|
def test_vod_handles_all_range_relations(self):
|
||||||
"""VOD clips every interval relation with positive playback duration."""
|
"""VOD clips every interval relation with positive playback duration."""
|
||||||
with AuthTestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
|
|||||||
@@ -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
|
# 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}
|
# timestamps baked into the schema at import time. They make the export
|
||||||
# /recordings after/before params default to datetime.now()). They make the
|
# non-deterministic and document a meaningless frozen epoch, so they are
|
||||||
# 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"
|
||||||
# stripped. The proper fix is to default those route params to None and resolve
|
# resolve it inside the handler. Kept as a guard against the pattern returning.
|
||||||
# "now" inside the handler.
|
|
||||||
VOLATILE_DEFAULT_THRESHOLD = 1_000_000_000
|
VOLATILE_DEFAULT_THRESHOLD = 1_000_000_000
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user