mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-10 00:32:48 +03:00
Compare commits
4
Commits
b02aea03cd
...
de416b7ae7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de416b7ae7 | ||
|
|
06967fec91 | ||
|
|
d69107de33 | ||
|
|
04480a18b6 |
@@ -1,7 +1,7 @@
|
|||||||
default_target: local
|
default_target: local
|
||||||
|
|
||||||
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
|
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
|
||||||
VERSION = 0.18.0
|
VERSION = 0.18.1
|
||||||
IMAGE_REPO ?= ghcr.io/blakeblackshear/frigate
|
IMAGE_REPO ?= ghcr.io/blakeblackshear/frigate
|
||||||
GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
|
GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
BOARDS= #Initialized empty
|
BOARDS= #Initialized empty
|
||||||
|
|||||||
@@ -397,19 +397,11 @@ dmesg | grep -i -E "gpu|drm|reset|hang"
|
|||||||
|
|
||||||
Messages like `trying reset from guc_exec_queue_timedout_job` or similar GPU reset/hang messages indicate a driver or hardware issue. Ensure your kernel and GPU drivers (especially Intel) are up to date.
|
Messages like `trying reset from guc_exec_queue_timedout_job` or similar GPU reset/hang messages indicate a driver or hardware issue. Ensure your kernel and GPU drivers (especially Intel) are up to date.
|
||||||
|
|
||||||
#### Step 6: Verify hardware acceleration configuration
|
#### Step 6: Verify go2rtc stream configuration
|
||||||
|
|
||||||
An incorrect `hwaccel_args` preset can cause ffmpeg to fail silently or consume excessive CPU, starving the detector of resources.
|
|
||||||
|
|
||||||
- After upgrading Frigate, verify your preset matches your hardware (e.g., `preset-intel-qsv-h264` instead of the deprecated `preset-vaapi`).
|
|
||||||
- For h265 cameras, use the corresponding h265 preset (e.g., `preset-intel-qsv-h265`).
|
|
||||||
- Note that `hwaccel_args` are only relevant for the detect stream. Frigate does not decode the record stream.
|
|
||||||
|
|
||||||
#### Step 7: Verify go2rtc stream configuration
|
|
||||||
|
|
||||||
Ensure that the ffmpeg source names in your go2rtc configuration match the correct camera stream. A misconfigured stream name (e.g., copying a config from one camera to another without updating the stream reference) will cause the wrong stream to be used or the stream to fail entirely.
|
Ensure that the ffmpeg source names in your go2rtc configuration match the correct camera stream. A misconfigured stream name (e.g., copying a config from one camera to another without updating the stream reference) will cause the wrong stream to be used or the stream to fail entirely.
|
||||||
|
|
||||||
#### Step 8: Check system resources
|
#### Step 7: Check system resources
|
||||||
|
|
||||||
If none of the above apply, the issue may be a general resource constraint. Monitor the following on your host:
|
If none of the above apply, the issue may be a general resource constraint. Monitor the following on your host:
|
||||||
|
|
||||||
|
|||||||
Vendored
+20
@@ -4073,6 +4073,16 @@ paths:
|
|||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: 100
|
default: 100
|
||||||
title: Limit
|
title: Limit
|
||||||
|
- name: offset
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
minimum: 0
|
||||||
|
- type: 'null'
|
||||||
|
default: 0
|
||||||
|
title: Offset
|
||||||
- name: after
|
- name: after
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
@@ -4378,6 +4388,16 @@ paths:
|
|||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: 50
|
default: 50
|
||||||
title: Limit
|
title: Limit
|
||||||
|
- name: offset
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
minimum: 0
|
||||||
|
- type: 'null'
|
||||||
|
default: 0
|
||||||
|
title: Offset
|
||||||
- name: cameras
|
- name: cameras
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class EventsQueryParams(BaseModel):
|
|||||||
zone: str | None = "all"
|
zone: str | None = "all"
|
||||||
zones: str | None = "all"
|
zones: str | None = "all"
|
||||||
limit: int | None = 100
|
limit: int | None = 100
|
||||||
|
offset: int | None = Field(0, ge=0)
|
||||||
after: float | None = None
|
after: float | None = None
|
||||||
before: float | None = None
|
before: float | None = None
|
||||||
time_range: str | None = DEFAULT_TIME_RANGE
|
time_range: str | None = DEFAULT_TIME_RANGE
|
||||||
@@ -55,6 +56,7 @@ class EventsSearchQueryParams(BaseModel):
|
|||||||
deprecated=True,
|
deprecated=True,
|
||||||
)
|
)
|
||||||
limit: int | None = 50
|
limit: int | None = 50
|
||||||
|
offset: int | None = Field(0, ge=0)
|
||||||
cameras: str | None = "all"
|
cameras: str | None = "all"
|
||||||
labels: str | None = "all"
|
labels: str | None = "all"
|
||||||
sub_labels: str | None = "all"
|
sub_labels: str | None = "all"
|
||||||
|
|||||||
+11
-2
@@ -129,6 +129,7 @@ def events(
|
|||||||
zones = zone
|
zones = zone
|
||||||
|
|
||||||
limit = params.limit
|
limit = params.limit
|
||||||
|
offset = params.offset
|
||||||
after = params.after
|
after = params.after
|
||||||
before = params.before
|
before = params.before
|
||||||
time_range = params.time_range
|
time_range = params.time_range
|
||||||
@@ -361,11 +362,15 @@ def events(
|
|||||||
else:
|
else:
|
||||||
order_by = Event.start_time.desc()
|
order_by = Event.start_time.desc()
|
||||||
|
|
||||||
|
# offset paging needs a stable order when scores or speeds tie
|
||||||
|
tiebreaker = [Event.id] if sort and sort.startswith(("score", "speed")) else []
|
||||||
|
|
||||||
events = (
|
events = (
|
||||||
Event.select(*selected_columns)
|
Event.select(*selected_columns)
|
||||||
.where(reduce(operator.and_, clauses))
|
.where(reduce(operator.and_, clauses))
|
||||||
.order_by(order_by)
|
.order_by(order_by, *tiebreaker)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
.dicts()
|
.dicts()
|
||||||
.iterator()
|
.iterator()
|
||||||
)
|
)
|
||||||
@@ -518,6 +523,7 @@ def events_search(
|
|||||||
search_type = params.search_type
|
search_type = params.search_type
|
||||||
include_thumbnails = params.include_thumbnails
|
include_thumbnails = params.include_thumbnails
|
||||||
limit = params.limit
|
limit = params.limit
|
||||||
|
offset = params.offset
|
||||||
sort = params.sort
|
sort = params.sort
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
@@ -824,6 +830,9 @@ def events_search(
|
|||||||
if search_results:
|
if search_results:
|
||||||
events_query = events_query.where(Event.id << list(search_results.keys()))
|
events_query = events_query.where(Event.id << list(search_results.keys()))
|
||||||
|
|
||||||
|
# sorts below are stable, so this orders ties for offset paging
|
||||||
|
events_query = events_query.order_by(Event.id)
|
||||||
|
|
||||||
# Fetch events and process them in a single pass
|
# Fetch events and process them in a single pass
|
||||||
processed_events = []
|
processed_events = []
|
||||||
for event in events_query.dicts():
|
for event in events_query.dicts():
|
||||||
@@ -881,7 +890,7 @@ def events_search(
|
|||||||
processed_events.sort(key=lambda x: x["start_time"], reverse=True)
|
processed_events.sort(key=lambda x: x["start_time"], reverse=True)
|
||||||
|
|
||||||
# Limit the number of events returned
|
# Limit the number of events returned
|
||||||
processed_events = processed_events[:limit]
|
processed_events = processed_events[offset:][:limit]
|
||||||
|
|
||||||
return JSONResponse(content=processed_events)
|
return JSONResponse(content=processed_events)
|
||||||
|
|
||||||
|
|||||||
+20
-11
@@ -43,6 +43,22 @@ logger = logging.getLogger(__name__)
|
|||||||
router = APIRouter(tags=[Tags.review])
|
router = APIRouter(tags=[Tags.review])
|
||||||
|
|
||||||
|
|
||||||
|
def get_label_clause(label: str, include_audio: bool = True):
|
||||||
|
"""Build a clause matching a label within a review segment's data.
|
||||||
|
|
||||||
|
Verified objects are stored with a `-verified` suffix (eg. `person-verified`)
|
||||||
|
so that variant is matched as well.
|
||||||
|
"""
|
||||||
|
clause = (ReviewSegment.data["objects"].cast("text") % f'*"{label}"*') | (
|
||||||
|
ReviewSegment.data["objects"].cast("text") % f'*"{label}-verified"*'
|
||||||
|
)
|
||||||
|
|
||||||
|
if include_audio:
|
||||||
|
clause |= ReviewSegment.data["audio"].cast("text") % f'*"{label}"*'
|
||||||
|
|
||||||
|
return clause
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/review",
|
"/review",
|
||||||
response_model=list[ReviewSegmentResponse],
|
response_model=list[ReviewSegmentResponse],
|
||||||
@@ -92,10 +108,7 @@ async def review(
|
|||||||
filtered_labels = labels.split(",")
|
filtered_labels = labels.split(",")
|
||||||
|
|
||||||
for label in filtered_labels:
|
for label in filtered_labels:
|
||||||
label_clauses.append(
|
label_clauses.append(get_label_clause(label))
|
||||||
(ReviewSegment.data["objects"].cast("text") % f'*"{label}"*')
|
|
||||||
| (ReviewSegment.data["audio"].cast("text") % f'*"{label}"*')
|
|
||||||
)
|
|
||||||
clauses.append(reduce(operator.or_, label_clauses))
|
clauses.append(reduce(operator.or_, label_clauses))
|
||||||
|
|
||||||
if zones != "all":
|
if zones != "all":
|
||||||
@@ -236,10 +249,7 @@ async def review_summary(
|
|||||||
filtered_labels = labels.split(",")
|
filtered_labels = labels.split(",")
|
||||||
|
|
||||||
for label in filtered_labels:
|
for label in filtered_labels:
|
||||||
label_clauses.append(
|
label_clauses.append(get_label_clause(label))
|
||||||
(ReviewSegment.data["objects"].cast("text") % f'*"{label}"*')
|
|
||||||
| (ReviewSegment.data["audio"].cast("text") % f'*"{label}"*')
|
|
||||||
)
|
|
||||||
clauses.append(reduce(operator.or_, label_clauses))
|
clauses.append(reduce(operator.or_, label_clauses))
|
||||||
if zones != "all":
|
if zones != "all":
|
||||||
# use matching so segments with multiple zones
|
# use matching so segments with multiple zones
|
||||||
@@ -337,9 +347,8 @@ async def review_summary(
|
|||||||
filtered_labels = labels.split(",")
|
filtered_labels = labels.split(",")
|
||||||
|
|
||||||
for label in filtered_labels:
|
for label in filtered_labels:
|
||||||
label_clauses.append(
|
label_clauses.append(get_label_clause(label, include_audio=False))
|
||||||
ReviewSegment.data["objects"].cast("text") % f'*"{label}"*'
|
|
||||||
)
|
|
||||||
clauses.append(reduce(operator.or_, label_clauses))
|
clauses.append(reduce(operator.or_, label_clauses))
|
||||||
|
|
||||||
# Find the time range of available data
|
# Find the time range of available data
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ PRESETS_HW_ACCEL_SCALE = {
|
|||||||
"preset-rpi-64-h264": "-r {0} -vf fps={0},scale={1}:{2}",
|
"preset-rpi-64-h264": "-r {0} -vf fps={0},scale={1}:{2}",
|
||||||
"preset-rpi-64-h265": "-r {0} -vf fps={0},scale={1}:{2}",
|
"preset-rpi-64-h265": "-r {0} -vf fps={0},scale={1}:{2}",
|
||||||
FFMPEG_HWACCEL_VAAPI: "-r {0} -vf fps={0},scale_vaapi=w={1}:h={2},hwdownload,format=nv12",
|
FFMPEG_HWACCEL_VAAPI: "-r {0} -vf fps={0},scale_vaapi=w={1}:h={2},hwdownload,format=nv12",
|
||||||
"preset-intel-qsv-h264": "-r {0} -vf vpp_qsv=w={1}:h={2}:format=nv12,hwdownload,format=nv12,fps={0},format=yuv420p",
|
"preset-intel-qsv-h264": "-r {0} -vf fps={0},vpp_qsv=w={1}:h={2}:format=nv12,hwdownload,format=nv12,format=yuv420p",
|
||||||
"preset-intel-qsv-h265": "-r {0} -vf vpp_qsv=w={1}:h={2}:format=nv12,hwdownload,format=nv12,fps={0},format=yuv420p",
|
"preset-intel-qsv-h265": "-r {0} -vf fps={0},vpp_qsv=w={1}:h={2}:format=nv12,hwdownload,format=nv12,format=yuv420p",
|
||||||
FFMPEG_HWACCEL_NVIDIA: "-r {0} -vf fps={0},scale_cuda=w={1}:h={2},hwdownload,format=nv12",
|
FFMPEG_HWACCEL_NVIDIA: "-r {0} -vf fps={0},scale_cuda=w={1}:h={2},hwdownload,format=nv12",
|
||||||
"preset-jetson-h264": "-r {0}", # scaled in decoder
|
"preset-jetson-h264": "-r {0}", # scaled in decoder
|
||||||
"preset-jetson-h265": "-r {0}", # scaled in decoder
|
"preset-jetson-h265": "-r {0}", # scaled in decoder
|
||||||
|
|||||||
@@ -168,6 +168,29 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
assert events[0]["id"] == id
|
assert events[0]["id"] == id
|
||||||
assert events[1]["id"] == id2
|
assert events[1]["id"] == id2
|
||||||
|
|
||||||
|
def test_get_event_list_offset_pages_score_sort(self):
|
||||||
|
now = datetime.now().timestamp()
|
||||||
|
scores = [0.6, 0.9, 0.7, 0.95, 0.8]
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
for i, score in enumerate(scores):
|
||||||
|
super().insert_mock_event(
|
||||||
|
f"event-{i}", start_time=now + i, data={"score": score}
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {"sort": "score_desc"}
|
||||||
|
full = [e["id"] for e in client.get("/events", params=params).json()]
|
||||||
|
paged = [
|
||||||
|
e["id"]
|
||||||
|
for offset in (0, 2, 4)
|
||||||
|
for e in client.get(
|
||||||
|
"/events", params={**params, "limit": 2, "offset": offset}
|
||||||
|
).json()
|
||||||
|
]
|
||||||
|
|
||||||
|
assert full == ["event-3", "event-1", "event-4", "event-2", "event-0"]
|
||||||
|
assert paged == full
|
||||||
|
|
||||||
def test_get_event_list_match_multilingual_attribute(self):
|
def test_get_event_list_match_multilingual_attribute(self):
|
||||||
event_id = "123456.zh"
|
event_id = "123456.zh"
|
||||||
attribute = "中文标签"
|
attribute = "中文标签"
|
||||||
@@ -219,6 +242,85 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
assert events[0]["id"] == event_id
|
assert events[0]["id"] == event_id
|
||||||
|
|
||||||
|
def test_events_search_offset_pages_score_sort(self):
|
||||||
|
now = datetime.now().timestamp()
|
||||||
|
scores = [0.6, 0.9, 0.7, 0.95, 0.8]
|
||||||
|
ids = [f"event-{i}" for i in range(len(scores))]
|
||||||
|
mock_embeddings = Mock()
|
||||||
|
mock_embeddings.search_thumbnail.return_value = [
|
||||||
|
(event_id, 0.1 * i) for i, event_id in enumerate(ids)
|
||||||
|
]
|
||||||
|
|
||||||
|
self.app.frigate_config.semantic_search.enabled = True
|
||||||
|
self.app.embeddings = mock_embeddings
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
for i, score in enumerate(scores):
|
||||||
|
super().insert_mock_event(
|
||||||
|
ids[i], start_time=now + i, data={"score": score}
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"search_type": "similarity",
|
||||||
|
"event_id": ids[0],
|
||||||
|
"sort": "score_desc",
|
||||||
|
}
|
||||||
|
paged = [
|
||||||
|
e["id"]
|
||||||
|
for offset in (0, 2, 4)
|
||||||
|
for e in client.get(
|
||||||
|
"/events/search",
|
||||||
|
params={**params, "limit": 2, "offset": offset},
|
||||||
|
).json()
|
||||||
|
]
|
||||||
|
|
||||||
|
assert paged == ["event-3", "event-1", "event-4", "event-2", "event-0"]
|
||||||
|
|
||||||
|
def test_events_search_offset_pages_orders_ties_by_id(self):
|
||||||
|
now = datetime.now().timestamp()
|
||||||
|
ids = ["event-c", "event-a", "event-b"]
|
||||||
|
mock_embeddings = Mock()
|
||||||
|
mock_embeddings.search_thumbnail.return_value = [
|
||||||
|
(event_id, 0.1) for event_id in ids
|
||||||
|
]
|
||||||
|
|
||||||
|
self.app.frigate_config.semantic_search.enabled = True
|
||||||
|
self.app.embeddings = mock_embeddings
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
for i, event_id in enumerate(ids):
|
||||||
|
super().insert_mock_event(
|
||||||
|
event_id, start_time=now + i, data={"score": 0.8}
|
||||||
|
)
|
||||||
|
|
||||||
|
for sort in ("score_desc", "relevance"):
|
||||||
|
params = {
|
||||||
|
"search_type": "similarity",
|
||||||
|
"event_id": ids[0],
|
||||||
|
"sort": sort,
|
||||||
|
}
|
||||||
|
paged = [
|
||||||
|
e["id"]
|
||||||
|
for offset in (0, 1, 2)
|
||||||
|
for e in client.get(
|
||||||
|
"/events/search",
|
||||||
|
params={**params, "limit": 1, "offset": offset},
|
||||||
|
).json()
|
||||||
|
]
|
||||||
|
|
||||||
|
assert paged == ["event-a", "event-b", "event-c"]
|
||||||
|
|
||||||
|
def test_event_list_rejects_negative_offset(self):
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
response = client.get("/events", params={"offset": -5})
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/events/search",
|
||||||
|
params={"query": "car", "offset": -5},
|
||||||
|
)
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
def test_similarity_search_hides_unauthorized_anchor_event(self):
|
def test_similarity_search_hides_unauthorized_anchor_event(self):
|
||||||
mock_embeddings = Mock()
|
mock_embeddings = Mock()
|
||||||
self.app.frigate_config.semantic_search.enabled = True
|
self.app.frigate_config.semantic_search.enabled = True
|
||||||
|
|||||||
@@ -240,9 +240,101 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
assert len(response_json) == 1
|
assert len(response_json) == 1
|
||||||
assert response_json[0]["id"] == id_reviewed
|
assert response_json[0]["id"] == id_reviewed
|
||||||
|
|
||||||
|
def test_get_review_with_label_filter_matches_verified(self):
|
||||||
|
"""Test that a label filter also matches the `-verified` variant."""
|
||||||
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
"123456.person", now, now + 2, data={"objects": ["person"]}
|
||||||
|
)
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
"123456.verified", now, now + 2, data={"objects": ["person-verified"]}
|
||||||
|
)
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
"123456.car", now, now + 2, data={"objects": ["car"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"labels": "person",
|
||||||
|
"after": now - 1,
|
||||||
|
"before": now + 3,
|
||||||
|
}
|
||||||
|
response = client.get("/review", params=params)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
assert {r["id"] for r in response_json} == {
|
||||||
|
"123456.person",
|
||||||
|
"123456.verified",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_get_review_with_label_filter_does_not_match_prefix(self):
|
||||||
|
"""Test that a label filter does not match labels that only share a prefix."""
|
||||||
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
"123456.carrot", now, now + 2, data={"objects": ["carrot"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"labels": "car",
|
||||||
|
"after": now - 1,
|
||||||
|
"before": now + 3,
|
||||||
|
}
|
||||||
|
response = client.get("/review", params=params)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 0
|
||||||
|
|
||||||
|
def test_get_review_with_audio_label_filter(self):
|
||||||
|
"""Test that a label filter still matches audio labels."""
|
||||||
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
"123456.audio", now, now + 2, data={"audio": ["speech"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"labels": "speech",
|
||||||
|
"after": now - 1,
|
||||||
|
"before": now + 3,
|
||||||
|
}
|
||||||
|
response = client.get("/review", params=params)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
assert len(response_json) == 1
|
||||||
|
assert response_json[0]["id"] == "123456.audio"
|
||||||
|
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
################################### GET /review/summary Endpoint #################################################
|
################################### GET /review/summary Endpoint #################################################
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
|
def test_get_review_summary_label_filter_matches_verified(self):
|
||||||
|
"""Test that the summary label filter also matches the `-verified` variant."""
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
"123456.verified", data={"objects": ["person-verified"]}
|
||||||
|
)
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
"123456.car", data={"objects": ["car"]}, severity=SeverityEnum.detection
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"cameras": "front_door",
|
||||||
|
"labels": "person",
|
||||||
|
"zones": "all",
|
||||||
|
"timezone": "utc",
|
||||||
|
}
|
||||||
|
response = client.get("/review/summary", params=params)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
assert response_json["last24Hours"]["total_alert"] == 1
|
||||||
|
assert response_json["last24Hours"]["total_detection"] == 0
|
||||||
|
|
||||||
|
today_formatted = datetime.today().strftime("%Y-%m-%d")
|
||||||
|
assert response_json[today_formatted]["total_alert"] == 1
|
||||||
|
assert response_json[today_formatted]["total_detection"] == 0
|
||||||
|
|
||||||
def test_get_review_summary_all_filters(self):
|
def test_get_review_summary_all_filters(self):
|
||||||
with AuthTestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
super().insert_mock_review_segment("123456.random")
|
super().insert_mock_review_segment("123456.random")
|
||||||
|
|||||||
@@ -198,7 +198,19 @@ export default function Explore() {
|
|||||||
|
|
||||||
const [url, params] = searchQuery;
|
const [url, params] = searchQuery;
|
||||||
|
|
||||||
const isAscending = params.sort?.includes("date_asc");
|
// a start_time cursor only works when rows are ordered by start_time,
|
||||||
|
// so every other sort pages by offset
|
||||||
|
const isDateSort =
|
||||||
|
params.sort === "date_asc" ||
|
||||||
|
params.sort === "date_desc" ||
|
||||||
|
(!params.sort && url === "events");
|
||||||
|
|
||||||
|
if (pageIndex > 0 && !isDateSort) {
|
||||||
|
return [
|
||||||
|
url,
|
||||||
|
{ ...params, offset: pageIndex * API_LIMIT, limit: API_LIMIT },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
if (pageIndex > 0 && previousPageData) {
|
if (pageIndex > 0 && previousPageData) {
|
||||||
const lastDate = previousPageData[previousPageData.length - 1].start_time;
|
const lastDate = previousPageData[previousPageData.length - 1].start_time;
|
||||||
@@ -206,7 +218,8 @@ export default function Explore() {
|
|||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
...params,
|
...params,
|
||||||
[isAscending ? "after" : "before"]: lastDate.toString(),
|
[params.sort === "date_asc" ? "after" : "before"]:
|
||||||
|
lastDate.toString(),
|
||||||
limit: API_LIMIT,
|
limit: API_LIMIT,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -238,10 +251,17 @@ export default function Explore() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchResults = useMemo(
|
// offset pages can overlap when results shift between page fetches
|
||||||
() => (data ? ([] as SearchResult[]).concat(...data) : []),
|
const searchResults = useMemo(() => {
|
||||||
[data],
|
if (!data) return [];
|
||||||
);
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return data.flat().filter((result) => {
|
||||||
|
if (seen.has(result.id)) return false;
|
||||||
|
seen.add(result.id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [data]);
|
||||||
const isLoadingInitialData = !data && !isValidating;
|
const isLoadingInitialData = !data && !isValidating;
|
||||||
const isLoadingMore =
|
const isLoadingMore =
|
||||||
isLoadingInitialData ||
|
isLoadingInitialData ||
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export type SearchQueryParams = {
|
|||||||
max_speed?: number;
|
max_speed?: number;
|
||||||
search_type?: string;
|
search_type?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
in_progress?: number;
|
in_progress?: number;
|
||||||
include_thumbnails?: number;
|
include_thumbnails?: number;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user