From 441162f052844ad27ac09b146821549d01023419 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 12 May 2022 21:06:27 -0700 Subject: [PATCH] Refactor to avoid use of "all-static" classes. --- src/card.ts | 4 +- src/components/gallery.ts | 12 +- src/components/live.ts | 4 +- src/components/surround-thumbnails.ts | 12 +- src/components/thumbnail-carousel.ts | 4 +- src/components/timeline.ts | 39 +- src/components/viewer.ts | 50 +- src/utils/ha/browse-media.ts | 737 +++++++++++++------------- src/utils/resolved-media.ts | 54 +- 9 files changed, 454 insertions(+), 462 deletions(-) diff --git a/src/card.ts b/src/card.ts index 89646db0..59aaa443 100644 --- a/src/card.ts +++ b/src/card.ts @@ -84,7 +84,7 @@ import { shouldUpdateBasedOnHass, sideLoadHomeAssistantElements } from './utils/ha'; -import { BrowseMediaUtil } from './utils/ha/browse-media.js'; +import { getEventID } from './utils/ha/browse-media.js'; import { supportsFeature } from './utils/ha/update.js'; import { isValidMediaShowInfo } from './utils/media-info.js'; import { ResolvedMediaCache } from './utils/resolved-media.js'; @@ -915,7 +915,7 @@ export class FrigateCard extends LitElement { }); return; } - const event_id = BrowseMediaUtil.getEventID(this._view.media); + const event_id = getEventID(this._view.media); if (!event_id) { this._setMessageAndUpdate({ message: localize('error.download_no_event_id'), diff --git a/src/components/gallery.ts b/src/components/gallery.ts index 4f03f0b9..80fabf2a 100644 --- a/src/components/gallery.ts +++ b/src/components/gallery.ts @@ -13,7 +13,11 @@ import { customElement, property } from 'lit/decorators.js'; import galleryStyle from '../scss/gallery.scss'; import { CameraConfig, frigateCardConfigDefaults, GalleryConfig } from '../types.js'; import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; -import { BrowseMediaUtil } from '../utils/ha/browse-media.js'; +import { + fetchChildMediaAndDispatchViewChange, + fetchLatestMediaAndDispatchViewChange, + getFullDependentBrowseMediaQueryParametersOrDispatchError +} from '../utils/ha/browse-media'; import { View } from '../view.js'; import { renderProgressIndicator } from './message.js'; import './thumbnail.js'; @@ -51,7 +55,7 @@ export class FrigateCardGallery extends LitElement { if (!this.view.target) { const browseMediaQueryParameters = - BrowseMediaUtil.getFullDependentBrowseMediaQueryParametersOrDispatchError( + getFullDependentBrowseMediaQueryParametersOrDispatchError( this, this.hass, this.cameras, @@ -63,7 +67,7 @@ export class FrigateCardGallery extends LitElement { return; } - BrowseMediaUtil.fetchLatestMediaAndDispatchViewChange( + fetchLatestMediaAndDispatchViewChange( this, this.hass, this.view, @@ -216,7 +220,7 @@ export class FrigateCardGalleryCore extends LitElement { { if (this.hass && this.view) { - BrowseMediaUtil.fetchChildMediaAndDispatchViewChange( + fetchChildMediaAndDispatchViewChange( this, this.hass, this.view, diff --git a/src/components/live.ts b/src/components/live.ts index adb2c111..6c78aa43 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -38,7 +38,7 @@ import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; import { contentsChanged } from '../utils/basic.js'; import { getCameraIcon, getCameraTitle } from '../utils/camera.js'; import { homeAssistantSignPath } from '../utils/ha'; -import { BrowseMediaUtil } from '../utils/ha/browse-media.js'; +import { getFullDependentBrowseMediaQueryParameters } from '../utils/ha/browse-media.js'; import { dispatchExistingMediaShowInfoAsEvent, dispatchMediaShowEvent @@ -125,7 +125,7 @@ export class FrigateCardLive extends LitElement { // Does not use getFullDependentBrowseMediaQueryParametersOrDispatchError to // ensure that non-Frigate cameras will work in live view (they will not // have a Frigate camera name). - const browseMediaParams = BrowseMediaUtil.getFullDependentBrowseMediaQueryParameters( + const browseMediaParams = getFullDependentBrowseMediaQueryParameters( this.hass, this.cameras, this.view.camera, diff --git a/src/components/surround-thumbnails.ts b/src/components/surround-thumbnails.ts index 3128005a..b65267b7 100644 --- a/src/components/surround-thumbnails.ts +++ b/src/components/surround-thumbnails.ts @@ -10,7 +10,10 @@ import { ThumbnailsControlConfig } from '../types.js'; import { dispatchFrigateCardEvent } from '../utils/basic.js'; -import { BrowseMediaUtil } from '../utils/ha/browse-media.js'; +import { + getFirstTrueMediaChildIndex, + multipleBrowseMediaQueryMerged +} from '../utils/ha/browse-media'; import { View } from '../view.js'; import { dispatchErrorMessageEvent } from './message.js'; import './surround.js'; @@ -68,14 +71,11 @@ export class FrigateCardSurround extends LitElement { } let parent: FrigateBrowseMediaSource | null; try { - parent = await BrowseMediaUtil.multipleBrowseMediaQueryMerged( - this.hass, - browseMediaParams, - ); + parent = await multipleBrowseMediaQueryMerged(this.hass, browseMediaParams); } catch (e) { return dispatchErrorMessageEvent(this, (e as Error).message); } - if (BrowseMediaUtil.getFirstTrueMediaChildIndex(parent) !== null) { + if (getFirstTrueMediaChildIndex(parent) !== null) { this.view ?.evolve({ ...(this.targetView && { view: this.targetView }), diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index 6310be56..d62d1711 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -7,7 +7,7 @@ import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss'; import type { FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js'; import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js'; -import { BrowseMediaUtil } from '../utils/ha/browse-media.js'; +import { isTrueMedia } from '../utils/ha/browse-media'; import { View } from '../view.js'; import { FrigateCardCarousel } from './carousel.js'; import './thumbnail.js'; @@ -154,7 +154,7 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { if ( !parent.children || !parent.children.length || - !BrowseMediaUtil.isTrueMedia(parent.children[childIndex]) + !isTrueMedia(parent.children[childIndex]) ) { return; } diff --git a/src/components/timeline.ts b/src/components/timeline.ts index ce219bcc..3b8ba9bf 100644 --- a/src/components/timeline.ts +++ b/src/components/timeline.ts @@ -37,7 +37,12 @@ import { import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; import { dispatchFrigateCardEvent } from '../utils/basic.js'; import { getCameraTitle } from '../utils/camera.js'; -import { BrowseMediaUtil } from '../utils/ha/browse-media.js'; +import { + createEventParentForChildren, + getBrowseMediaQueryParameters, + isTrueMedia, + multipleBrowseMediaQuery +} from '../utils/ha/browse-media'; import { View, ViewContext } from '../view'; import { dispatchErrorMessageEvent, dispatchMessageEvent } from './message.js'; import './surround-thumbnails.js'; @@ -137,7 +142,7 @@ class TimelineEventManager { const event = child.frigate?.event; if ( event && - BrowseMediaUtil.isTrueMedia(child) && + isTrueMedia(child) && ['video', 'image'].includes(child.media_content_type) ) { let item = this._dataset.get(event.id); @@ -282,20 +287,15 @@ class TimelineEventManager { this._dateStart && cameraConfig.camera_name !== CAMERA_BIRDSEYE ) { - const param = BrowseMediaUtil.getBrowseMediaQueryParameters( - hass, - cameraID, - cameraConfig, - { - // Events are always fetched for the maximum extent of the managed - // range. This is because events may change at any point in time - // (e.g. a long-running event that ends). - before: this._dateEnd.getTime() / 1000, - after: this._dateStart.getTime() / 1000, - unlimited: true, - mediaType: mediaType as 'clips' | 'snapshots', - }, - ); + const param = getBrowseMediaQueryParameters(hass, cameraID, cameraConfig, { + // Events are always fetched for the maximum extent of the managed + // range. This is because events may change at any point in time + // (e.g. a long-running event that ends). + before: this._dateEnd.getTime() / 1000, + after: this._dateStart.getTime() / 1000, + unlimited: true, + mediaType: mediaType as 'clips' | 'snapshots', + }); if (param) { params.push(param); } @@ -309,7 +309,7 @@ class TimelineEventManager { let results: Map; try { - results = await BrowseMediaUtil.multipleBrowseMediaQuery(hass, params); + results = await multipleBrowseMediaQuery(hass, params); } catch (e) { return dispatchErrorMessageEvent(element, (e as Error).message); } @@ -572,10 +572,7 @@ export class FrigateCardTimelineCore extends LitElement { return null; } - const target = BrowseMediaUtil.createEventParentForChildren( - 'Timeline events', - children, - ); + const target = createEventParentForChildren('Timeline events', children); return { target: target, childIndex: childIndex < 0 ? null : childIndex, diff --git a/src/components/viewer.ts b/src/components/viewer.ts index ab332aeb..73c3fd26 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -28,9 +28,16 @@ import type { } from '../types.js'; import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; import { contentsChanged } from '../utils/basic.js'; -import { BrowseMediaUtil } from '../utils/ha/browse-media.js'; +import { + fetchLatestMediaAndDispatchViewChange, + getEventStartTime, + getFullDependentBrowseMediaQueryParametersOrDispatchError, + isTrueMedia, + multipleBrowseMediaQueryMerged, + overrideMultiBrowseMediaQueryParameters +} from '../utils/ha/browse-media.js'; import { createMediaShowInfo } from '../utils/media-info.js'; -import { ResolvedMediaCache, ResolvedMediaUtil } from '../utils/resolved-media.js'; +import { ResolvedMediaCache, resolveMedia } from '../utils/resolved-media.js'; import { View } from '../view.js'; import { AutoMediaPlugin } from './embla-plugins/automedia.js'; import { Lazyload, LazyloadType } from './embla-plugins/lazyload.js'; @@ -66,7 +73,7 @@ export class FrigateCardViewer extends LitElement { } const browseMediaQueryParameters = - BrowseMediaUtil.getFullDependentBrowseMediaQueryParametersOrDispatchError( + getFullDependentBrowseMediaQueryParametersOrDispatchError( this, this.hass, this.cameras, @@ -83,14 +90,13 @@ export class FrigateCardViewer extends LitElement { return; } - BrowseMediaUtil.fetchLatestMediaAndDispatchViewChange( + fetchLatestMediaAndDispatchViewChange( this, this.hass, this.view, - BrowseMediaUtil.overrideMultiBrowseMediaQueryParameters( - browseMediaQueryParameters, - { mediaType: mediaType }, - ), + overrideMultiBrowseMediaQueryParameters(browseMediaQueryParameters, { + mediaType: mediaType, + }), ); return renderProgressIndicator(); } @@ -161,8 +167,8 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { i < (target.children || []).length; ++i ) { - if (BrowseMediaUtil.isTrueMedia(target.children[i])) { - await ResolvedMediaUtil.resolveMedia( + if (isTrueMedia(target.children[i])) { + await resolveMedia( this.hass, target.children[i], this.resolvedMediaCache, @@ -321,7 +327,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { let prevIndex: number | null = null; for (let i = this.view.childIndex - 1; i >= 0; i--) { const media = this.view.target.children[i]; - if (media && BrowseMediaUtil.isTrueMedia(media)) { + if (media && isTrueMedia(media)) { prevIndex = i; break; } @@ -331,7 +337,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { let nextIndex: number | null = null; for (let i = this.view.childIndex + 1; i < this.view.target.children.length; i++) { const media = this.view.target.children[i]; - if (media && BrowseMediaUtil.isTrueMedia(media)) { + if (media && isTrueMedia(media)) { nextIndex = i; break; } @@ -365,7 +371,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { return null; } - const snapshotStartTime = BrowseMediaUtil.getEventStartTime(snapshot); + const snapshotStartTime = getEventStartTime(snapshot); if (!snapshotStartTime) { return null; } @@ -383,10 +389,10 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { let latest: number | null = null; for (let i = 0; i < this.view.target.children.length; i++) { const child = this.view.target.children[i]; - if (!BrowseMediaUtil.isTrueMedia(child)) { + if (!isTrueMedia(child)) { continue; } - const startTime = BrowseMediaUtil.getEventStartTime(child); + const startTime = getEventStartTime(child); if (startTime && (earliest === null || startTime < earliest)) { earliest = startTime; @@ -401,7 +407,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { let clips: FrigateBrowseMediaSource | null; - const params = BrowseMediaUtil.overrideMultiBrowseMediaQueryParameters( + const params = overrideMultiBrowseMediaQueryParameters( this.browseMediaQueryParameters, { mediaType: 'clips', @@ -411,7 +417,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { ); try { - clips = await BrowseMediaUtil.multipleBrowseMediaQueryMerged(this.hass, params); + clips = await multipleBrowseMediaQueryMerged(this.hass, params); } catch (e) { // This is best effort. return null; @@ -423,10 +429,10 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { for (let i = 0; i < clips.children.length; i++) { const child = clips.children[i]; - if (!BrowseMediaUtil.isTrueMedia(child)) { + if (!isTrueMedia(child)) { continue; } - const clipStartTime = BrowseMediaUtil.getEventStartTime(child); + const clipStartTime = getEventStartTime(child); if (clipStartTime && clipStartTime === snapshotStartTime) { return this.view.evolve({ view: 'clip', @@ -487,12 +493,12 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { !this.view || !this.view.target || !this.view.target.children || - !BrowseMediaUtil.isTrueMedia(this.view.target.children[childIndex]) + !isTrueMedia(this.view.target.children[childIndex]) ) { return; } - ResolvedMediaUtil.resolveMedia( + resolveMedia( this.hass, this.view.target.children[childIndex], this.resolvedMediaCache, @@ -664,7 +670,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { !this.hass || !this.view || !this.viewerConfig || - !BrowseMediaUtil.isTrueMedia(mediaToRender) || + !isTrueMedia(mediaToRender) || !['video', 'image'].includes(mediaToRender.media_content_type) ) { return; diff --git a/src/utils/ha/browse-media.ts b/src/utils/ha/browse-media.ts index 828e4b62..cf7a7fb8 100644 --- a/src/utils/ha/browse-media.ts +++ b/src/utils/ha/browse-media.ts @@ -1,414 +1,405 @@ import { HomeAssistant } from 'custom-card-helpers'; import { homeAssistantWSRequest } from '.'; import { - dispatchErrorMessageEvent, - dispatchMessageEvent + dispatchErrorMessageEvent, + dispatchMessageEvent } from '../../components/message.js'; import { localize } from '../../localize/localize.js'; import { - BrowseMediaQueryParameters, - CameraConfig, - FrigateBrowseMediaSource, - frigateBrowseMediaSourceSchema, - MEDIA_CLASS_PLAYLIST, - MEDIA_TYPE_PLAYLIST + BrowseMediaQueryParameters, + CameraConfig, + FrigateBrowseMediaSource, + frigateBrowseMediaSourceSchema, + MEDIA_CLASS_PLAYLIST, + MEDIA_TYPE_PLAYLIST } from '../../types.js'; import { View } from '../../view.js'; import { getCameraTitle } from '../camera.js'; -export class BrowseMediaUtil { - /** - * Return the Frigate event_id given a FrigateBrowseMediaSource object. - * @param media The event to get the id from. - * @returns The `event_id` or `null` if not successfully parsed. - */ - static getEventID(media: FrigateBrowseMediaSource): string | null { - return media.frigate?.event.id ?? null; +/** + * Return the Frigate event_id given a FrigateBrowseMediaSource object. + * @param media The event to get the id from. + * @returns The `event_id` or `null` if not successfully parsed. + */ +export const getEventID = (media: FrigateBrowseMediaSource): string | null => { + return media.frigate?.event.id ?? null; +}; + +/** + * Return the event start time given a FrigateBrowseMediaSource object. + * @param browseMedia The media object to get the start time from. + * @returns The start time in unix/epoch time, or null if it cannot be determined. + */ +export const getEventStartTime = (media: FrigateBrowseMediaSource): number | null => { + return media.frigate?.event.start_time ?? null; +}; + +/** + * Determine if a FrigateBrowseMediaSource object is truly a media item (vs a folder). + * @param media The media object. + * @returns `true` if it's truly a media item, `false` otherwise. + */ +export const isTrueMedia = (media?: FrigateBrowseMediaSource): boolean => { + return !!media && !media.can_expand; +}; + +/** + * From a FrigateBrowseMediaSource item extract the first true media item from the + * children (i.e. a clip/snapshot, not a folder). + * @param media The media object with children. + * @returns The first true media item found. + */ +export const getFirstTrueMediaChildIndex = ( + media: FrigateBrowseMediaSource | null, +): number | null => { + if (!media || !media.children) { + return null; } + const index = media.children.findIndex((child) => isTrueMedia(child)); + return index >= 0 ? index : null; +}; - /** - * Return the event start time given a FrigateBrowseMediaSource object. - * @param browseMedia The media object to get the start time from. - * @returns The start time in unix/epoch time, or null if it cannot be determined. - */ - static getEventStartTime(media: FrigateBrowseMediaSource): number | null { - return media.frigate?.event.start_time ?? null; - } +/** + * Browse Frigate media with a media content id. May throw. + * @param hass The HomeAssistant object. + * @param media_content_id The media content id to browse. + * @returns A FrigateBrowseMediaSource object or null on malformed. + */ +export const browseMedia = async ( + hass: HomeAssistant, + media_content_id: string, +): Promise => { + const request = { + type: 'media_source/browse_media', + media_content_id: media_content_id, + }; + return await homeAssistantWSRequest(hass, frigateBrowseMediaSourceSchema, request); +}; - /** - * Determine if a FrigateBrowseMediaSource object is truly a media item (vs a folder). - * @param media The media object. - * @returns `true` if it's truly a media item, `false` otherwise. - */ - static isTrueMedia(media?: FrigateBrowseMediaSource): boolean { - return !!media && !media.can_expand; - } +/** + * Browse Frigate media with a media query. May throw. + * @param hass The HomeAssistant object. + * @param params The search parameters to use to search for media. + * @returns A FrigateBrowseMediaSource object or null on malformed. + */ +export const browseMediaQuery = async ( + hass: HomeAssistant, + params: BrowseMediaQueryParameters, +): Promise => { + return browseMedia( + hass, + // Defined in: + // https://github.com/blakeblackshear/frigate-hass-integration/blob/master/custom_components/frigate/media_source.py + [ + 'media-source://frigate', + params.clientId, + 'event-search', + params.mediaType, - /** - * From a FrigateBrowseMediaSource item extract the first true media item from the - * children (i.e. a clip/snapshot, not a folder). - * @param media The media object with children. - * @returns The first true media item found. - */ - static getFirstTrueMediaChildIndex( - media: FrigateBrowseMediaSource | null, - ): number | null { - if (!media || !media.children) { - return null; - } - const index = media.children.findIndex((child) => this.isTrueMedia(child)); - return index >= 0 ? index : null; - } + // If the name field ends in '.all' the integration will return up to 10K events. + params.unlimited ? '.all' : '', + params.after ? String(Math.floor(params.after)) : '', + params.before ? String(Math.ceil(params.before)) : '', + params.cameraName, + params.label, + params.zone, + ].join('/'), + ); +}; - /** - * Browse Frigate media with a media content id. May throw. - * @param hass The HomeAssistant object. - * @param media_content_id The media content id to browse. - * @returns A FrigateBrowseMediaSource object or null on malformed. - */ - static async browseMedia( - hass: HomeAssistant, - media_content_id: string, - ): Promise { - const request = { - type: 'media_source/browse_media', - media_content_id: media_content_id, - }; - return await homeAssistantWSRequest(hass, frigateBrowseMediaSourceSchema, request); - } +/** + * Browse multiple Frigate media queries. May throw. + * @param hass The HomeAssistant object. + * @param params An array of search parameters to use to search for media. + * @returns A map of FrigateBrowseMediaSource object or null on malformed. + */ +export const multipleBrowseMediaQuery = async ( + hass: HomeAssistant, + params: BrowseMediaQueryParameters | BrowseMediaQueryParameters[], +): Promise> => { + params = Array.isArray(params) ? params : [params]; + const output: Map = new Map(); + await Promise.all( + params.map(async (param: BrowseMediaQueryParameters): Promise => { + output.set(param, await browseMediaQuery(hass, param)); + }), + ); + return output; +}; - /** - * Browse Frigate media with a media query. May throw. - * @param hass The HomeAssistant object. - * @param params The search parameters to use to search for media. - * @returns A FrigateBrowseMediaSource object or null on malformed. - */ - static async browseMediaQuery( - hass: HomeAssistant, - params: BrowseMediaQueryParameters, - ): Promise { - return this.browseMedia( - hass, - // Defined in: - // https://github.com/blakeblackshear/frigate-hass-integration/blob/master/custom_components/frigate/media_source.py - [ - 'media-source://frigate', - params.clientId, - 'event-search', - params.mediaType, +/** + * Browse multiple Frigate media queries, then merged them. May throw. + * @param hass The HomeAssistant object. + * @param params An array of search parameters to use to search for media. + * @returns A single FrigateBrowseMediaSource object or null on malformed. + */ +export const multipleBrowseMediaQueryMerged = async ( + hass: HomeAssistant, + params: BrowseMediaQueryParameters | BrowseMediaQueryParameters[], +): Promise => { + return mergeFrigateBrowseMediaSources(await multipleBrowseMediaQuery(hass, params)); +}; - // If the name field ends in '.all' the integration will return up to 10K events. - params.unlimited ? '.all' : '', - params.after ? String(Math.floor(params.after)) : '', - params.before ? String(Math.ceil(params.before)) : '', - params.cameraName, - params.label, - params.zone, - ].join('/'), - ); - } +/** + * Merge multiple FrigateBrowseMediaSource into a single. Note that this may + * use information from the query to differentiate results that may otherwise + * be identical. + * @param input A map of query -> result. + * @returns A single FrigateBrowseMediaSource object. + */ +export const mergeFrigateBrowseMediaSources = async ( + input: Map, +): Promise => { + const children: FrigateBrowseMediaSource[] = []; - /** - * Browse multiple Frigate media queries. May throw. - * @param hass The HomeAssistant object. - * @param params An array of search parameters to use to search for media. - * @returns A map of FrigateBrowseMediaSource object or null on malformed. - */ - static async multipleBrowseMediaQuery( - hass: HomeAssistant, - params: BrowseMediaQueryParameters | BrowseMediaQueryParameters[], - ): Promise> { - params = Array.isArray(params) ? params : [params]; - const output: Map = new Map(); - await Promise.all( - params.map(async (param: BrowseMediaQueryParameters): Promise => { - output.set(param, await this.browseMediaQuery(hass, param)); - }), - ); - return output; - } - - /** - * Browse multiple Frigate media queries, then merged them. May throw. - * @param hass The HomeAssistant object. - * @param params An array of search parameters to use to search for media. - * @returns A single FrigateBrowseMediaSource object or null on malformed. - */ - static async multipleBrowseMediaQueryMerged( - hass: HomeAssistant, - params: BrowseMediaQueryParameters | BrowseMediaQueryParameters[], - ): Promise { - return this.mergeFrigateBrowseMediaSources( - await this.multipleBrowseMediaQuery(hass, params), - ); - } - - /** - * Merge multiple FrigateBrowseMediaSource into a single. Note that this may - * use information from the query to differentiate results that may otherwise - * be identical. - * @param input A map of query -> result. - * @returns A single FrigateBrowseMediaSource object. - */ - static mergeFrigateBrowseMediaSources( - input: Map, - ): FrigateBrowseMediaSource { - const children: FrigateBrowseMediaSource[] = []; - - for (const [query, result] of input.entries()) { - for (const child of result.children || []) { - if (this.isTrueMedia(child)) { - children.push(child); + for (const [query, result] of input.entries()) { + for (const child of result.children || []) { + if (isTrueMedia(child)) { + children.push(child); + } else { + // If there are multiple inputs, separate the folder names with the + // query title (if available). + if (query.title && input.size > 1) { + children.push({ ...child, title: `[${query.title}] ${child.title}` }); } else { - // If there are multiple inputs, separate the folder names with the - // query title (if available). - if (query.title && input.size > 1) { - children.push({ ...child, title: `[${query.title}] ${child.title}` }); - } else { - children.push(child); - } + children.push(child); } } } - - const eventSort = ( - a: FrigateBrowseMediaSource, - b: FrigateBrowseMediaSource, - ): number => { - if ( - !a.frigate?.event || - (b.frigate?.event && b.frigate.event.start_time > a.frigate.event.start_time) - ) { - return 1; - } - - if ( - !b.frigate?.event || - (a.frigate?.event && b.frigate.event.start_time < a.frigate.event.start_time) - ) { - return -1; - } - return 0; - }; - - return this.createEventParentForChildren('Merged events', children.sort(eventSort)); } - /** - * Get the parameters to search for media. - * @returns A BrowseMediaQueryParameters object. - */ - static getBrowseMediaQueryParameters( - hass: HomeAssistant, - cameraID: string, - cameraConfig?: CameraConfig, - overrides?: Partial, - ): BrowseMediaQueryParameters | null { - if (!cameraConfig || !cameraConfig.camera_name) { - return null; + const eventSort = ( + a: FrigateBrowseMediaSource, + b: FrigateBrowseMediaSource, + ): number => { + if ( + !a.frigate?.event || + (b.frigate?.event && b.frigate.event.start_time > a.frigate.event.start_time) + ) { + return 1; } - return { - clientId: cameraConfig.client_id, - cameraName: cameraConfig.camera_name, - label: cameraConfig.label, - zone: cameraConfig.zone, - title: getCameraTitle(hass, cameraConfig), - cameraID: cameraID, - ...overrides, - }; - } - /** - * Apply overrides to multiple query parameters. - * @param parameters An array of query parameters. - * @param overrides The overrides to apply. - * @returns The override query parameters. - */ - static overrideMultiBrowseMediaQueryParameters( - parameters: BrowseMediaQueryParameters[], - overrides: Partial, - ): BrowseMediaQueryParameters[] { - const output: BrowseMediaQueryParameters[] = []; - parameters.forEach((param) => { - output.push({ ...param, ...overrides }); - }); - return output; - } + if ( + !b.frigate?.event || + (a.frigate?.event && b.frigate.event.start_time < a.frigate.event.start_time) + ) { + return -1; + } + return 0; + }; - /** - * Get BrowseMediaQueryParameters for a camera (including its dependencies). - * @param hass Home Assistant object. - * @param cameras Cameras map. - * @param camera Name of the current camera. - * @param mediaType Optional media type to include in the parameters. - * @returns An array of query parameters. - */ - static getFullDependentBrowseMediaQueryParameters( - hass: HomeAssistant, - cameras: Map, - camera: string, - mediaType?: 'clips' | 'snapshots', - ): BrowseMediaQueryParameters[] | null { - const cameraIDs: Set = new Set(); - const getDependentCameras = (camera: string): void => { - const cameraConfig = cameras.get(camera); - if (cameraConfig) { - cameraIDs.add(camera); - for (const eventCameraID of cameraConfig.dependent_cameras || []) { - if (!cameraIDs.has(eventCameraID)) { - getDependentCameras(eventCameraID); - } + return createEventParentForChildren('Merged events', children.sort(eventSort)); +}; + +/** + * Get the parameters to search for media. + * @returns A BrowseMediaQueryParameters object. + */ +export const getBrowseMediaQueryParameters = ( + hass: HomeAssistant, + cameraID: string, + cameraConfig?: CameraConfig, + overrides?: Partial, +): BrowseMediaQueryParameters | null => { + if (!cameraConfig || !cameraConfig.camera_name) { + return null; + } + return { + clientId: cameraConfig.client_id, + cameraName: cameraConfig.camera_name, + label: cameraConfig.label, + zone: cameraConfig.zone, + title: getCameraTitle(hass, cameraConfig), + cameraID: cameraID, + ...overrides, + }; +}; + +/** + * Apply overrides to multiple query parameters. + * @param parameters An array of query parameters. + * @param overrides The overrides to apply. + * @returns The override query parameters. + */ +export const overrideMultiBrowseMediaQueryParameters = ( + parameters: BrowseMediaQueryParameters[], + overrides: Partial, +): BrowseMediaQueryParameters[] => { + const output: BrowseMediaQueryParameters[] = []; + parameters.forEach((param) => { + output.push({ ...param, ...overrides }); + }); + return output; +}; + +/** + * Get BrowseMediaQueryParameters for a camera (including its dependencies). + * @param hass Home Assistant object. + * @param cameras Cameras map. + * @param camera Name of the current camera. + * @param mediaType Optional media type to include in the parameters. + * @returns An array of query parameters. + */ +export const getFullDependentBrowseMediaQueryParameters = ( + hass: HomeAssistant, + cameras: Map, + camera: string, + mediaType?: 'clips' | 'snapshots', +): BrowseMediaQueryParameters[] | null => { + const cameraIDs: Set = new Set(); + const getDependentCameras = (camera: string): void => { + const cameraConfig = cameras.get(camera); + if (cameraConfig) { + cameraIDs.add(camera); + for (const eventCameraID of cameraConfig.dependent_cameras || []) { + if (!cameraIDs.has(eventCameraID)) { + getDependentCameras(eventCameraID); } } - }; - getDependentCameras(camera); - - const params: BrowseMediaQueryParameters[] = []; - for (const cameraID of cameraIDs) { - const param = BrowseMediaUtil.getBrowseMediaQueryParameters( - hass, - cameraID, - cameras.get(cameraID), - mediaType ? { mediaType: mediaType } : {}, - ); - // Fail on a single bad camera, as it's almost certainly a user error that - // should be fixed rather than hidden. - if (!param) { - return null; - } - params.push(param); } - return params.length ? params : null; - } + }; + getDependentCameras(camera); - /** - * Get BrowseMediaQueryParameters for a camera (including its dependencies) or dispatch an error. - * @param element The element from which to dispatch the error. - * @param hass Home Assistant object. - * @param cameras Cameras map. - * @param camera Name of the current camera. - * @param mediaType Optional media type to include in the parameters. - * @returns An array of query parameters. - */ - static getFullDependentBrowseMediaQueryParametersOrDispatchError( - element: HTMLElement, - hass: HomeAssistant, - cameras: Map, - camera: string, - mediaType?: 'clips' | 'snapshots', - ): BrowseMediaQueryParameters[] | null { - const params = this.getFullDependentBrowseMediaQueryParameters( + const params: BrowseMediaQueryParameters[] = []; + for (const cameraID of cameraIDs) { + const param = getBrowseMediaQueryParameters( hass, - cameras, - camera, - mediaType, + cameraID, + cameras.get(cameraID), + mediaType ? { mediaType: mediaType } : {}, ); - if (!params) { - dispatchErrorMessageEvent( - element, - localize('error.no_camera_name'), - cameras.get(camera), - ); + // Fail on a single bad camera, as it's almost certainly a user error that + // should be fixed rather than hidden. + if (!param) { return null; } - return params; + params.push(param); + } + return params.length ? params : null; +}; + +/** + * Get BrowseMediaQueryParameters for a camera (including its dependencies) or dispatch an error. + * @param element The element from which to dispatch the error. + * @param hass Home Assistant object. + * @param cameras Cameras map. + * @param camera Name of the current camera. + * @param mediaType Optional media type to include in the parameters. + * @returns An array of query parameters. + */ +export const getFullDependentBrowseMediaQueryParametersOrDispatchError = ( + element: HTMLElement, + hass: HomeAssistant, + cameras: Map, + camera: string, + mediaType?: 'clips' | 'snapshots', +): BrowseMediaQueryParameters[] | null => { + const params = getFullDependentBrowseMediaQueryParameters( + hass, + cameras, + camera, + mediaType, + ); + if (!params) { + dispatchErrorMessageEvent( + element, + localize('error.no_camera_name'), + cameras.get(camera), + ); + return null; + } + return params; +}; + +/** + * Fetch the latest media and dispatch a change view event to reflect the + * results. If no media is found a suitable message event will be triggered + * instead. + * @param element The HTMLElement to dispatch events from. + * @param hass The Home Assistant object. + * @param view The current view to evolve. + * @param browseMediaQueryParameters The media parameters to query with. + * @returns + */ +export const fetchLatestMediaAndDispatchViewChange = async ( + element: HTMLElement, + hass: HomeAssistant, + view: Readonly, + browseMediaQueryParameters: BrowseMediaQueryParameters | BrowseMediaQueryParameters[], +): Promise => { + let parent: FrigateBrowseMediaSource | null; + try { + parent = await multipleBrowseMediaQueryMerged(hass, browseMediaQueryParameters); + } catch (e) { + return dispatchErrorMessageEvent(element, (e as Error).message); + } + const childIndex = getFirstTrueMediaChildIndex(parent); + if (!parent || !parent.children || childIndex == null) { + return dispatchMessageEvent( + element, + view.isClipRelatedView() + ? localize('common.no_clip') + : localize('common.no_snapshot'), + view.isClipRelatedView() ? 'mdi:filmstrip-off' : 'mdi:camera-off', + ); } - /** - * Fetch the latest media and dispatch a change view event to reflect the - * results. If no media is found a suitable message event will be triggered - * instead. - * @param element The HTMLElement to dispatch events from. - * @param hass The Home Assistant object. - * @param view The current view to evolve. - * @param browseMediaQueryParameters The media parameters to query with. - * @returns - */ - static async fetchLatestMediaAndDispatchViewChange( - element: HTMLElement, - hass: HomeAssistant, - view: Readonly, - browseMediaQueryParameters: - | BrowseMediaQueryParameters - | BrowseMediaQueryParameters[], - ): Promise { - let parent: FrigateBrowseMediaSource | null; - try { - parent = await BrowseMediaUtil.multipleBrowseMediaQueryMerged( - hass, - browseMediaQueryParameters, - ); - } catch (e) { - return dispatchErrorMessageEvent(element, (e as Error).message); - } - const childIndex = BrowseMediaUtil.getFirstTrueMediaChildIndex(parent); - if (!parent || !parent.children || childIndex == null) { - return dispatchMessageEvent( - element, - view.isClipRelatedView() - ? localize('common.no_clip') - : localize('common.no_snapshot'), - view.isClipRelatedView() ? 'mdi:filmstrip-off' : 'mdi:camera-off', - ); - } + view + .evolve({ + target: parent, + childIndex: childIndex, + }) + .dispatchChangeEvent(element); +}; - view - .evolve({ - target: parent, - childIndex: childIndex, - }) - .dispatchChangeEvent(element); +/** + * Fetch the media of a child FrigateBrowseMediaSource object and dispatch a change + * view event to reflect the results. + * @param node The HTMLElement to dispatch events from. + * @param hass The Home Assistant object. + * @param view The current view to evolve. + * @param child The FrigateBrowseMediaSource child to query for. + * @returns + */ +export const fetchChildMediaAndDispatchViewChange = async ( + element: HTMLElement, + hass: HomeAssistant, + view: Readonly, + child: Readonly, +): Promise => { + let parent: FrigateBrowseMediaSource; + try { + parent = await browseMedia(hass, child.media_content_id); + } catch (e) { + return dispatchErrorMessageEvent(element, (e as Error).message); } - /** - * Fetch the media of a child FrigateBrowseMediaSource object and dispatch a change - * view event to reflect the results. - * @param node The HTMLElement to dispatch events from. - * @param hass The Home Assistant object. - * @param view The current view to evolve. - * @param child The FrigateBrowseMediaSource child to query for. - * @returns - */ - static async fetchChildMediaAndDispatchViewChange( - element: HTMLElement, - hass: HomeAssistant, - view: Readonly, - child: Readonly, - ): Promise { - let parent: FrigateBrowseMediaSource; - try { - parent = await BrowseMediaUtil.browseMedia(hass, child.media_content_id); - } catch (e) { - return dispatchErrorMessageEvent(element, (e as Error).message); - } + view + .evolve({ + target: parent, + }) + .dispatchChangeEvent(element); +}; - view - .evolve({ - target: parent, - }) - .dispatchChangeEvent(element); - } - - /** - * Given an array of media children, create a parent for them. - * @param title The title to use for the parent. - * @param children The children media items. - * @returns A single parent containing the children. - */ - static createEventParentForChildren( - title: string, - children: FrigateBrowseMediaSource[], - ): FrigateBrowseMediaSource { - return { - title: title, - media_class: MEDIA_CLASS_PLAYLIST, - media_content_type: MEDIA_TYPE_PLAYLIST, - media_content_id: '', - can_play: false, - can_expand: true, - children_media_class: MEDIA_CLASS_PLAYLIST, - thumbnail: null, - children: children, - }; - } -} +/** + * Given an array of media children, create a parent for them. + * @param title The title to use for the parent. + * @param children The children media items. + * @returns A single parent containing the children. + */ +export const createEventParentForChildren = ( + title: string, + children: FrigateBrowseMediaSource[], +): FrigateBrowseMediaSource => { + return { + title: title, + media_class: MEDIA_CLASS_PLAYLIST, + media_content_type: MEDIA_TYPE_PLAYLIST, + media_content_id: '', + can_play: false, + can_expand: true, + children_media_class: MEDIA_CLASS_PLAYLIST, + thumbnail: null, + children: children, + }; +}; diff --git a/src/utils/resolved-media.ts b/src/utils/resolved-media.ts index 47feb58a..3ce77436 100644 --- a/src/utils/resolved-media.ts +++ b/src/utils/resolved-media.ts @@ -1,9 +1,9 @@ import { HomeAssistant } from 'custom-card-helpers'; import QuickLRU from 'quick-lru'; import { - FrigateBrowseMediaSource, - ResolvedMedia, - resolvedMediaSchema + FrigateBrowseMediaSource, + ResolvedMedia, + resolvedMediaSchema } from '../types.js'; import { homeAssistantWSRequest } from './ha'; @@ -34,31 +34,25 @@ export class ResolvedMediaCache { } } -export class ResolvedMediaUtil { - static async resolveMedia( - hass: HomeAssistant, - mediaSource?: FrigateBrowseMediaSource, - cache?: ResolvedMediaCache, - ): Promise { - if (!mediaSource) { - return null; - } - const cachedValue = cache ? cache.get(mediaSource.media_content_id) : undefined; - if (cachedValue) { - return cachedValue; - } - const request = { - type: 'media_source/resolve_media', - media_content_id: mediaSource.media_content_id, - }; - const resolvedMedia = await homeAssistantWSRequest( - hass, - resolvedMediaSchema, - request, - ); - if (cache && resolvedMedia) { - cache.set(mediaSource.media_content_id, resolvedMedia); - } - return resolvedMedia; +export const resolveMedia = async ( + hass: HomeAssistant, + mediaSource?: FrigateBrowseMediaSource, + cache?: ResolvedMediaCache, +): Promise => { + if (!mediaSource) { + return null; } -} + const cachedValue = cache ? cache.get(mediaSource.media_content_id) : undefined; + if (cachedValue) { + return cachedValue; + } + const request = { + type: 'media_source/resolve_media', + media_content_id: mediaSource.media_content_id, + }; + const resolvedMedia = await homeAssistantWSRequest(hass, resolvedMediaSchema, request); + if (cache && resolvedMedia) { + cache.set(mediaSource.media_content_id, resolvedMedia); + } + return resolvedMedia; +};