diff --git a/src/camera/engine-factory.ts b/src/camera/engine-factory.ts index c249829e..7e367d6f 100644 --- a/src/camera/engine-factory.ts +++ b/src/camera/engine-factory.ts @@ -32,7 +32,11 @@ export class CameraManagerEngineFactory { return cameraConfig ? this.getEngineForCamera(cameraConfig) : null; } - public getEngineForCamera(cameraConfig: CameraConfig): CameraManagerEngine | null { + public getEngineForCamera(cameraConfig?: CameraConfig): CameraManagerEngine | null { + if (!cameraConfig) { + return null; + } + let engineKey: string | null = null; if (cameraConfig.frigate.camera_name) { engineKey = 'frigate'; diff --git a/src/camera/engine.ts b/src/camera/engine.ts index a4f9c554..21513008 100644 --- a/src/camera/engine.ts +++ b/src/camera/engine.ts @@ -53,11 +53,13 @@ export interface CameraManagerEngine { ): Promise | null>; generateMediaFromEvents( + cameraConfig: CameraConfig, query: EventQuery, results: QueryReturnType, ): ViewMedia[] | null; generateMediaFromRecordings( + cameraConfig: CameraConfig, query: RecordingQuery, results: QueryReturnType, ): ViewMedia[] | null; diff --git a/src/camera/frigate/engine-frigate.ts b/src/camera/frigate/engine-frigate.ts index 0d23839f..07ddcd2b 100644 --- a/src/camera/frigate/engine-frigate.ts +++ b/src/camera/frigate/engine-frigate.ts @@ -161,12 +161,11 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine { media: ViewMedia, favorite: boolean, ): Promise { - const clientID = cameraConfig.frigate.client_id; if (!FrigateViewMediaClassifier.isFrigateEvent(media)) { return; } - await retainEvent(hass, clientID, media.getID(cameraConfig), favorite); + await retainEvent(hass, cameraConfig.frigate.client_id, media.getID(), favorite); media.setFavorite(favorite); } @@ -309,6 +308,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine { } public generateMediaFromEvents( + cameraConfig: CameraConfig, query: EventQuery, results: QueryReturnType, ): ViewMedia[] | null { @@ -336,6 +336,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine { const media = FrigateViewMediaFactory.createEventViewMedia( mediaType, query.cameraID, + cameraConfig, event, ); if (media) { @@ -346,6 +347,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine { } public generateMediaFromRecordings( + cameraConfig: CameraConfig, query: RecordingQuery, results: QueryReturnType, ): ViewMedia[] | null { @@ -358,6 +360,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine { const media = FrigateViewMediaFactory.createRecordingViewMedia( query.cameraID, recording, + cameraConfig, ); if (media) { output.push(media); diff --git a/src/camera/frigate/media.ts b/src/camera/frigate/media.ts index 89965b8e..46fd38ad 100644 --- a/src/camera/frigate/media.ts +++ b/src/camera/frigate/media.ts @@ -12,16 +12,27 @@ import { getEventMediaContentID, getEventThumbnailURL, getEventTitle, + getRecordingID, getRecordingMediaContentID, getRecordingTitle, } from './util'; export class FrigateEventViewMedia extends ViewMedia implements EventViewMedia { protected _event: FrigateEvent; + protected _contentID: string; + protected _thumbnail: string; - constructor(mediaType: ViewMediaType, cameraID: string, event: FrigateEvent) { + constructor( + mediaType: ViewMediaType, + cameraID: string, + event: FrigateEvent, + contentID: string, + thumbnail: string, + ) { super(mediaType, cameraID); this._event = event; + this._contentID = contentID; + this._thumbnail = thumbnail; } public hasClip(): boolean { @@ -33,36 +44,17 @@ export class FrigateEventViewMedia extends ViewMedia implements EventViewMedia { public getEndTime(): Date | null { return this._event.end_time ? fromUnixTime(this._event.end_time) : null; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public getID(_cameraConfig?: CameraConfig): string { + public getID(): string { return this._event.id; } - public getContentID(cameraConfig?: CameraConfig): string | null { - if ( - !cameraConfig || - !cameraConfig.frigate.client_id || - !cameraConfig.frigate.camera_name - ) { - return null; - } - return getEventMediaContentID( - cameraConfig.frigate.client_id, - cameraConfig.frigate.camera_name, - this._event, - this._mediaType === 'clip' ? 'clips' : 'snapshots', - ); + public getContentID(): string { + return this._contentID; } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public getTitle(_cameraConfig?: CameraConfig): string | null { + public getTitle(): string | null { return getEventTitle(this._event); } - - public getThumbnail(cameraConfig?: CameraConfig): string | null { - if (cameraConfig?.frigate.client_id) { - return getEventThumbnailURL(cameraConfig.frigate.client_id, this._event); - } - return null; + public getThumbnail(): string | null { + return this._thumbnail; } public isFavorite(): boolean | null { return this._event.retain_indefinitely ?? null; @@ -89,37 +81,31 @@ export class FrigateEventViewMedia extends ViewMedia implements EventViewMedia { isEqual(this.getWhat(), that.getWhat()) ); } - - public getClipEquivalent(): EventViewMedia | null { - if (!this.hasClip()) { - return null; - } - return FrigateViewMediaFactory.createEventViewMedia( - 'clip', - this._cameraID, - this._event, - ); - } } export class FrigateRecordingViewMedia extends ViewMedia implements RecordingViewMedia { protected _recording: FrigateRecording; + protected _id: string; + protected _contentID: string; + protected _title: string; - constructor(mediaType: ViewMediaType, cameraID: string, recording: FrigateRecording) { + constructor( + mediaType: ViewMediaType, + cameraID: string, + recording: FrigateRecording, + id: string, + contentID: string, + title: string, + ) { super(mediaType, cameraID); this._recording = recording; + this._id = id; + this._contentID = contentID; + this._title = title; } - public getID(cameraConfig?: CameraConfig): string | null { - // ID name is derived from the real camera name (not CameraID) since the - // recordings for the same camera across multiple zones will be the same and - // can be dedup'd from this id. - if (cameraConfig) { - return `${cameraConfig.frigate?.client_id ?? ''}/${ - cameraConfig.frigate.camera_name ?? '' - }/${this._recording.startTime.getTime()}/${this._recording.endTime.getTime()}}`; - } - return null; + public getID(): string { + return this._id; } public getStartTime(): Date { return this._recording.startTime; @@ -127,26 +113,11 @@ export class FrigateRecordingViewMedia extends ViewMedia implements RecordingVie public getEndTime(): Date { return this._recording.endTime; } - public getContentID(cameraConfig?: CameraConfig): string | null { - if ( - !cameraConfig || - !cameraConfig.frigate.client_id || - !cameraConfig.frigate.camera_name - ) { - return null; - } - return getRecordingMediaContentID( - cameraConfig.frigate.client_id, - cameraConfig.frigate.camera_name, - this._recording, - ); + public getContentID(): string | null { + return this._contentID; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public getTitle(cameraConfig?: CameraConfig): string | null { - if (!cameraConfig) { - return null; - } - return getRecordingTitle(cameraConfig, this._recording); + public getTitle(): string | null { + return this._title; } public getEventCount(): number { return this._recording.events; @@ -155,23 +126,54 @@ export class FrigateRecordingViewMedia extends ViewMedia implements RecordingVie export class FrigateViewMediaFactory { static createEventViewMedia( - type: 'clip' | 'snapshot', + mediaType: 'clip' | 'snapshot', cameraID: string, + cameraConfig: CameraConfig, event: FrigateEvent, ): FrigateEventViewMedia | null { if ( - (type === 'clip' && event.has_clip) || - (type === 'snapshot' && event.has_snapshot) + (mediaType === 'clip' && !event.has_clip) || + (mediaType === 'snapshot' && !event.has_snapshot) || + !cameraConfig.frigate.client_id || + !cameraConfig.frigate.camera_name ) { - return new FrigateEventViewMedia(type, cameraID, event); + return null; } - return null; + + return new FrigateEventViewMedia( + mediaType, + cameraID, + event, + getEventMediaContentID( + cameraConfig.frigate.client_id, + cameraConfig.frigate.camera_name, + event, + mediaType === 'clip' ? 'clips' : 'snapshots', + ), + getEventThumbnailURL(cameraConfig.frigate.client_id, event), + ); } static createRecordingViewMedia( cameraID: string, recording: FrigateRecording, + cameraConfig: CameraConfig, ): FrigateRecordingViewMedia | null { - return new FrigateRecordingViewMedia('recording', cameraID, recording); + if (!cameraConfig.frigate.client_id || !cameraConfig.frigate.camera_name) { + return null; + } + + return new FrigateRecordingViewMedia( + 'recording', + cameraID, + recording, + getRecordingID(cameraConfig, recording), + getRecordingMediaContentID( + cameraConfig.frigate.client_id, + cameraConfig.frigate.camera_name, + recording, + ), + getRecordingTitle(cameraConfig, recording), + ); } } diff --git a/src/camera/frigate/util.ts b/src/camera/frigate/util.ts index 112ba4ff..b916ada9 100644 --- a/src/camera/frigate/util.ts +++ b/src/camera/frigate/util.ts @@ -82,3 +82,18 @@ export const getRecordingMediaContentID = ( String(recording.startTime.getHours()).padStart(2, '0'), ].join('/'); }; + +/** + * Get a recording ID for internal de-duping. + */ +export const getRecordingID = ( + cameraConfig: CameraConfig, + recording: FrigateRecording, +): string => { + // ID name is derived from the real camera name (not CameraID) since the + // recordings for the same camera across multiple zones will be the same and + // can be dedup'd from this id. + return `${cameraConfig.frigate?.client_id ?? ''}/${ + cameraConfig.frigate.camera_name ?? '' + }/${recording.startTime.getTime()}/${recording.endTime.getTime()}}`; +}; diff --git a/src/camera/manager.ts b/src/camera/manager.ts index 1cbddacd..609791bb 100644 --- a/src/camera/manager.ts +++ b/src/camera/manager.ts @@ -184,19 +184,21 @@ export class CameraManager { const mediaArray: ViewMedia[] = []; for (const [query, result] of results.entries()) { - const engine = this._engineFactory.getEngineForQuery(this._cameras, query); - if (engine) { + const cameraConfig = this._cameras.get(query.cameraID); + const engine = this._engineFactory.getEngineForCamera(cameraConfig); + + if (engine && cameraConfig) { let media: ViewMedia[] | null = null; if ( QueryClassifier.isEventQuery(query) && QueryResultClassifier.isEventQueryResult(result) ) { - media = engine.generateMediaFromEvents(query, result); + media = engine.generateMediaFromEvents(cameraConfig, query, result); } else if ( QueryClassifier.isRecordingQuery(query) && QueryResultClassifier.isRecordingQuery(result) ) { - media = engine.generateMediaFromRecordings(query, result); + media = engine.generateMediaFromRecordings(cameraConfig, query, result); } if (media) { mediaArray.push(...media); @@ -210,7 +212,7 @@ export class CameraManager { // are assumed to be unique. uniqBy( mediaArray, - (media) => media.getID(this._cameras.get(media.getCameraID())) ?? media, + (media) => media.getID() ?? media, ), // Sort all items leading with the most recent. diff --git a/src/card.ts b/src/card.ts index 049d4ffe..377ac9b7 100644 --- a/src/card.ts +++ b/src/card.ts @@ -1402,10 +1402,10 @@ export class FrigateCard extends LitElement { const media = this._view.queryResults?.getSelectedResult(); if (this._view.isViewerView() && media && this._cameras) { - media_content_id = media.getContentID(cameraConfig); + media_content_id = media.getContentID(); media_content_type = media.getContentType(); - title = media.getTitle(cameraConfig); - thumbnail = media.getThumbnail(cameraConfig); + title = media.getTitle(); + thumbnail = media.getThumbnail(); } else if (this._view?.is('live') && cameraEntity) { media_content_id = `media-source://camera/${cameraEntity}`; media_content_type = 'application/vnd.apple.mpegurl'; diff --git a/src/components/thumbnail.ts b/src/components/thumbnail.ts index f7b3aeee..2065b603 100644 --- a/src/components/thumbnail.ts +++ b/src/components/thumbnail.ts @@ -256,8 +256,8 @@ export class FrigateCardThumbnail extends LitElement { return; } - const thumbnail = this.media.getThumbnail(this.cameraConfig); - const title = this.media.getTitle(this.cameraConfig) ?? ''; + const thumbnail = this.media.getThumbnail(); + const title = this.media.getTitle() ?? ''; const starClasses = { star: true, @@ -271,7 +271,6 @@ export class FrigateCardThumbnail extends LitElement { // Only show timeline control if the recording has a start & end time. (this.media.getStartTime() && this.media.getEndTime())); - const clientID = this.cameraConfig?.frigate.client_id; return html` ${ViewMediaClassifier.isEvent(this.media) ? html`` : html``} - ${this.show_favorite_control && this.media && this.hass && clientID + ${this.show_favorite_control && this.media && this.hass ? html`