fix: Reolink metadata should not reject media with labels (#1900)

- Related to: #1870
This commit is contained in:
Dermot Duffy
2025-02-17 20:13:21 -08:00
committed by GitHub
parent c66c3ec919
commit bb147a45fd
6 changed files with 128 additions and 33 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ class BrowseMediaEventViewMedia extends ViewMedia implements EventViewMedia {
return this._browseMedia.thumbnail; return this._browseMedia.thumbnail;
} }
public getWhat(): string[] | null { public getWhat(): string[] | null {
return null; return this._browseMedia._metadata?.what ?? null;
} }
public getScore(): number | null { public getScore(): number | null {
return null; return null;
+1
View File
@@ -2,4 +2,5 @@ export interface BrowseMediaMetadata {
cameraID: string; cameraID: string;
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
what?: string[];
} }
+18 -7
View File
@@ -68,20 +68,22 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
return null; return null;
} }
// Title of the form "21:47:03 0:00:44" // Titles may be of the form:
// - "21:47:03 0:00:44"
// - "21:47:03 0:00:44 Person" (https://github.com/dermotduffy/advanced-camera-card/issues/1870)
// - "21:47:03 0:00:44 Vehicle Person" (https://github.com/dermotduffy/advanced-camera-card/issues/1870)
const parts = media.title.split(/ +/); const parts = media.title.split(/ +/);
if (parts.length !== 2) {
return null;
}
const startDate = parse(parts[0], 'HH:mm:ss', parent._metadata.startDate); const startDate = parse(parts[0], 'HH:mm:ss', parent._metadata.startDate);
if (!isValidDate(startDate)) { if (!isValidDate(startDate)) {
return null; return null;
} }
const durationMatch = parts[1].match( const durationMatch =
/(?<hours>\d+):(?<minutes>\d+):(?<seconds>\d+)/, parts.length > 1
); ? parts[1].match(/(?<hours>\d+):(?<minutes>\d+):(?<seconds>\d+)/)
: null;
const duration = durationMatch?.groups const duration = durationMatch?.groups
? { ? {
hours: Number(durationMatch.groups.hours), hours: Number(durationMatch.groups.hours),
@@ -90,10 +92,19 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
} }
: null; : null;
const what =
parts.length > 2
? parts
.splice(2)
.map((l) => l.toLowerCase())
.sort()
: null;
return { return {
cameraID: cameraID, cameraID: cameraID,
startDate: startDate, startDate: startDate,
endDate: duration ? add(startDate, duration) : startDate, endDate: duration ? add(startDate, duration) : startDate,
...(what && { what: what }),
}; };
} }
+1 -1
View File
@@ -382,7 +382,7 @@ export const CONF_PERFORMANCE_STYLE_BORDER_RADIUS = `${CONF_PERFORMANCE}.style.b
export const CONF_PROFILES = 'profiles' as const; export const CONF_PROFILES = 'profiles' as const;
export const CONF_REMOTE_CONTROL = 'remote_control' as const; const CONF_REMOTE_CONTROL = 'remote_control' as const;
export const CONF_REMOTE_CONTROL_ENTITIES_CAMERA = export const CONF_REMOTE_CONTROL_ENTITIES_CAMERA =
`${CONF_REMOTE_CONTROL}.entities.camera` as const; `${CONF_REMOTE_CONTROL}.entities.camera` as const;
@@ -143,6 +143,28 @@ const TEST_FILES: BrowseMedia = {
can_expand: false, can_expand: false,
thumbnail: null, thumbnail: null,
}, },
{
title: '21:35:05 0:00:41 Zebra Person Vehicle',
media_class: 'video',
media_content_type: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052905_211_S.mp4',
children_media_class: null,
can_play: true,
can_expand: false,
thumbnail: null,
},
{
title: '21:37:00',
media_class: 'video',
media_content_type: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052905_211_S.mp4',
children_media_class: null,
can_play: true,
can_expand: false,
thumbnail: null,
},
{ {
title: '22:04:49 0:00:35', title: '22:04:49 0:00:35',
media_class: 'video', media_class: 'video',
@@ -363,6 +385,39 @@ describe('ReolinkCameraManagerEngine', () => {
}, },
{ {
browseMedia: [ browseMedia: [
{
_metadata: {
cameraID: 'office',
startDate: new Date('2024-11-04T21:37:00.000Z'),
endDate: new Date('2024-11-04T21:37:00.000Z'),
},
can_expand: false,
can_play: true,
children_media_class: null,
media_class: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052905_211_S.mp4',
media_content_type: 'video',
thumbnail: null,
title: '21:37:00',
},
{
_metadata: {
cameraID: 'office',
endDate: new Date('2024-11-04T21:35:46.000Z'),
startDate: new Date('2024-11-04T21:35:05.000Z'),
what: ['person', 'vehicle', 'zebra'],
},
can_expand: false,
can_play: true,
children_media_class: null,
media_class: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052905_211_S.mp4',
media_content_type: 'video',
thumbnail: null,
title: '21:35:05 0:00:41 Zebra Person Vehicle',
},
{ {
_metadata: { _metadata: {
cameraID: 'office', cameraID: 'office',
@@ -720,8 +775,8 @@ describe('ReolinkCameraManagerEngine', () => {
thumbnail: null, thumbnail: null,
children: [ children: [
{ {
// Title not matching format. // Title has invalid title (should be timestamp).
title: '21:23:53 0:00:34 _____MALFORMED_____', title: 'AA:BB:CC',
media_class: 'video', media_class: 'video',
media_content_type: 'video', media_content_type: 'video',
media_content_id: media_content_id:
@@ -1,6 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended'; import { mock } from 'vitest-mock-extended';
import { CardController } from '../../../../src/card-controller/controller';
import { WebkitFullScreenProvider } from '../../../../src/card-controller/fullscreen/webkit'; import { WebkitFullScreenProvider } from '../../../../src/card-controller/fullscreen/webkit';
import { ConditionStateManager } from '../../../../src/conditions/state-manager'; import { ConditionStateManager } from '../../../../src/conditions/state-manager';
import { import {
@@ -14,21 +13,19 @@ const createWebkitVideoElement = (): HTMLVideoElement &
return document.createElement('video'); return document.createElement('video');
}; };
const setElement = (api: CardController, element: HTMLElement): void => { const createPlayer = (element: HTMLElement): AdvancedCameraCardMediaPlayer => {
const player = mock<AdvancedCameraCardMediaPlayer>(); const player = mock<AdvancedCameraCardMediaPlayer>();
player.getFullscreenElement.mockReturnValue(element); player.getFullscreenElement.mockReturnValue(element);
return player;
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
}; };
// @vitest-environment jsdom // @vitest-environment jsdom
describe('WebkitFullScreenProvider', () => { describe('WebkitFullScreenProvider', () => {
afterEach(() => { beforeAll(() => {
vi.restoreAllMocks(); vi.useFakeTimers();
});
afterAll(() => {
vi.useRealTimers(); vi.useRealTimers();
}); });
@@ -60,7 +57,12 @@ describe('WebkitFullScreenProvider', () => {
const element = createWebkitVideoElement(); const element = createWebkitVideoElement();
element.webkitDisplayingFullscreen = fullscreen; element.webkitDisplayingFullscreen = fullscreen;
setElement(api, element); const player = createPlayer(element);
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
expect(provider.isInFullscreen()).toBe(fullscreen); expect(provider.isInFullscreen()).toBe(fullscreen);
}); });
@@ -74,7 +76,12 @@ describe('WebkitFullScreenProvider', () => {
const element = createWebkitVideoElement(); const element = createWebkitVideoElement();
element.webkitSupportsFullscreen = supported; element.webkitSupportsFullscreen = supported;
setElement(api, element); const player = createPlayer(element);
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
expect(provider.isSupported()).toBe(supported); expect(provider.isSupported()).toBe(supported);
}); });
@@ -89,7 +96,12 @@ describe('WebkitFullScreenProvider', () => {
element.webkitEnterFullscreen = vi.fn(); element.webkitEnterFullscreen = vi.fn();
element.webkitSupportsFullscreen = true; element.webkitSupportsFullscreen = true;
setElement(api, element); const player = createPlayer(element);
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
provider.setFullscreen(true); provider.setFullscreen(true);
@@ -104,7 +116,12 @@ describe('WebkitFullScreenProvider', () => {
element.webkitExitFullscreen = vi.fn(); element.webkitExitFullscreen = vi.fn();
element.webkitSupportsFullscreen = true; element.webkitSupportsFullscreen = true;
setElement(api, element); const player = createPlayer(element);
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
provider.setFullscreen(false); provider.setFullscreen(false);
@@ -120,7 +137,12 @@ describe('WebkitFullScreenProvider', () => {
element.webkitExitFullscreen = vi.fn(); element.webkitExitFullscreen = vi.fn();
element.webkitSupportsFullscreen = false; element.webkitSupportsFullscreen = false;
setElement(api, element); const player = createPlayer(element);
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
provider.setFullscreen(true); provider.setFullscreen(true);
provider.setFullscreen(false); provider.setFullscreen(false);
@@ -138,7 +160,12 @@ describe('WebkitFullScreenProvider', () => {
element.webkitEnterFullscreen = vi.fn(); element.webkitEnterFullscreen = vi.fn();
element.webkitExitFullscreen = vi.fn(); element.webkitExitFullscreen = vi.fn();
setElement(api, element); const player = createPlayer(element);
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
provider.setFullscreen(true); provider.setFullscreen(true);
provider.setFullscreen(false); provider.setFullscreen(false);
@@ -204,8 +231,6 @@ describe('WebkitFullScreenProvider', () => {
}); });
it('should play the video after fullscreen ends', () => { it('should play the video after fullscreen ends', () => {
vi.useFakeTimers();
const handler = vi.fn(); const handler = vi.fn();
const api = createCardAPI(); const api = createCardAPI();
const stateManager = new ConditionStateManager(); const stateManager = new ConditionStateManager();
@@ -218,10 +243,13 @@ describe('WebkitFullScreenProvider', () => {
const element = createWebkitVideoElement(); const element = createWebkitVideoElement();
element.play = vi.fn(); element.play = vi.fn();
setElement(api, element); const player = createPlayer(element);
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
const player = mock<AdvancedCameraCardMediaPlayer>();
player.getFullscreenElement.mockReturnValue(element);
const mediaLoadedInfo = createMediaLoadedInfo({ player }); const mediaLoadedInfo = createMediaLoadedInfo({ player });
stateManager.setState({ mediaLoadedInfo }); stateManager.setState({ mediaLoadedInfo });