From a3b37f79fa64c786005ecd74c9ef3144416c8686 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 12 Mar 2025 12:01:51 -0500 Subject: [PATCH] api for search queries --- frigate/api/app.py | 33 +++++++++ .../api/defs/query/events_query_parameters.py | 2 + frigate/api/event.py | 70 +++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/frigate/api/app.py b/frigate/api/app.py index 5ce90130f..d9a57a3c1 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -619,6 +619,39 @@ def get_sub_labels(split_joined: Optional[int] = None): return JSONResponse(content=sub_labels) +@router.get("/identifiers") +def get_identifiers(split_joined: Optional[int] = None): + try: + events = Event.select(Event.data).distinct() + except Exception: + return JSONResponse( + content=({"success": False, "message": "Failed to get identifiers"}), + status_code=404, + ) + + identifiers = [] + for e in events: + if e.data is not None and "identifier" in e.data: + identifiers.append(e.data["identifier"]) + + while None in identifiers: + identifiers.remove(None) + + if split_joined: + original_identifiers = identifiers.copy() + for identifier in original_identifiers: + if identifier and "," in identifier: + identifiers.remove(identifier) + parts = identifier.split(",") + for part in parts: + if part.strip() not in identifiers: + identifiers.append(part.strip()) + + identifiers = list(set(identifiers)) + identifiers.sort() + return JSONResponse(content=identifiers) + + @router.get("/timeline") def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None): clauses = [] diff --git a/frigate/api/defs/query/events_query_parameters.py b/frigate/api/defs/query/events_query_parameters.py index 01c79abb0..9f73d8583 100644 --- a/frigate/api/defs/query/events_query_parameters.py +++ b/frigate/api/defs/query/events_query_parameters.py @@ -27,6 +27,7 @@ class EventsQueryParams(BaseModel): max_score: Optional[float] = None min_speed: Optional[float] = None max_speed: Optional[float] = None + identifier: Optional[str] = "all" is_submitted: Optional[int] = None min_length: Optional[float] = None max_length: Optional[float] = None @@ -55,6 +56,7 @@ class EventsSearchQueryParams(BaseModel): max_score: Optional[float] = None min_speed: Optional[float] = None max_speed: Optional[float] = None + identifier: Optional[str] = "all" sort: Optional[str] = None diff --git a/frigate/api/event.py b/frigate/api/event.py index af2972b09..f6f497061 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -101,6 +101,7 @@ def events(params: EventsQueryParams = Depends()): min_length = params.min_length max_length = params.max_length event_id = params.event_id + identifier = params.identifier sort = params.sort @@ -158,6 +159,39 @@ def events(params: EventsQueryParams = Depends()): sub_label_clause = reduce(operator.or_, sub_label_clauses) clauses.append((sub_label_clause)) + if identifier != "all": + # use matching so joined identifiers are included + # for example an identifier 'ABC123' would get events + # with identifiers 'ABC123' and 'ABC123, XYZ789' + # also supports regex with slashes before and after the pattern + identifier_clauses = [] + filtered_identifiers = identifier.split(",") + + if "None" in filtered_identifiers: + filtered_identifiers.remove("None") + identifier_clauses.append((Event.data["identifier"].is_null())) + + for identifier in filtered_identifiers: + if identifier.startswith("r:"): # Regex pattern + pattern = identifier[2:] # Strip the "r:" prefix + identifier_clauses.append( + (Event.data["identifier"].cast("text").regexp(pattern)) + ) + print(pattern) + else: # Regular exact matching plus list inclusion + identifier_clauses.append( + (Event.data["identifier"].cast("text") == identifier) + ) + identifier_clauses.append( + (Event.data["identifier"].cast("text") % f"*{identifier},*") + ) + identifier_clauses.append( + (Event.data["identifier"].cast("text") % f"*, {identifier}*") + ) + + identifier_clause = reduce(operator.or_, identifier_clauses) + clauses.append((identifier_clause)) + if zones != "all": # use matching so events with multiple zones # still match on a search where any zone matches @@ -399,6 +433,7 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) has_clip = params.has_clip has_snapshot = params.has_snapshot is_submitted = params.is_submitted + identifier = params.identifier # for similarity search event_id = params.event_id @@ -468,6 +503,39 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) event_filters.append((reduce(operator.or_, zone_clauses))) + if identifier != "all": + # use matching so joined identifiers are included + # for example an identifier 'ABC123' would get events + # with identifiers 'ABC123' and 'ABC123, XYZ789' + # also supports regex with slashes before and after the pattern + identifier_clauses = [] + filtered_identifiers = identifier.split(",") + + if "None" in filtered_identifiers: + filtered_identifiers.remove("None") + identifier_clauses.append((Event.data["identifier"].is_null())) + + for identifier in filtered_identifiers: + if identifier.startswith("r:"): # Regex pattern + pattern = identifier[2:] # Strip the "r:" prefix + identifier_clauses.append( + (Event.data["identifier"].cast("text").regexp(pattern)) + ) + print(pattern) + else: # Regular exact matching plus list inclusion + identifier_clauses.append( + (Event.data["identifier"].cast("text") == identifier) + ) + identifier_clauses.append( + (Event.data["identifier"].cast("text") % f"*{identifier},*") + ) + identifier_clauses.append( + (Event.data["identifier"].cast("text") % f"*, {identifier}*") + ) + + identifier_clause = reduce(operator.or_, identifier_clauses) + event_filters.append((identifier_clause)) + if after: event_filters.append((Event.start_time > after)) @@ -685,6 +753,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()): Event.camera, Event.label, Event.sub_label, + Event.data, fn.strftime( "%Y-%m-%d", fn.datetime( @@ -699,6 +768,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()): Event.camera, Event.label, Event.sub_label, + Event.data, (Event.start_time + seconds_offset).cast("int") / (3600 * 24), Event.zones, )