De-duplicate events at the same time.
This commit is contained in:
@@ -20,6 +20,8 @@ import { Entity } from '../../utils/ha/entity-registry/types';
|
|||||||
import { BrowseMediaManager } from '../../utils/ha/browse-media/browse-media-manager';
|
import { BrowseMediaManager } from '../../utils/ha/browse-media/browse-media-manager';
|
||||||
import {
|
import {
|
||||||
BROWSE_MEDIA_CACHE_SECONDS,
|
BROWSE_MEDIA_CACHE_SECONDS,
|
||||||
|
MEDIA_CLASS_IMAGE,
|
||||||
|
MEDIA_CLASS_VIDEO,
|
||||||
RichBrowseMedia,
|
RichBrowseMedia,
|
||||||
} from '../../utils/ha/browse-media/types';
|
} from '../../utils/ha/browse-media/types';
|
||||||
import { BrowseMediaMetadata } from './types';
|
import { BrowseMediaMetadata } from './types';
|
||||||
@@ -27,6 +29,89 @@ import { rangesOverlap } from '../range';
|
|||||||
import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media';
|
import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media';
|
||||||
import { canonicalizeHAURL } from '../../utils/ha';
|
import { canonicalizeHAURL } from '../../utils/ha';
|
||||||
import { RequestCache } from '../cache';
|
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.
|
* 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(
|
public async getMediaDownloadPath(
|
||||||
hass: ExtendedHomeAssistant,
|
hass: ExtendedHomeAssistant,
|
||||||
_cameraConfig: CameraConfig,
|
_cameraConfig: CameraConfig,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import format from 'date-fns/format';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import { formatDateAndTime } from '../../utils/basic';
|
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 {
|
import {
|
||||||
ViewMedia,
|
ViewMedia,
|
||||||
EventViewMedia,
|
EventViewMedia,
|
||||||
@@ -11,6 +12,7 @@ import { BrowseMediaMetadata } from '../browse-media/types';
|
|||||||
|
|
||||||
class BrowseMediaEventViewMedia extends ViewMedia implements EventViewMedia {
|
class BrowseMediaEventViewMedia extends ViewMedia implements EventViewMedia {
|
||||||
protected _browseMedia: RichBrowseMedia<BrowseMediaMetadata>;
|
protected _browseMedia: RichBrowseMedia<BrowseMediaMetadata>;
|
||||||
|
protected _id: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
mediaType: ViewMediaType,
|
mediaType: ViewMediaType,
|
||||||
@@ -19,11 +21,19 @@ class BrowseMediaEventViewMedia extends ViewMedia implements EventViewMedia {
|
|||||||
) {
|
) {
|
||||||
super(mediaType, cameraID);
|
super(mediaType, cameraID);
|
||||||
this._browseMedia = browseMedia;
|
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 {
|
public getStartTime(): Date | null {
|
||||||
return this._browseMedia._metadata?.startDate ?? null;
|
return this._browseMedia._metadata?.startDate ?? null;
|
||||||
}
|
}
|
||||||
@@ -34,7 +44,7 @@ class BrowseMediaEventViewMedia extends ViewMedia implements EventViewMedia {
|
|||||||
return VideoContentType.MP4;
|
return VideoContentType.MP4;
|
||||||
}
|
}
|
||||||
public getID(): string {
|
public getID(): string {
|
||||||
return this.getContentID();
|
return this._id;
|
||||||
}
|
}
|
||||||
public getContentID(): string {
|
public getContentID(): string {
|
||||||
return this._browseMedia.media_content_id;
|
return this._browseMedia.media_content_id;
|
||||||
|
|||||||
@@ -43,9 +43,6 @@ export class FrigateEventViewMedia extends ViewMedia implements EventViewMedia {
|
|||||||
this._subLabels = subLabels ?? null;
|
this._subLabels = subLabels ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasClip(): boolean {
|
|
||||||
return !!this._event.has_clip;
|
|
||||||
}
|
|
||||||
public getStartTime(): Date {
|
public getStartTime(): Date {
|
||||||
return fromUnixTime(this._event.start_time);
|
return fromUnixTime(this._event.start_time);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,12 @@ import { MotionEyeEventQueryResults } from './types';
|
|||||||
import orderBy from 'lodash-es/orderBy';
|
import orderBy from 'lodash-es/orderBy';
|
||||||
import startOfDay from 'date-fns/startOfDay';
|
import startOfDay from 'date-fns/startOfDay';
|
||||||
import add from 'date-fns/add';
|
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 { BrowseMediaMetadata } from '../browse-media/types';
|
||||||
import { BrowseMediaViewMediaFactory } from '../browse-media/media';
|
|
||||||
import motioneyeLogo from './assets/motioneye-logo.svg';
|
import motioneyeLogo from './assets/motioneye-logo.svg';
|
||||||
|
|
||||||
class MotionEyeQueryResultsClassifier {
|
class MotionEyeQueryResultsClassifier {
|
||||||
@@ -172,7 +175,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
|||||||
matcher: (media: RichBrowseMedia<BrowseMediaMetadata>) =>
|
matcher: (media: RichBrowseMedia<BrowseMediaMetadata>) =>
|
||||||
media.can_expand &&
|
media.can_expand &&
|
||||||
(!!dateFormat || media.title === next) &&
|
(!!dateFormat || media.title === next) &&
|
||||||
this._mediaIsWithinDates(media, matchOptions?.start, matchOptions?.end),
|
isMediaWithinDates(media, matchOptions?.start, matchOptions?.end),
|
||||||
advance: (media) => generateNextStep(parts, media),
|
advance: (media) => generateNextStep(parts, media),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -271,7 +274,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
|||||||
},
|
},
|
||||||
matcher: (media: RichBrowseMedia<BrowseMediaMetadata>) =>
|
matcher: (media: RichBrowseMedia<BrowseMediaMetadata>) =>
|
||||||
!media.can_expand &&
|
!media.can_expand &&
|
||||||
this._mediaIsWithinDates(media, perCameraQuery.start, perCameraQuery.end),
|
isMediaWithinDates(media, perCameraQuery.start, perCameraQuery.end),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{ useCache: engineOptions?.useCache },
|
{ useCache: engineOptions?.useCache },
|
||||||
@@ -313,34 +316,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
|||||||
if (!MotionEyeQueryResultsClassifier.isMotionEyeEventQueryResults(results)) {
|
if (!MotionEyeQueryResultsClassifier.isMotionEyeEventQueryResults(results)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return getViewMediaFromBrowseMediaArray(results.browseMedia);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMediaMetadata(
|
public async getMediaMetadata(
|
||||||
|
|||||||
@@ -621,10 +621,6 @@ export class FrigateCardViewerProvider
|
|||||||
// If this specific media item has no clip, then do nothing (even if all
|
// If this specific media item has no clip, then do nothing (even if all
|
||||||
// the other media items do).
|
// the other media items do).
|
||||||
!ViewMediaClassifier.isEvent(this.media) ||
|
!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)
|
!MediaQueriesClassifier.areEventQueries(this.view.query)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ export interface EventViewMedia extends ViewMedia {
|
|||||||
getWhat(): string[] | null;
|
getWhat(): string[] | null;
|
||||||
getTags(): string[] | null;
|
getTags(): string[] | null;
|
||||||
isGroupableWith(that: EventViewMedia): boolean;
|
isGroupableWith(that: EventViewMedia): boolean;
|
||||||
hasClip(): boolean | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecordingViewMedia extends ViewMedia {
|
export interface RecordingViewMedia extends ViewMedia {
|
||||||
|
|||||||
Reference in New Issue
Block a user