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
@@ -0,0 +1,119 @@
import { describe, expect, it, vi } from 'vitest';
import { findBestMediaTimeIndex } from '../../src/utils/find-best-media-time-index';
import { ViewFolder } from '../../src/view/item';
import { TestViewMedia } from '../test-utils';
describe('findBestMediaTimeIndex', () => {
it('should handle non-media items', () => {
const folder = new ViewFolder({ id: 'folder', type: 'ha', title: 'folder' }, []);
const media = new TestViewMedia({
startTime: new Date('2024-01-01T10:00:00'),
endTime: new Date('2024-01-01T11:00:00'),
});
const index = findBestMediaTimeIndex(
[folder, media],
new Date('2024-01-01T10:30:00'),
);
expect(index).toBe(1);
});
it('should find longest match', () => {
const media1 = new TestViewMedia({
startTime: new Date('2024-01-01T10:00:00'),
endTime: new Date('2024-01-01T10:30:00'),
});
const media2 = new TestViewMedia({
startTime: new Date('2024-01-01T10:00:00'),
endTime: new Date('2024-01-01T11:00:00'),
});
const index = findBestMediaTimeIndex(
[media1, media2],
new Date('2024-01-01T10:15:00'),
);
expect(index).toBe(1);
});
it('should favor specified camera', () => {
const media1 = new TestViewMedia({
cameraID: 'camera1',
startTime: new Date('2024-01-01T10:00:00'),
endTime: new Date('2024-01-01T11:00:00'),
});
const media2 = new TestViewMedia({
cameraID: 'camera2',
startTime: new Date('2024-01-01T10:00:00'),
endTime: new Date('2024-01-01T10:30:00'),
});
// Even though media1 is longer, favor media2's camera.
const index = findBestMediaTimeIndex(
[media1, media2],
new Date('2024-01-01T10:15:00'),
'camera2',
);
expect(index).toBe(1);
});
it('should handle multiple matches from non-favored cameras', () => {
const media1 = new TestViewMedia({
cameraID: 'camera1',
startTime: new Date('2024-01-01T10:00:00'),
endTime: new Date('2024-01-01T10:30:00'),
});
const media2 = new TestViewMedia({
cameraID: 'camera3',
startTime: new Date('2024-01-01T10:00:00'),
endTime: new Date('2024-01-01T11:00:00'),
});
const index = findBestMediaTimeIndex(
[media1, media2],
new Date('2024-01-01T10:15:00'),
'camera2', // Favored camera not present
);
expect(index).toBe(1); // Longest match wins
});
it('should handle missing start or end time', () => {
const media = new TestViewMedia({
startTime: null,
endTime: null,
});
vi.spyOn(media, 'includesTime').mockReturnValue(true);
const index = findBestMediaTimeIndex([media], new Date());
expect(index).toBeNull();
});
it('should return null when no match', () => {
const media = new TestViewMedia({
startTime: new Date('2024-01-01T10:00:00'),
endTime: new Date('2024-01-01T11:00:00'),
});
const index = findBestMediaTimeIndex([media], new Date('2024-01-01T12:00:00'));
expect(index).toBeNull();
});
it('should reject longer match if favored camera already matched', () => {
const media1 = new TestViewMedia({
cameraID: 'favored',
startTime: new Date('2024-01-01T10:00:00'),
endTime: new Date('2024-01-01T10:30:00'),
});
const media2 = new TestViewMedia({
cameraID: 'not-favored',
startTime: new Date('2024-01-01T10:00:00'),
endTime: new Date('2024-01-01T11:00:00'),
});
// media2 is longer, but media1 is favored and already matches.
const index = findBestMediaTimeIndex(
[media1, media2],
new Date('2024-01-01T10:15:00'),
'favored',
);
expect(index).toBe(0);
});
});