feat: Add cameras[].media.severities to filter review media by severity

Review queries were issued without a severity filter, so the thumbnail
bar, gallery and timeline showed every Frigate review item -- alerts and
detections alike -- regardless of what a camera is configured to alert
on. Severity could only be narrowed interactively, in the media filter
drawer, and that choice did not persist.

`media.severities` resolves into the default query the same way
`media.reviewed` does, and is overridden by an explicit severity filter
so the media filter drawer keeps working. Unset means all severities, so
existing behaviour is unchanged.

    cameras:
      - camera_entity: camera.office
        media:
          severities:
            - high
This commit is contained in:
flan
2026-09-04 17:56:20 +00:00
parent 54e261fd54
commit 88e1fba401
8 changed files with 171 additions and 9 deletions
+85
View File
@@ -700,6 +700,91 @@ describe('UnifiedQueryBuilder', () => {
expect(query.getNodes()[0]).not.toHaveProperty('reviewed');
});
it('should apply severities filter from config for reviews query', () => {
const { cameraManager, foldersManager, store } = createMocks();
store.getCameraIDs.mockReturnValue(new Set(['camera.office']));
store.getCameraConfig.mockReturnValue(
createCameraConfig({ media: { type: 'auto', severities: ['high'] } }),
);
cameraManager.getCameraCapabilities.mockReturnValue(
createCapabilities({ reviews: true }),
);
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildDefaultCameraQuery();
assert(query);
expect(query.getNodes()[0]).toMatchObject({
type: QueryType.Review,
severity: new Set(['high']),
});
});
it('should not apply severities filter when unset in config', () => {
const { cameraManager, foldersManager, store } = createMocks();
store.getCameraIDs.mockReturnValue(new Set(['camera.office']));
store.getCameraConfig.mockReturnValue(
createCameraConfig({ media: { type: 'auto' } }),
);
cameraManager.getCameraCapabilities.mockReturnValue(
createCapabilities({ reviews: true }),
);
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildDefaultCameraQuery();
assert(query);
assert(isReviewQuery(query.getNodes()[0]));
expect(query.getNodes()[0]).not.toHaveProperty('severity');
});
it('should not apply severities filter to non-review queries', () => {
const { cameraManager, foldersManager, store } = createMocks();
store.getCameraIDs.mockReturnValue(new Set(['camera.office']));
store.getCameraConfig.mockReturnValue(
createCameraConfig({ media: { type: 'auto', severities: ['high'] } }),
);
cameraManager.getCameraCapabilities.mockReturnValue(
createCapabilities({ clips: true }),
);
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildDefaultCameraQuery();
assert(query);
expect(query.getNodes()[0]).toMatchObject({
type: QueryType.Event,
hasClip: true,
});
expect(query.getNodes()[0]).not.toHaveProperty('severity');
});
it('should prefer an explicit severity option over the config', () => {
const { cameraManager, foldersManager, store } = createMocks();
store.getCameraIDs.mockReturnValue(new Set(['camera.office']));
store.getCameraConfig.mockReturnValue(
createCameraConfig({ media: { type: 'auto', severities: ['high'] } }),
);
cameraManager.getCameraCapabilities.mockReturnValue(
createCapabilities({ reviews: true }),
);
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildDefaultCameraQuery(undefined, {
severity: new Set(['medium'] as const),
});
assert(query);
expect(query.getNodes()[0]).toMatchObject({
type: QueryType.Review,
severity: new Set(['medium']),
});
});
it('should build clips query for camera with clips capability', () => {
const { cameraManager, foldersManager, store } = createMocks();
store.getCameraIDs.mockReturnValue(new Set(['camera.office']));
+18
View File
@@ -6,6 +6,7 @@ import type { UnifiedQuery } from '../../../src/view/unified-query';
import {
getReviewedQueryFilterFromConfig,
getReviewedQueryFilterFromQuery,
getSeverityQueryFilterFromConfig,
} from '../../../src/view/utils/query-filter';
import { createFolder } from '../../test-utils';
import { createEventQuery, TestViewMedia } from '../test-utils';
@@ -81,4 +82,21 @@ describe('query-filter', () => {
expect(getReviewedQueryFilterFromConfig('unreviewed')).toBe(false);
});
});
describe('getSeverityQueryFilterFromConfig', () => {
it('should return undefined when unset', () => {
expect(getSeverityQueryFilterFromConfig(undefined)).toBeUndefined();
});
it('should return undefined when empty', () => {
expect(getSeverityQueryFilterFromConfig([])).toBeUndefined();
});
it('should return a set of the configured severities', () => {
expect(getSeverityQueryFilterFromConfig(['high'])).toEqual(new Set(['high']));
expect(getSeverityQueryFilterFromConfig(['high', 'medium'])).toEqual(
new Set(['high', 'medium']),
);
});
});
});