feat: Add support for Frigate reviews / detections [initial PR] (#2315)

- Add support for Frigate reviews / detections.
 - Add support for GenAI metadata.
- Significant internal refactor to more flexible "UnifiedQuery" to allow
mixing cameras with simple metadata and review metadata (e.g. a timeline
view of a Frigate camera with reviews, and a Reolink camera with simple
metadata).
 - Add support for folder media as camera media.

There are a few more PRs to commit prior to this going live, but
commiting this for now due to the scale of the change.

BREAKING CHANGE: `media_type` and `events_type` are retired under
`live`, `viewer` and `timeline` configuration sections, instead media
type is associated (once) with the camera under `media`.
This commit is contained in:
Dermot Duffy
2026-01-19 13:32:50 -08:00
committed by GitHub
parent 6eb0a87ca8
commit fc32727860
215 changed files with 14491 additions and 6160 deletions
+54 -1
View File
@@ -1,9 +1,13 @@
import { clone } from 'lodash-es';
import { FolderPathComponent } from '../card-controller/folders/types';
import { FolderConfig } from '../config/schema/folders';
import { Severity } from '../types';
export enum ViewMediaType {
Clip = 'clip',
Snapshot = 'snapshot',
Recording = 'recording',
Review = 'review',
}
export enum VideoContentType {
@@ -14,6 +18,7 @@ export enum VideoContentType {
export interface ViewMediaSourceOptions {
cameraID?: string;
folder?: FolderConfig;
path?: readonly FolderPathComponent[];
}
export class ViewMedia {
@@ -59,6 +64,9 @@ export class ViewMedia {
public getTitle(): string | null {
return null;
}
public getDescription(): string | null {
return null;
}
public getThumbnail(): string | null {
return null;
}
@@ -68,6 +76,9 @@ export class ViewMedia {
public isFavorite(): boolean | null {
return null;
}
public isReviewed(): boolean | null {
return null;
}
public includesTime(seek: Date): boolean {
const startTime = this.getStartTime();
const endTime = this.getUsableEndTime();
@@ -80,9 +91,25 @@ export class ViewMedia {
public setFavorite(_favorite: boolean): void {
return;
}
public getSeverity(): Severity | null {
return null;
}
public getWhere(): string[] | null {
return null;
}
/**
* Creates a shallow clone of this ViewMedia instance.
*
* This is needed because Lit components use reference equality (===) to
* detect property changes. When we mutate an item's state (e.g.,
* setReviewed), the reference doesn't change, so Lit doesn't re-render. By
* cloning the item before mutation and replacing it in the QueryResults, we
* ensure Lit sees a new reference and updates the UI.
*/
public clone(): this {
return clone(this);
}
}
export interface EventViewMedia extends ViewMedia {
@@ -96,6 +123,12 @@ export interface RecordingViewMedia extends ViewMedia {
getEventCount(): number | null;
}
export interface ReviewViewMedia extends ViewMedia {
getWhat(): string[] | null;
isReviewed(): boolean | null;
setReviewed(reviewed: boolean): void;
}
interface ViewFolderParameters {
icon?: string | null;
id?: string | null;
@@ -105,14 +138,20 @@ interface ViewFolderParameters {
export class ViewFolder {
private _folder: FolderConfig;
private _path: readonly FolderPathComponent[];
private _icon: string | null;
private _id: string | null;
private _title: string | null;
private _thumbnail: string | null;
constructor(folder: FolderConfig, params?: ViewFolderParameters) {
constructor(
folder: FolderConfig,
path: readonly FolderPathComponent[],
params?: ViewFolderParameters,
) {
this._folder = folder;
this._path = path;
this._icon = params?.icon ?? null;
this._id = params?.id ?? null;
@@ -123,21 +162,35 @@ export class ViewFolder {
public getFolder(): FolderConfig {
return this._folder;
}
public getPath(): readonly FolderPathComponent[] {
return this._path;
}
public getID(): string | null {
return this._id;
}
public getTitle(): string | null {
return this._title;
}
public getDescription(): string | null {
return null;
}
public getThumbnail(): string | null {
return this._thumbnail;
}
public getIcon(): string | null {
return this._icon;
}
public getSeverity(): Severity | null {
return null;
}
public isFavorite(): boolean | null {
return null;
}
/** See ViewMedia.clone() for explanation. */
public clone(): this {
return clone(this);
}
}
export type ViewItem = ViewMedia | ViewFolder;