feat: Implement basic general folder support (#2051)

- Related: #1748
This commit is contained in:
Dermot Duffy
2025-05-21 19:59:21 -07:00
committed by GitHub
parent 2eb0d9e35e
commit c6a4c8aea2
350 changed files with 12837 additions and 4509 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
import { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
import { HomeAssistant } from '../../ha/types';
import { localize } from '../../localize/localize';
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
import { Camera, CameraInitializationOptions } from '../camera';
import { CameraInitializationError } from '../error';
@@ -1,21 +1,21 @@
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
import { CameraConfig } from '../../config/schema/cameras';
import { BROWSE_MEDIA_CACHE_SECONDS } from '../../ha/browse-media/types';
import { BrowseMediaWalker } from '../../ha/browse-media/walker';
import { getMediaDownloadPath } from '../../ha/download';
import { EntityRegistryManager } from '../../ha/registry/entity/types';
import { ResolvedMediaCache } from '../../ha/resolved-media';
import { HomeAssistant } from '../../ha/types';
import { canonicalizeHAURL } from '../../utils/ha';
import { BrowseMediaManager } from '../../utils/ha/browse-media/browse-media-manager';
import { BROWSE_MEDIA_CACHE_SECONDS } from '../../utils/ha/browse-media/types';
import { EntityRegistryManager } from '../../utils/ha/registry/entity/types';
import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media';
import { ViewMedia } from '../../view/media';
import { RequestCache } from '../cache';
import { Endpoint } from '../../types';
import { ViewMedia } from '../../view/item';
import { ViewItemCapabilities } from '../../view/types';
import { CameraManagerEngine } from '../engine';
import { GenericCameraManagerEngine } from '../generic/engine-generic';
import { CameraManagerReadOnlyConfigStore } from '../store';
import {
CameraEndpoint,
CameraEventCallback,
CameraManagerMediaCapabilities,
DataQuery,
CameraManagerRequestCache,
CameraQuery,
EventQuery,
PartialEventQuery,
QueryType,
@@ -28,22 +28,22 @@ export class BrowseMediaCameraManagerEngine
extends GenericCameraManagerEngine
implements CameraManagerEngine
{
protected _browseMediaManager: BrowseMediaManager;
protected _browseMediaWalker: BrowseMediaWalker;
protected _entityRegistryManager: EntityRegistryManager;
protected _resolvedMediaCache: ResolvedMediaCache;
protected _requestCache: RequestCache;
protected _requestCache: CameraManagerRequestCache;
public constructor(
entityRegistryManager: EntityRegistryManager,
stateWatcher: StateWatcherSubscriptionInterface,
browseMediaManager: BrowseMediaManager,
browseMediaManager: BrowseMediaWalker,
resolvedMediaCache: ResolvedMediaCache,
requestCache: RequestCache,
requestCache: CameraManagerRequestCache,
eventCallback?: CameraEventCallback,
) {
super(stateWatcher, eventCallback);
this._entityRegistryManager = entityRegistryManager;
this._browseMediaManager = browseMediaManager;
this._browseMediaWalker = browseMediaManager;
this._resolvedMediaCache = resolvedMediaCache;
this._requestCache = requestCache;
}
@@ -66,18 +66,11 @@ export class BrowseMediaCameraManagerEngine
hass: HomeAssistant,
_cameraConfig: CameraConfig,
media: ViewMedia,
): Promise<CameraEndpoint | null> {
const contentID = media.getContentID();
if (!contentID) {
return null;
}
const resolvedMedia = await resolveMedia(hass, contentID, this._resolvedMediaCache);
return resolvedMedia
? { endpoint: canonicalizeHAURL(hass, resolvedMedia.url) }
: null;
): Promise<Endpoint | null> {
return getMediaDownloadPath(hass, media.getContentID(), this._resolvedMediaCache);
}
public getQueryResultMaxAge(query: DataQuery): number | null {
public getQueryResultMaxAge(query: CameraQuery): number | null {
if (query.type === QueryType.Event) {
return BROWSE_MEDIA_CACHE_SECONDS;
}
@@ -85,7 +78,7 @@ export class BrowseMediaCameraManagerEngine
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public getMediaCapabilities(_media: ViewMedia): CameraManagerMediaCapabilities {
public getMediaCapabilities(_media: ViewMedia): ViewItemCapabilities {
return {
canFavorite: false,
canDownload: true,
-85
View File
@@ -1,85 +0,0 @@
import { format } from 'date-fns';
import isEqual from 'lodash-es/isEqual';
import { formatDateAndTime } from '../../utils/basic';
import { RichBrowseMedia } from '../../utils/ha/browse-media/types';
import {
ViewMedia,
EventViewMedia,
ViewMediaType,
VideoContentType,
} from '../../view/media';
import { BrowseMediaMetadata } from '../browse-media/types';
class BrowseMediaEventViewMedia extends ViewMedia implements EventViewMedia {
protected _browseMedia: RichBrowseMedia<BrowseMediaMetadata>;
protected _id: string;
constructor(
mediaType: ViewMediaType,
cameraID: string,
browseMedia: RichBrowseMedia<BrowseMediaMetadata>,
) {
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 getStartTime(): Date | null {
return this._browseMedia._metadata?.startDate ?? null;
}
public getEndTime(): Date | null {
return this._browseMedia._metadata?.endDate ?? null;
}
public getVideoContentType(): VideoContentType | null {
return VideoContentType.MP4;
}
public getID(): string {
return this._id;
}
public getContentID(): string {
return this._browseMedia.media_content_id;
}
public getTitle(): string | null {
const startTime = this.getStartTime();
return startTime ? formatDateAndTime(startTime) : this._browseMedia.title;
}
public getThumbnail(): string | null {
return this._browseMedia.thumbnail;
}
public getWhat(): string[] | null {
return this._browseMedia._metadata?.what ?? null;
}
public getScore(): number | null {
return null;
}
public getTags(): string[] | null {
return null;
}
public isGroupableWith(that: EventViewMedia): boolean {
return (
this.getMediaType() === that.getMediaType() &&
isEqual(this.getWhere(), that.getWhere()) &&
isEqual(this.getWhat(), that.getWhat())
);
}
}
export class BrowseMediaViewMediaFactory {
static createEventViewMedia(
mediaType: 'clip' | 'snapshot',
browseMedia: RichBrowseMedia<BrowseMediaMetadata>,
cameraID: string,
): BrowseMediaEventViewMedia {
return new BrowseMediaEventViewMedia(mediaType, cameraID, browseMedia);
}
}
-6
View File
@@ -1,6 +0,0 @@
export interface BrowseMediaMetadata {
cameraID: string;
startDate: Date;
endDate: Date;
what?: string[];
}
@@ -1,49 +0,0 @@
import {
RichBrowseMedia,
MEDIA_CLASS_VIDEO,
MEDIA_CLASS_IMAGE,
} from '../../../utils/ha/browse-media/types';
import { ViewMedia } from '../../../view/media';
import { BrowseMediaViewMediaFactory } from '../media';
import { BrowseMediaMetadata } from '../types';
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,
);
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()];
};
@@ -1,50 +0,0 @@
import { RichBrowseMedia } from '../../../utils/ha/browse-media/types';
import { rangesOverlap } from '../../range';
import { BrowseMediaMetadata } from '../types';
/**
* 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 there's no metadata, nothing matches.
if (!media._metadata) {
return false;
}
if (start && end) {
// 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,
end: end,
},
);
}
if (!start && end) {
return media._metadata.startDate <= end;
}
if (start && !end) {
return media._metadata.startDate >= start;
}
// If no date is specified at all, everything matches.
return true;
};
+2 -65
View File
@@ -1,69 +1,6 @@
import isEqual from 'lodash-es/isEqual';
import orderBy from 'lodash-es/orderBy';
import sortedUniqBy from 'lodash-es/sortedUniqBy';
import { orderBy, sortedUniqBy } from 'lodash-es';
import { DateRange, MemoryRangeSet } from './range';
import { DataQuery, QueryResults, RecordingSegment } from './types';
interface RequestCacheItem<Request, Response> {
request: Request;
response: Response;
expires?: Date;
}
interface CameraManagerCache<Request, Response> {
get(request: Request): Response | null;
has(request: Request): boolean;
set(request: Request, response: Response, expiry?: Date): void;
}
export class MemoryRequestCache<Request, Response>
implements CameraManagerCache<Request, Response>
{
protected _data: RequestCacheItem<Request, Response>[] = [];
public get(request: Request): Response | null {
const now = new Date();
for (const item of this._data) {
if (
(!item.expires || now <= item.expires) &&
this._contains(request, item.request)
) {
return item.response;
}
}
return null;
}
public clear(): void {
this._data = [];
}
public has(request: Request): boolean {
return !!this.get(request);
}
public set(request: Request, response: Response, expiry?: Date): void {
this._data.push({
request: request,
response: response,
expires: expiry,
});
// Clean up old requests on set.
this._expireOldRequests();
}
protected _contains(a: Request, b: Request): boolean {
return isEqual(a, b);
}
protected _expireOldRequests(): void {
const now = new Date();
this._data = this._data.filter((item) => !item.expires || now < item.expires);
}
}
export class RequestCache extends MemoryRequestCache<DataQuery, QueryResults> {}
import { RecordingSegment } from './types';
class MemoryRangedCache<Data> {
protected _ranges: MemoryRangeSet = new MemoryRangeSet();
+2 -1
View File
@@ -2,8 +2,9 @@ import { ActionsExecutor } from '../card-controller/actions/types';
import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher';
import { PTZAction, PTZActionPhase } from '../config/schema/actions/custom/ptz';
import { CameraConfig } from '../config/schema/cameras';
import { isTriggeredState } from '../ha/is-triggered-state';
import { HassStateDifference } from '../ha/types';
import { localize } from '../localize/localize';
import { HassStateDifference, isTriggeredState } from '../utils/ha';
import { Capabilities } from './capabilities';
import { CameraManagerEngine } from './engine';
import { CameraNoIDError } from './error';
+10 -10
View File
@@ -1,14 +1,14 @@
import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher';
import { CameraConfig } from '../config/schema/cameras';
import { BrowseMediaWalker } from '../ha/browse-media/walker';
import { EntityRegistryManager } from '../ha/registry/entity/types';
import { ResolvedMediaCache } from '../ha/resolved-media';
import { HomeAssistant } from '../ha/types';
import { localize } from '../localize/localize';
import { BrowseMediaManager } from '../utils/ha/browse-media/browse-media-manager';
import { EntityRegistryManager } from '../utils/ha/registry/entity/types';
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
import { RecordingSegmentsCache, RequestCache } from './cache';
import { RecordingSegmentsCache } from './cache';
import { CameraManagerEngine } from './engine';
import { CameraInitializationError } from './error';
import { CameraEventCallback, Engine } from './types';
import { CameraEventCallback, CameraManagerRequestCache, Engine } from './types';
import { getCameraEntityFromConfig } from './utils/camera-entity-from-config';
interface CameraManagerEngineFactoryOptions {
@@ -44,7 +44,7 @@ export class CameraManagerEngineFactory {
this._entityRegistryManager,
options.stateWatcher,
new RecordingSegmentsCache(),
new RequestCache(),
new CameraManagerRequestCache(),
options.eventCallback,
);
break;
@@ -55,9 +55,9 @@ export class CameraManagerEngineFactory {
cameraManagerEngine = new MotionEyeCameraManagerEngine(
this._entityRegistryManager,
options.stateWatcher,
new BrowseMediaManager(),
new BrowseMediaWalker(),
options.resolvedMediaCache,
new RequestCache(),
new CameraManagerRequestCache(),
options.eventCallback,
);
break;
@@ -66,9 +66,9 @@ export class CameraManagerEngineFactory {
cameraManagerEngine = new ReolinkCameraManagerEngine(
this._entityRegistryManager,
options.stateWatcher,
new BrowseMediaManager(),
new BrowseMediaWalker(),
options.resolvedMediaCache,
new RequestCache(),
new CameraManagerRequestCache(),
options.eventCallback,
);
}
+7 -7
View File
@@ -1,15 +1,15 @@
import { CameraConfig } from '../config/schema/cameras';
import { HomeAssistant } from '../ha/types';
import { ViewMedia } from '../view/media';
import { Endpoint } from '../types';
import { ViewMedia } from '../view/item';
import { ViewItemCapabilities } from '../view/types';
import { Camera } from './camera';
import { CameraManagerReadOnlyConfigStore } from './store';
import {
CameraEndpoint,
CameraEndpoints,
CameraEndpointsContext,
CameraManagerCameraMetadata,
CameraManagerMediaCapabilities,
DataQuery,
CameraQuery,
Engine,
EngineOptions,
EventQuery,
@@ -90,7 +90,7 @@ export interface CameraManagerEngine {
hass: HomeAssistant,
cameraConfig: CameraConfig,
media: ViewMedia,
): Promise<CameraEndpoint | null>;
): Promise<Endpoint | null>;
favoriteMedia(
hass: HomeAssistant,
@@ -99,7 +99,7 @@ export interface CameraManagerEngine {
favorite: boolean,
): Promise<void>;
getQueryResultMaxAge(query: DataQuery): number | null;
getQueryResultMaxAge(query: CameraQuery): number | null;
getMediaSeekTime(
hass: HomeAssistant,
@@ -121,7 +121,7 @@ export interface CameraManagerEngine {
cameraConfig: CameraConfig,
): CameraManagerCameraMetadata;
getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null;
getMediaCapabilities(media: ViewMedia): ViewItemCapabilities | null;
getCameraEndpoints(
cameraConfig: CameraConfig,
+2 -2
View File
@@ -1,13 +1,13 @@
import uniq from 'lodash-es/uniq';
import { uniq } from 'lodash-es';
import { ActionsExecutor } from '../../card-controller/actions/types';
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz';
import { CameraConfig } from '../../config/schema/cameras';
import { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
import { HomeAssistant } from '../../ha/types';
import { localize } from '../../localize/localize';
import { PTZCapabilities, PTZMovementType } from '../../types';
import { errorToConsole } from '../../utils/basic';
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
import { Camera, CameraInitializationOptions } from '../camera';
import { Capabilities } from '../capabilities';
import { CameraManagerEngine } from '../engine';
+27 -28
View File
@@ -1,22 +1,21 @@
import { add, endOfHour, format, fromUnixTime, startOfHour } from 'date-fns';
import isEqual from 'lodash-es/isEqual';
import orderBy from 'lodash-es/orderBy';
import throttle from 'lodash-es/throttle';
import uniqWith from 'lodash-es/uniqWith';
import { isEqual, orderBy, throttle, uniqWith } from 'lodash-es';
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
import { CameraConfig } from '../../config/schema/cameras';
import { getEntityTitle } from '../../ha/get-entity-title';
import { EntityRegistryManager } from '../../ha/registry/entity/types';
import { HomeAssistant } from '../../ha/types';
import { Endpoint } from '../../types';
import {
allPromises,
formatDate,
prettifyTitle,
runWhenIdleIfSupported,
} from '../../utils/basic';
import { getEntityTitle } from '../../utils/ha';
import { EntityRegistryManager } from '../../utils/ha/registry/entity/types';
import { ViewMedia } from '../../view/media';
import { ViewMediaClassifier } from '../../view/media-classifier';
import { RecordingSegmentsCache, RequestCache } from '../cache';
import { ViewMedia, ViewMediaType } from '../../view/item';
import { ViewItemClassifier } from '../../view/item-classifier';
import { ViewItemCapabilities } from '../../view/types';
import { RecordingSegmentsCache } from '../cache';
import { Camera } from '../camera';
import {
CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
@@ -26,13 +25,12 @@ import { GenericCameraManagerEngine } from '../generic/engine-generic';
import { DateRange } from '../range';
import { CameraManagerReadOnlyConfigStore } from '../store';
import {
CameraEndpoint,
CameraEndpoints,
CameraEndpointsContext,
CameraEventCallback,
CameraManagerCameraMetadata,
CameraManagerMediaCapabilities,
DataQuery,
CameraManagerRequestCache,
CameraQuery,
Engine,
EngineOptions,
EventQuery,
@@ -112,7 +110,7 @@ export class FrigateCameraManagerEngine
protected _entityRegistryManager: EntityRegistryManager;
protected _frigateEventWatcher: FrigateEventWatcher;
protected _recordingSegmentsCache: RecordingSegmentsCache;
protected _requestCache: RequestCache;
protected _requestCache: CameraManagerRequestCache;
// Garbage collect segments at most once an hour.
protected _throttledSegmentGarbageCollector = throttle(
@@ -125,7 +123,7 @@ export class FrigateCameraManagerEngine
entityRegistryManager: EntityRegistryManager,
stateWatcher: StateWatcherSubscriptionInterface,
recordingSegmentsCache: RecordingSegmentsCache,
requestCache: RequestCache,
requestCache: CameraManagerRequestCache,
eventCallback?: CameraEventCallback,
) {
super(stateWatcher, eventCallback);
@@ -158,13 +156,13 @@ export class FrigateCameraManagerEngine
_hass: HomeAssistant,
cameraConfig: CameraConfig,
media: ViewMedia,
): Promise<CameraEndpoint | null> {
): Promise<Endpoint | null> {
if (FrigateViewMediaClassifier.isFrigateEvent(media)) {
return {
endpoint:
`/api/frigate/${cameraConfig.frigate.client_id}` +
`/notifications/${media.getID()}/` +
`${ViewMediaClassifier.isClip(media) ? 'clip.mp4' : 'snapshot.jpg'}` +
`${ViewItemClassifier.isClip(media) ? 'clip.mp4' : 'snapshot.jpg'}` +
`?download=true`,
sign: true,
};
@@ -541,7 +539,7 @@ export class FrigateCameraManagerEngine
protected _getCameraIDMatch(
store: CameraManagerReadOnlyConfigStore,
query: DataQuery,
query: CameraQuery,
instanceID: string,
cameraName: string,
): string | null {
@@ -588,17 +586,17 @@ export class FrigateCameraManagerEngine
if (!cameraConfig) {
continue;
}
let mediaType: 'clip' | 'snapshot' | null = null;
let mediaType: ViewMediaType | null = null;
if (
!query.hasClip &&
!query.hasSnapshot &&
(event.has_clip || event.has_snapshot)
) {
mediaType = event.has_clip ? 'clip' : 'snapshot';
mediaType = event.has_clip ? ViewMediaType.Clip : ViewMediaType.Snapshot;
} else if (query.hasSnapshot && event.has_snapshot) {
mediaType = 'snapshot';
mediaType = ViewMediaType.Snapshot;
} else if (query.hasClip && event.has_clip) {
mediaType = 'clip';
mediaType = ViewMediaType.Clip;
}
if (!mediaType) {
continue;
@@ -646,7 +644,7 @@ export class FrigateCameraManagerEngine
return output;
}
public getQueryResultMaxAge(query: DataQuery): number | null {
public getQueryResultMaxAge(query: CameraQuery): number | null {
if (query.type === QueryType.Event) {
return EVENT_REQUEST_CACHE_MAX_AGE_SECONDS;
} else if (query.type === QueryType.Recording) {
@@ -664,11 +662,12 @@ export class FrigateCameraManagerEngine
): Promise<number | null> {
const start = media.getStartTime();
const end = media.getEndTime();
if (!start || !end || target < start || target > end) {
const cameraID = media.getCameraID();
if (!start || !end || target < start || target > end || !cameraID) {
return null;
}
const cameraID = media.getCameraID();
const query: RecordingSegmentsQuery = {
cameraIDs: new Set([cameraID]),
start: start,
@@ -898,9 +897,9 @@ export class FrigateCameraManagerEngine
return seekMilliseconds / 1000;
}
public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities {
public getMediaCapabilities(media: ViewMedia): ViewItemCapabilities {
return {
canFavorite: ViewMediaClassifier.isEvent(media),
canFavorite: ViewItemClassifier.isEvent(media),
canDownload: true,
};
}
@@ -926,7 +925,7 @@ export class FrigateCameraManagerEngine
cameraConfig: CameraConfig,
context?: CameraEndpointsContext,
): CameraEndpoints | null {
const getUIEndpoint = (): CameraEndpoint | null => {
const getUIEndpoint = (): Endpoint | null => {
if (!cameraConfig.frigate.url) {
return null;
}
@@ -976,7 +975,7 @@ export class FrigateCameraManagerEngine
};
};
const getJSMPEG = (): CameraEndpoint | null => {
const getJSMPEG = (): Endpoint | null => {
return {
endpoint:
`/api/frigate/${cameraConfig.frigate.client_id}` +
@@ -53,6 +53,7 @@ export class FrigateEventWatcher implements FrigateEventWatcherSubscriptionInter
let json: unknown;
try {
json = JSON.parse(data);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
console.warn('Received non-JSON payload as Frigate event', data);
return;
@@ -1,4 +1,4 @@
import { ViewMedia } from '../../view/media';
import { ViewMedia } from '../../view/item';
import { FrigateEventViewMedia, FrigateRecordingViewMedia } from './media';
export class FrigateViewMediaClassifier {
+6 -8
View File
@@ -1,5 +1,5 @@
import { fromUnixTime } from 'date-fns';
import isEqual from 'lodash-es/isEqual';
import { isEqual } from 'lodash-es';
import { CameraConfig } from '../../config/schema/cameras';
import {
EventViewMedia,
@@ -7,7 +7,7 @@ import {
VideoContentType,
ViewMedia,
ViewMediaType,
} from '../../view/media';
} from '../../view/item';
import { FrigateEvent, FrigateRecording } from './types';
import {
getEventMediaContentID,
@@ -36,7 +36,7 @@ export class FrigateEventViewMedia extends ViewMedia implements EventViewMedia {
// sublabels (`_splitSubLabels` in engine-frigate.ts).
subLabels?: string[],
) {
super(mediaType, cameraID);
super(mediaType, { cameraID });
this._event = event;
this._contentID = contentID;
this._thumbnail = thumbnail;
@@ -88,8 +88,6 @@ export class FrigateEventViewMedia extends ViewMedia implements EventViewMedia {
public getTags(): string[] | null {
return this._subLabels;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public isGroupableWith(that: EventViewMedia): boolean {
return (
this.getMediaType() === that.getMediaType() &&
@@ -113,7 +111,7 @@ export class FrigateRecordingViewMedia extends ViewMedia implements RecordingVie
contentID: string,
title: string,
) {
super(mediaType, cameraID);
super(mediaType, { cameraID });
this._recording = recording;
this._id = id;
this._contentID = contentID;
@@ -150,7 +148,7 @@ export class FrigateRecordingViewMedia extends ViewMedia implements RecordingVie
export class FrigateViewMediaFactory {
static createEventViewMedia(
mediaType: 'clip' | 'snapshot',
mediaType: ViewMediaType,
cameraID: string,
cameraConfig: CameraConfig,
event: FrigateEvent,
@@ -191,7 +189,7 @@ export class FrigateViewMediaFactory {
}
return new FrigateRecordingViewMedia(
'recording',
ViewMediaType.Recording,
cameraID,
recording,
getRecordingID(cameraConfig, recording),
+1 -1
View File
@@ -1,7 +1,7 @@
import { HomeAssistant } from '../../ha/types';
import { homeAssistantWSRequest } from '../../ha/ws-request';
import { localize } from '../../localize/localize';
import { AdvancedCameraCardError } from '../../types';
import { homeAssistantWSRequest } from '../../utils/ha/ws-request';
import { RecordingSegment } from '../types';
import {
EventSummary,
+9 -10
View File
@@ -1,23 +1,22 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz';
import { CameraConfig } from '../../config/schema/cameras';
import { getEntityTitle } from '../../ha/get-entity-title';
import { HomeAssistant } from '../../ha/types';
import { getEntityTitle } from '../../utils/ha';
import { ViewMedia } from '../../view/media';
import { Endpoint } from '../../types';
import { ViewMedia } from '../../view/item';
import { ViewItemCapabilities } from '../../view/types';
import { Camera } from '../camera';
import { Capabilities } from '../capabilities';
import { CameraManagerEngine } from '../engine';
import { CameraManagerReadOnlyConfigStore } from '../store';
import {
CameraEndpoint,
CameraEndpoints,
CameraEndpointsContext,
CameraEventCallback,
CameraManagerCameraMetadata,
CameraManagerMediaCapabilities,
DataQuery,
CameraQuery,
Engine,
EngineOptions,
EventQuery,
@@ -155,7 +154,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
_hass: HomeAssistant,
_cameraConfig: CameraConfig,
_media: ViewMedia,
): Promise<CameraEndpoint | null> {
): Promise<Endpoint | null> {
return null;
}
@@ -168,7 +167,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
return;
}
public getQueryResultMaxAge(_query: DataQuery): number | null {
public getQueryResultMaxAge(_query: CameraQuery): number | null {
return null;
}
@@ -211,7 +210,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
};
}
public getMediaCapabilities(_media: ViewMedia): CameraManagerMediaCapabilities | null {
public getMediaCapabilities(_media: ViewMedia): ViewItemCapabilities | null {
return null;
}
@@ -219,7 +218,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
cameraConfig: CameraConfig,
_context?: CameraEndpointsContext,
): CameraEndpoints | null {
const getWebRTCCard = (): CameraEndpoint | null => {
const getWebRTCCard = (): Endpoint | null => {
// The user may override this in their webrtc_card configuration.
const endpoint = cameraConfig.camera_entity ? cameraConfig.camera_entity : null;
return endpoint ? { endpoint: endpoint } : null;
+37 -32
View File
@@ -1,12 +1,13 @@
import { add } from 'date-fns';
import cloneDeep from 'lodash-es/cloneDeep';
import sum from 'lodash-es/sum';
import { cloneDeep, sum } from 'lodash-es';
import PQueue from 'p-queue';
import { CardCameraAPI } from '../card-controller/types.js';
import { sortItems } from '../card-controller/view/sort.js';
import { PTZAction, PTZActionPhase } from '../config/schema/actions/custom/ptz.js';
import { CameraConfig, CamerasConfig } from '../config/schema/cameras.js';
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
import { localize } from '../localize/localize.js';
import { Endpoint } from '../types.js';
import {
allPromises,
arrayify,
@@ -16,19 +17,19 @@ import {
} from '../utils/basic.js';
import { getCameraID } from '../utils/camera.js';
import { log } from '../utils/debug.js';
import { ViewMedia } from '../view/media.js';
import { ViewItemClassifier } from '../view/item-classifier.js';
import { ViewItem, ViewMedia } from '../view/item.js';
import { ViewItemCapabilities } from '../view/types.js';
import { Capabilities } from './capabilities.js';
import { CameraManagerEngineFactory } from './engine-factory.js';
import { CameraManagerEngine } from './engine.js';
import { CameraInitializationError } from './error.js';
import { CameraManagerReadOnlyConfigStore, CameraManagerStore } from './store.js';
import {
CameraEndpoint,
CameraEndpoints,
CameraEndpointsContext,
CameraManagerCameraMetadata,
CameraManagerMediaCapabilities,
DataQuery,
CameraQuery,
Engine,
EngineOptions,
EventQuery,
@@ -38,7 +39,7 @@ import {
MediaMetadataQuery,
MediaMetadataQueryResults,
MediaQuery,
PartialDataQuery,
PartialCameraQuery,
PartialEventQuery,
PartialQueryConcreteType,
PartialRecordingQuery,
@@ -55,24 +56,25 @@ import {
RecordingSegmentsQueryResultsMap,
ResultsMap,
} from './types.js';
import { sortMedia } from './utils/sort-media.js';
export class QueryClassifier {
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
export class CameraQueryClassifier {
public static isEventQuery(
query: CameraQuery | PartialCameraQuery,
): query is EventQuery {
return query.type === QueryType.Event;
}
public static isRecordingQuery(
query: DataQuery | PartialDataQuery,
query: CameraQuery | PartialCameraQuery,
): query is RecordingQuery {
return query.type === QueryType.Recording;
}
public static isRecordingSegmentsQuery(
query: DataQuery | PartialDataQuery,
query: CameraQuery | PartialCameraQuery,
): query is RecordingSegmentsQuery {
return query.type === QueryType.RecordingSegments;
}
public static isMediaMetadataQuery(
query: DataQuery | PartialDataQuery,
query: CameraQuery | PartialCameraQuery,
): query is MediaMetadataQuery {
return query.type === QueryType.MediaMetadata;
}
@@ -103,7 +105,7 @@ export class QueryResultClassifier {
export interface ExtendedMediaQueryResult<T extends MediaQuery> {
queries: T[];
results: ViewMedia[];
results: ViewItem[];
}
export class CameraManager {
@@ -318,7 +320,7 @@ export class CameraManager {
});
}
protected _generateDefaultQueries<PQT extends PartialDataQuery>(
protected _generateDefaultQueries<PQT extends PartialCameraQuery>(
cameraIDs: string | Set<string>,
partialQuery: PQT,
): PartialQueryConcreteType<PQT>[] | null {
@@ -330,17 +332,17 @@ export class CameraManager {
}
for (const [engine, cameraIDs] of engines) {
let queries: DataQuery[] | null = null;
let queries: CameraQuery[] | null = null;
/* istanbul ignore else: the else path cannot be reached -- @preserve */
if (QueryClassifier.isEventQuery(partialQuery)) {
if (CameraQueryClassifier.isEventQuery(partialQuery)) {
queries = engine.generateDefaultEventQuery(this._store, cameraIDs, partialQuery);
} else if (QueryClassifier.isRecordingQuery(partialQuery)) {
} else if (CameraQueryClassifier.isRecordingQuery(partialQuery)) {
queries = engine.generateDefaultRecordingQuery(
this._store,
cameraIDs,
partialQuery,
);
} else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
} else if (CameraQueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
queries = engine.generateDefaultRecordingSegmentsQuery(
this._store,
cameraIDs,
@@ -426,7 +428,7 @@ export class CameraManager {
public async extendMediaQueries<T extends MediaQuery>(
queries: T[],
results: ViewMedia[],
results: ViewItem[],
direction: 'earlier' | 'later',
engineOptions?: EngineOptions,
): Promise<ExtendedMediaQueryResult<T> | null> {
@@ -438,6 +440,9 @@ export class CameraManager {
const getTimeFromResults = (want: 'earliest' | 'latest'): Date | null => {
let output: Date | null = null;
for (const result of results) {
if (!ViewItemClassifier.isMedia(result)) {
continue;
}
const startTime = result.getStartTime();
if (
startTime &&
@@ -495,7 +500,7 @@ export class CameraManager {
return null;
}
const outputMedia = sortMedia(results.concat(newChunkMedia));
const outputMedia = sortItems(results.concat(newChunkMedia));
// If the media did not _ACTUALLY_ get longer, there is no new media despite
// the increased limit, so just return null.
@@ -509,7 +514,7 @@ export class CameraManager {
};
}
public async getMediaDownloadPath(media: ViewMedia): Promise<CameraEndpoint | null> {
public async getMediaDownloadPath(media: ViewMedia): Promise<Endpoint | null> {
const cameraConfig = this._store.getCameraConfigForMedia(media);
const engine = this._store.getEngineForMedia(media);
const hass = this._api.getHASSManager().getHASS();
@@ -520,7 +525,7 @@ export class CameraManager {
return await engine.getMediaDownloadPath(hass, cameraConfig, media);
}
public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null {
public getMediaCapabilities(media: ViewMedia): ViewItemCapabilities | null {
const engine = this._store.getEngineForMedia(media);
if (!engine) {
return null;
@@ -603,7 +608,7 @@ export class CameraManager {
);
}
protected async _handleQuery<QT extends DataQuery>(
protected async _handleQuery<QT extends CameraQuery>(
query: QT | QT[],
engineOptions?: EngineOptions,
): Promise<Map<QT, QueryReturnType<QT>>> {
@@ -623,28 +628,28 @@ export class CameraManager {
let engineResult: Map<QT, QueryReturnType<QT>> | null = null;
/* istanbul ignore else: the else path cannot be reached -- @preserve */
if (QueryClassifier.isEventQuery(query)) {
if (CameraQueryClassifier.isEventQuery(query)) {
engineResult = (await engine.getEvents(
hass,
this._store,
query,
engineOptions,
)) as Map<QT, QueryReturnType<QT>> | null;
} else if (QueryClassifier.isRecordingQuery(query)) {
} else if (CameraQueryClassifier.isRecordingQuery(query)) {
engineResult = (await engine.getRecordings(
hass,
this._store,
query,
engineOptions,
)) as Map<QT, QueryReturnType<QT>> | null;
} else if (QueryClassifier.isRecordingSegmentsQuery(query)) {
} else if (CameraQueryClassifier.isRecordingSegmentsQuery(query)) {
engineResult = (await engine.getRecordingSegments(
hass,
this._store,
query,
engineOptions,
)) as Map<QT, QueryReturnType<QT>> | null;
} else if (QueryClassifier.isMediaMetadataQuery(query)) {
} else if (CameraQueryClassifier.isMediaMetadataQuery(query)) {
engineResult = (await engine.getMediaMetadata(
hass,
this._store,
@@ -697,7 +702,7 @@ export class CameraManager {
return results;
}
protected _convertQueryResultsToMedia<QT extends DataQuery>(
protected _convertQueryResultsToMedia<QT extends CameraQuery>(
results: ResultsMap<QT>,
): ViewMedia[] {
const mediaArray: ViewMedia[] = [];
@@ -714,12 +719,12 @@ export class CameraManager {
let media: ViewMedia[] | null = null;
/* istanbul ignore else: the else path cannot be reached -- @preserve */
if (
QueryClassifier.isEventQuery(query) &&
CameraQueryClassifier.isEventQuery(query) &&
QueryResultClassifier.isEventQueryResult(result)
) {
media = engine.generateMediaFromEvents(hass, this._store, query, result);
} else if (
QueryClassifier.isRecordingQuery(query) &&
CameraQueryClassifier.isRecordingQuery(query) &&
QueryResultClassifier.isRecordingQueryResult(result)
) {
media = engine.generateMediaFromRecordings(hass, this._store, query, result);
@@ -729,7 +734,7 @@ export class CameraManager {
}
}
}
return sortMedia(mediaArray);
return sortItems(mediaArray);
}
public getCameraEndpoints(
@@ -1,32 +1,29 @@
import { add, endOfDay, parse, startOfDay } from 'date-fns';
import orderBy from 'lodash-es/orderBy';
import { orderBy } from 'lodash-es';
import { CameraConfig } from '../../config/schema/cameras';
import { HomeAssistant } from '../../ha/types';
import { allPromises, formatDate, isValidDate } from '../../utils/basic';
import {
BrowseMediaStep,
BrowseMediaTarget,
} from '../../utils/ha/browse-media/browse-media-manager';
import { getViewMediaFromBrowseMediaArray } from '../../ha/browse-media/browse-media-to-view-media';
import {
BROWSE_MEDIA_CACHE_SECONDS,
BrowseMedia,
BrowseMediaCache,
BrowseMediaMetadata,
MEDIA_CLASS_IMAGE,
MEDIA_CLASS_VIDEO,
RichBrowseMedia,
} from '../../utils/ha/browse-media/types';
import { ViewMedia } from '../../view/media';
} from '../../ha/browse-media/types';
import { BrowseMediaStep, BrowseMediaTarget } from '../../ha/browse-media/walker';
import { isMediaWithinDates } from '../../ha/browse-media/within-dates';
import { HomeAssistant } from '../../ha/types';
import { Endpoint } from '../../types';
import { allPromises, formatDate, isValidDate } from '../../utils/basic';
import { ViewMedia } from '../../view/item';
import { BrowseMediaCamera } from '../browse-media/camera';
import { BrowseMediaCameraManagerEngine } from '../browse-media/engine-browse-media';
import { BrowseMediaMetadata } from '../browse-media/types';
import { getViewMediaFromBrowseMediaArray } from '../browse-media/utils/browse-media-to-view-media';
import { isMediaWithinDates } from '../browse-media/utils/within-dates';
import { MemoryRequestCache } from '../cache';
import { Camera } from '../camera';
import { Capabilities } from '../capabilities';
import { CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT } from '../engine';
import { CameraManagerReadOnlyConfigStore } from '../store';
import {
CameraEndpoint,
CameraEndpoints,
CameraEndpointsContext,
CameraManagerCameraMetadata,
@@ -67,8 +64,8 @@ const MOTIONEYE_REPL_SUBSTITUTIONS: Record<string, string> = {
const MOTIONEYE_REPL_REGEXP = new RegExp(/(%Y|%m|%d|%H|%M|%S)/g);
export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine {
protected _directoryCache = new MemoryRequestCache<string, BrowseMedia>();
protected _fileCache = new MemoryRequestCache<string, BrowseMedia>();
protected _directoryCache = new BrowseMediaCache<BrowseMediaMetadata>();
protected _fileCache = new BrowseMediaCache<BrowseMediaMetadata>();
public getEngineType(): Engine {
return Engine.MotionEye;
@@ -224,7 +221,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
};
// For motionEye snapshots and clips are mutually exclusive.
return await this._browseMediaManager.walkBrowseMedias(
return await this._browseMediaWalker.walk(
hass,
[
...(matchOptions?.hasClip !== false && !matchOptions?.hasSnapshot
@@ -291,7 +288,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
);
const limit = perCameraQuery.limit ?? CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT;
const media = await this._browseMediaManager.walkBrowseMedias(
const media = await this._browseMediaWalker.walk(
hass,
[
{
@@ -431,7 +428,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
cameraConfig: CameraConfig,
context?: CameraEndpointsContext,
): CameraEndpoints | null {
const getUIEndpoint = (): CameraEndpoint | null => {
const getUIEndpoint = (): Endpoint | null => {
return cameraConfig.motioneye?.url
? {
endpoint: cameraConfig.motioneye.url,
+1 -2
View File
@@ -1,5 +1,4 @@
import { RichBrowseMedia } from '../../utils/ha/browse-media/types';
import { BrowseMediaMetadata } from '../browse-media/types';
import { BrowseMediaMetadata, RichBrowseMedia } from '../../ha/browse-media/types';
import { Engine, EventQueryResults } from '../types';
// ================================
+1 -1
View File
@@ -1,4 +1,4 @@
import orderBy from 'lodash-es/orderBy';
import { orderBy } from 'lodash-es';
interface Range<T extends Date | number> {
start: T;
+1 -1
View File
@@ -1,10 +1,10 @@
import { ActionsExecutor } from '../../card-controller/actions/types';
import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz';
import { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
import { HomeAssistant } from '../../ha/types';
import { localize } from '../../localize/localize';
import { PTZCapabilities, PTZMovementType } from '../../types';
import { createSelectOptionAction } from '../../utils/action.js';
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
import { BrowseMediaCamera } from '../browse-media/camera';
import { Camera, CameraInitializationOptions } from '../camera';
import { Capabilities } from '../capabilities';
+17 -16
View File
@@ -1,26 +1,26 @@
import { add, endOfDay, parse, startOfDay } from 'date-fns';
import { orderBy } from 'lodash-es';
import { CameraConfig } from '../../config/schema/cameras';
import { HomeAssistant } from '../../ha/types';
import { allPromises, formatDate, isValidDate } from '../../utils/basic';
import { sortMediaByStartDate } from '../../utils/ha/browse-media/browse-media-manager';
import { getViewMediaFromBrowseMediaArray } from '../../ha/browse-media/browse-media-to-view-media';
import { sortMediaByStartDate } from '../../ha/browse-media/sort-browse-media-by-start-date';
import {
BROWSE_MEDIA_CACHE_SECONDS,
BrowseMedia,
BrowseMediaCache,
BrowseMediaMetadata,
MEDIA_CLASS_VIDEO,
RichBrowseMedia,
} from '../../utils/ha/browse-media/types';
import { ViewMedia } from '../../view/media';
} from '../../ha/browse-media/types';
import { isMediaWithinDates } from '../../ha/browse-media/within-dates';
import { HomeAssistant } from '../../ha/types';
import { Endpoint } from '../../types';
import { allPromises, formatDate, isValidDate } from '../../utils/basic';
import { ViewMedia } from '../../view/item';
import { BrowseMediaCameraManagerEngine } from '../browse-media/engine-browse-media';
import { BrowseMediaMetadata } from '../browse-media/types';
import { getViewMediaFromBrowseMediaArray } from '../browse-media/utils/browse-media-to-view-media';
import { isMediaWithinDates } from '../browse-media/utils/within-dates';
import { MemoryRequestCache } from '../cache';
import { Camera } from '../camera';
import { CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT } from '../engine';
import { CameraManagerReadOnlyConfigStore } from '../store';
import {
CameraEndpoint,
CameraEndpoints,
CameraEndpointsContext,
CameraManagerCameraMetadata,
@@ -48,7 +48,8 @@ export class ReolinkQueryResultsClassifier {
}
export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
protected _cache = new MemoryRequestCache<string, BrowseMedia>();
protected _camerasCache = new BrowseMediaCache<BrowseMediaReolinkCameraMetadata>();
protected _cache = new BrowseMediaCache<BrowseMediaMetadata>();
public getEngineType(): Engine {
return Engine.Reolink;
@@ -172,7 +173,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
// that match the expected camera. Some Reolink cameras will not show up
// here causing errors.
// https://github.com/dermotduffy/advanced-camera-card/issues/1723
const camerasWithMedia = await this._browseMediaManager.walkBrowseMedias(
const camerasWithMedia = await this._browseMediaWalker.walk(
hass,
[
{
@@ -188,7 +189,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
},
],
{
...(engineOptions?.useCache !== false && { cache: this._cache }),
...(engineOptions?.useCache !== false && { cache: this._camerasCache }),
},
);
@@ -196,7 +197,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
return null;
}
return await this._browseMediaManager.walkBrowseMedias(
return await this._browseMediaWalker.walk(
hass,
[
{
@@ -263,7 +264,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
let media: RichBrowseMedia<BrowseMediaMetadata>[] = [];
if (directories?.length) {
media = await this._browseMediaManager.walkBrowseMedias(
media = await this._browseMediaWalker.walk(
hass,
[
{
@@ -395,7 +396,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
cameraConfig: CameraConfig,
context?: CameraEndpointsContext,
): CameraEndpoints | null {
const getUIEndpoint = (): CameraEndpoint | null => {
const getUIEndpoint = (): Endpoint | null => {
return cameraConfig.reolink?.url
? {
endpoint: cameraConfig.reolink.url,
+1 -2
View File
@@ -1,5 +1,4 @@
import { RichBrowseMedia } from '../../utils/ha/browse-media/types';
import { BrowseMediaMetadata } from '../browse-media/types';
import { BrowseMediaMetadata, RichBrowseMedia } from '../../ha/browse-media/types';
import { Engine, EventQueryResults } from '../types';
export interface BrowseMediaReolinkCameraMetadata {
+5 -3
View File
@@ -1,7 +1,7 @@
import { CameraConfig } from '../config/schema/cameras';
import { CapabilityKey } from '../types';
import { allPromises } from '../utils/basic';
import { ViewMedia } from '../view/media';
import { ViewMedia } from '../view/item';
import { Camera } from './camera';
import { CameraManagerEngine } from './engine';
import { CapabilitySearchOptions, Engine } from './types';
@@ -133,7 +133,8 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
}
public getCameraConfigForMedia(media: ViewMedia): CameraConfig | null {
return this.getCameraConfig(media.getCameraID());
const cameraID = media.getCameraID();
return cameraID ? this.getCameraConfig(cameraID) : null;
}
public getEngineOfType(engine: Engine): CameraManagerEngine | null {
@@ -163,7 +164,8 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
}
public getEngineForMedia(media: ViewMedia): CameraManagerEngine | null {
return this.getEngineForCameraID(media.getCameraID());
const cameraID = media.getCameraID();
return cameraID ? this.getEngineForCameraID(cameraID) : null;
}
/**
+17 -21
View File
@@ -1,7 +1,8 @@
import { ExpiringEqualityCache } from '../cache/expiring-cache';
import { SSLCiphers } from '../config/schema/cameras';
import { AdvancedCameraCardView } from '../config/schema/common/const';
import { CapabilityKey, Icon } from '../types';
import { ViewMedia } from '../view/media';
import { CapabilityKey, Endpoint, Icon } from '../types';
import { ViewMedia } from '../view/item';
// ====
// Base
@@ -28,11 +29,11 @@ export enum Engine {
Reolink = 'reolink',
}
export interface DataQuery {
export interface CameraQuery {
type: QueryType;
cameraIDs: Set<string>;
}
export type PartialDataQuery = Partial<DataQuery>;
export type PartialCameraQuery = Partial<CameraQuery>;
interface TimeBasedDataQuery {
start: Date;
@@ -44,7 +45,7 @@ interface LimitedDataQuery {
}
export interface MediaQuery
extends DataQuery,
extends CameraQuery,
Partial<TimeBasedDataQuery>,
Partial<LimitedDataQuery> {
favorite?: boolean;
@@ -100,11 +101,6 @@ interface CapabilitySearchAllAny {
}
export type CapabilitySearchOptions = CapabilityKey | CapabilitySearchAllAny;
export interface CameraManagerMediaCapabilities {
canFavorite: boolean;
canDownload: boolean;
}
export interface CameraManagerCameraMetadata {
title: string;
icon: Icon;
@@ -118,16 +114,11 @@ export interface CameraEndpointsContext {
view?: AdvancedCameraCardView;
}
export interface CameraEndpoint {
endpoint: string;
sign?: boolean;
}
export interface CameraEndpoints {
ui?: CameraEndpoint;
go2rtc?: CameraEndpoint;
jsmpeg?: CameraEndpoint;
webrtcCard?: CameraEndpoint;
ui?: Endpoint;
go2rtc?: Endpoint;
jsmpeg?: Endpoint;
webrtcCard?: Endpoint;
}
export interface CameraProxyConfig {
@@ -157,6 +148,11 @@ export interface CameraEvent {
}
export type CameraEventCallback = (ev: CameraEvent) => void;
export class CameraManagerRequestCache extends ExpiringEqualityCache<
CameraQuery,
QueryResults
> {}
// ===========
// Event Query
// ===========
@@ -202,7 +198,7 @@ export interface RecordingQueryResults extends QueryResults {
// Recording Segments Query
// ========================
export interface RecordingSegmentsQuery extends DataQuery, TimeBasedDataQuery {
export interface RecordingSegmentsQuery extends CameraQuery, TimeBasedDataQuery {
type: QueryType.RecordingSegments;
}
export type PartialRecordingSegmentsQuery = Partial<RecordingSegmentsQuery>;
@@ -216,7 +212,7 @@ export interface RecordingSegmentsQueryResults extends QueryResults {
// Media metadata Query
// ====================
export interface MediaMetadataQuery extends DataQuery {
export interface MediaMetadataQuery extends CameraQuery {
type: QueryType.MediaMetadata;
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { CameraConfig } from '../../config/schema/cameras';
import { CameraEndpoint } from '../types';
import { Endpoint } from '../../types';
export const getDefaultGo2RTCEndpoint = (
cameraConfig: CameraConfig,
@@ -7,7 +7,7 @@ export const getDefaultGo2RTCEndpoint = (
url?: string;
stream?: string;
},
): CameraEndpoint | null => {
): Endpoint | null => {
const url = options?.url ?? cameraConfig.go2rtc?.url;
const stream = options?.stream ?? cameraConfig.go2rtc?.stream;
-16
View File
@@ -1,16 +0,0 @@
import orderBy from 'lodash-es/orderBy';
import uniqBy from 'lodash-es/uniqBy';
import { ViewMedia } from '../../view/media';
export const sortMedia = (mediaArray: ViewMedia[]): ViewMedia[] => {
return orderBy(
// Ensure uniqueness by the ID (if specified), otherwise all elements
// are assumed to be unique.
uniqBy(mediaArray, (media) => media.getID() ?? media),
// Sort all items leading oldest -> youngest (so media is loaded in this
// order in the viewer which matches the left-to-right timeline order).
(media) => media.getStartTime() ?? media.getID(),
'asc',
);
};