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
+110
View File
@@ -8,12 +8,17 @@ import {
getRecordingID,
getRecordingMediaContentID,
getRecordingTitle,
getReviewMediaContentID,
getReviewSeverity,
getReviewThumbnailURL,
getReviewTitle,
} from '../../../src/camera-manager/frigate/util';
import { CameraConfig } from '../../../src/config/schema/cameras';
import {
createCameraConfig,
createFrigateEvent,
createFrigateRecording,
createFrigateReview,
} from '../../test-utils';
describe('getEventTitle', () => {
@@ -152,3 +157,108 @@ describe('getRecordingID', () => {
).toBe('//1682776800000/1682780399000');
});
});
describe('getReviewTitle', () => {
const start = new Date('2023-05-06T10:43:00');
const end = new Date('2023-05-06T10:44:12');
afterEach(() => {
vi.useRealTimers();
});
it('should get finished review title without objects', () => {
expect(
getReviewTitle(
createFrigateReview({
start_time: start.getTime() / 1000,
end_time: end.getTime() / 1000,
data: {
objects: [],
zones: [],
},
}),
),
).toBe('2023-05-06 10:43 [72s]');
});
it('should get finished review title with objects', () => {
expect(
getReviewTitle(
createFrigateReview({
start_time: start.getTime() / 1000,
end_time: end.getTime() / 1000,
data: {
objects: ['person', 'dog'],
},
}),
),
).toBe('Person, Dog');
});
it('should get in-progress review title', () => {
vi.useFakeTimers();
vi.setSystemTime(add(start, { seconds: 60 }));
expect(
getReviewTitle(
createFrigateReview({
start_time: start.getTime() / 1000,
end_time: null,
data: {
objects: [],
},
}),
),
).toBe('2023-05-06 10:43 [60s]');
});
});
describe('getReviewMediaContentID', () => {
it('should get review content ID', () => {
expect(
getReviewMediaContentID(
'clientid',
'kitchen',
createFrigateReview({
start_time: new Date('2023-04-29T14:00:00').getTime() / 1000,
}),
),
).toBe('media-source://frigate/clientid/recordings/kitchen/2023-04-29/14');
});
});
describe('getReviewThumbnailURL', () => {
it('should get thumbnail URL', () => {
expect(
getReviewThumbnailURL(
'clientid',
createFrigateReview({
thumb_path: '/media/frigate/thumb.jpg',
}),
),
).toBe('/api/frigate/clientid/thumb.jpg');
});
it('should return null when no thumb path', () => {
expect(
getReviewThumbnailURL(
'clientid',
createFrigateReview({
thumb_path: null,
}),
),
).toBeNull();
});
});
describe('getReviewSeverity', () => {
it('should get alert severity', () => {
expect(getReviewSeverity('alert')).toBe('high');
});
it('should get detection severity', () => {
expect(getReviewSeverity('detection')).toBe('medium');
});
it('should get significant_motion severity', () => {
expect(getReviewSeverity('significant_motion')).toBe('low');
});
});