De-duplicate events at the same time.

This commit is contained in:
Dermot Duffy
2023-03-26 17:36:23 -07:00
parent 304c7e3164
commit d2ddc3e8f0
7 changed files with 109 additions and 86 deletions
@@ -20,6 +20,8 @@ import { Entity } from '../../utils/ha/entity-registry/types';
import { BrowseMediaManager } from '../../utils/ha/browse-media/browse-media-manager';
import {
BROWSE_MEDIA_CACHE_SECONDS,
MEDIA_CLASS_IMAGE,
MEDIA_CLASS_VIDEO,
RichBrowseMedia,
} from '../../utils/ha/browse-media/types';
import { BrowseMediaMetadata } from './types';
@@ -27,6 +29,89 @@ import { rangesOverlap } from '../range';
import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media';
import { canonicalizeHAURL } from '../../utils/ha';
import { RequestCache } from '../cache';
import { BrowseMediaViewMediaFactory } from './media';
/**
* A utility method to determine if a browse media object matches against a
* start and end date.
* @param media The browse media object (with rich metadata).
* @param start The optional start date.
* @param end The optional end date.
* @returns `true` if the media falls within the provided dates.
*/
export const isMediaWithinDates = (
media: RichBrowseMedia<BrowseMediaMetadata>,
start?: Date,
end?: Date,
): boolean => {
// If no date is specified at all, everything matches.
const dateReference = start ?? end;
if (!dateReference) {
return true;
}
// If there's no metadata, nothing matches.
if (!media._metadata) {
return false;
}
// Determine if:
// - The media starts within the query timeframe.
// - The media ends within the query timeframe.
// - The media entirely encompasses the query timeframe.
return rangesOverlap(
{
start: media._metadata.startDate,
end: media._metadata.endDate,
},
{
start: start ?? dateReference,
end: end ?? dateReference,
},
);
};
export const getViewMediaFromBrowseMediaArray = (
browseMedia: RichBrowseMedia<BrowseMediaMetadata>[],
): ViewMedia[] | null => {
const lookup: Map<string, ViewMedia> = new Map();
for (const browseMediaItem of browseMedia) {
const cameraID = browseMediaItem._metadata?.cameraID;
if (!cameraID) {
continue;
}
const mediaType =
browseMediaItem.media_class === MEDIA_CLASS_VIDEO
? 'clip'
: browseMediaItem.media_class === MEDIA_CLASS_IMAGE
? 'snapshot'
: null;
if (!mediaType) {
continue;
}
const media = BrowseMediaViewMediaFactory.createEventViewMedia(
mediaType,
browseMediaItem,
cameraID,
);
if (media) {
const id = media.getID();
const existing = lookup.get(id);
// De-duplicate events with precisely the same ID (same
// hour/minute/second) choosing clip > snapshot.
if (
!existing ||
(existing.getMediaType() === 'snapshot' && media.getMediaType() === 'clip')
) {
lookup.set(id, media);
}
}
}
return [...lookup.values()];
};
/**
* A base class for cameras that read events from HA BrowseMedia interface.
@@ -83,46 +168,6 @@ export class BrowseMediaCameraManagerEngine
];
}
/**
* A utility method to determine if a browse media object matches against a
* start and end date.
* @param media The browse media object (with rich metadata).
* @param start The optional start date.
* @param end The optional end date.
* @returns `true` if the media falls within the provided dates.
*/
protected _mediaIsWithinDates = (
media: RichBrowseMedia<BrowseMediaMetadata>,
start?: Date,
end?: Date,
): boolean => {
// If no date is specified at all, everything matches.
const dateReference = start ?? end;
if (!dateReference) {
return true;
}
// If there's no metadata, nothing matches.
if (!media._metadata) {
return false;
}
// Determine if:
// - The media starts within the query timeframe.
// - The media ends within the query timeframe.
// - The media entirely encompasses the query timeframe.
return rangesOverlap(
{
start: media._metadata.startDate,
end: media._metadata.endDate,
},
{
start: start ?? dateReference,
end: end ?? dateReference,
},
);
};
public async getMediaDownloadPath(
hass: ExtendedHomeAssistant,
_cameraConfig: CameraConfig,
+15 -5
View File
@@ -1,6 +1,7 @@
import format from 'date-fns/format';
import isEqual from 'lodash-es/isEqual';
import { formatDateAndTime } from '../../utils/basic';
import { MEDIA_CLASS_VIDEO, RichBrowseMedia } from '../../utils/ha/browse-media/types';
import { RichBrowseMedia } from '../../utils/ha/browse-media/types';
import {
ViewMedia,
EventViewMedia,
@@ -11,6 +12,7 @@ import { BrowseMediaMetadata } from '../browse-media/types';
class BrowseMediaEventViewMedia extends ViewMedia implements EventViewMedia {
protected _browseMedia: RichBrowseMedia<BrowseMediaMetadata>;
protected _id: string;
constructor(
mediaType: ViewMediaType,
@@ -19,11 +21,19 @@ class BrowseMediaEventViewMedia extends ViewMedia implements EventViewMedia {
) {
super(mediaType, cameraID);
this._browseMedia = browseMedia;
// Generate a custom ID that uses the start date (to allow multiple
// BrowseMedia objects (e.g. images and movies) to be de-duplicated).
if (browseMedia._metadata?.startDate) {
this._id = `${cameraID}/${format(
browseMedia._metadata.startDate,
'yyyy-MM-dd HH:mm:ss',
)}`;
} else {
this._id = browseMedia.media_content_id;
}
}
public hasClip(): boolean {
return this._browseMedia.media_class === MEDIA_CLASS_VIDEO;
}
public getStartTime(): Date | null {
return this._browseMedia._metadata?.startDate ?? null;
}
@@ -34,7 +44,7 @@ class BrowseMediaEventViewMedia extends ViewMedia implements EventViewMedia {
return VideoContentType.MP4;
}
public getID(): string {
return this.getContentID();
return this._id;
}
public getContentID(): string {
return this._browseMedia.media_content_id;
-3
View File
@@ -43,9 +43,6 @@ export class FrigateEventViewMedia extends ViewMedia implements EventViewMedia {
this._subLabels = subLabels ?? null;
}
public hasClip(): boolean {
return !!this._event.has_clip;
}
public getStartTime(): Date {
return fromUnixTime(this._event.start_time);
}
@@ -38,9 +38,12 @@ import { MotionEyeEventQueryResults } from './types';
import orderBy from 'lodash-es/orderBy';
import startOfDay from 'date-fns/startOfDay';
import add from 'date-fns/add';
import { BrowseMediaCameraManagerEngine } from '../browse-media/engine-browse-media';
import {
BrowseMediaCameraManagerEngine,
getViewMediaFromBrowseMediaArray,
isMediaWithinDates,
} from '../browse-media/engine-browse-media';
import { BrowseMediaMetadata } from '../browse-media/types';
import { BrowseMediaViewMediaFactory } from '../browse-media/media';
import motioneyeLogo from './assets/motioneye-logo.svg';
class MotionEyeQueryResultsClassifier {
@@ -172,7 +175,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
matcher: (media: RichBrowseMedia<BrowseMediaMetadata>) =>
media.can_expand &&
(!!dateFormat || media.title === next) &&
this._mediaIsWithinDates(media, matchOptions?.start, matchOptions?.end),
isMediaWithinDates(media, matchOptions?.start, matchOptions?.end),
advance: (media) => generateNextStep(parts, media),
},
];
@@ -271,7 +274,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
},
matcher: (media: RichBrowseMedia<BrowseMediaMetadata>) =>
!media.can_expand &&
this._mediaIsWithinDates(media, perCameraQuery.start, perCameraQuery.end),
isMediaWithinDates(media, perCameraQuery.start, perCameraQuery.end),
},
],
{ useCache: engineOptions?.useCache },
@@ -313,34 +316,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
if (!MotionEyeQueryResultsClassifier.isMotionEyeEventQueryResults(results)) {
return null;
}
const output: ViewMedia[] = [];
for (const browseMedia of results.browseMedia) {
const cameraID = browseMedia._metadata?.cameraID;
if (!cameraID) {
continue;
}
const mediaType =
browseMedia.media_class === MEDIA_CLASS_VIDEO
? 'clip'
: browseMedia.media_class === MEDIA_CLASS_IMAGE
? 'snapshot'
: null;
if (!mediaType) {
continue;
}
const media = BrowseMediaViewMediaFactory.createEventViewMedia(
mediaType,
browseMedia,
cameraID,
);
if (media) {
output.push(media);
}
}
return output;
return getViewMediaFromBrowseMediaArray(results.browseMedia);
}
public async getMediaMetadata(
-4
View File
@@ -621,10 +621,6 @@ export class FrigateCardViewerProvider
// If this specific media item has no clip, then do nothing (even if all
// the other media items do).
!ViewMediaClassifier.isEvent(this.media) ||
// If the event certainly has no clip, don't bother going further. If
// we're not sure for this camera type (i.e. hasClip() === null) the query
// will proceed anyway.
this.media.hasClip() === false ||
!MediaQueriesClassifier.areEventQueries(this.view.query)
) {
return;
-1
View File
@@ -71,7 +71,6 @@ export interface EventViewMedia extends ViewMedia {
getWhat(): string[] | null;
getTags(): string[] | null;
isGroupableWith(that: EventViewMedia): boolean;
hasClip(): boolean | null;
}
export interface RecordingViewMedia extends ViewMedia {