diff --git a/src/camera-manager/reolink/engine-reolink.ts b/src/camera-manager/reolink/engine-reolink.ts index 7ff0dba1..bd76e3b6 100644 --- a/src/camera-manager/reolink/engine-reolink.ts +++ b/src/camera-manager/reolink/engine-reolink.ts @@ -2,6 +2,7 @@ import { add, endOfDay, parse, startOfDay } from 'date-fns'; import { orderBy } from 'lodash-es'; import { CameraConfig } from '../../config/schema/cameras'; import { getViewMediaFromBrowseMediaArray } from '../../ha/browse-media/browse-media-to-view-media'; +import { sortMostRecentFirst } from '../../ha/browse-media/sort'; import { BROWSE_MEDIA_CACHE_SECONDS, BrowseMedia, @@ -212,6 +213,8 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine { matcher: (media: RichBrowseMedia) => media.can_expand && isMediaWithinDates(media, matchOptions?.start, matchOptions?.end), + sorter: (media: RichBrowseMedia[]) => + sortMostRecentFirst(media), }, ], { @@ -275,6 +278,8 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine { matcher: (media: RichBrowseMedia) => !media.can_expand && isMediaWithinDates(media, perCameraQuery.start, perCameraQuery.end), + sorter: (media: RichBrowseMedia[]) => + sortMostRecentFirst(media), }, ], { diff --git a/src/ha/browse-media/sort.ts b/src/ha/browse-media/sort.ts new file mode 100644 index 00000000..1504b2ee --- /dev/null +++ b/src/ha/browse-media/sort.ts @@ -0,0 +1,15 @@ +import { orderBy } from 'lodash-es'; +import { BrowseMediaMetadata, RichBrowseMedia } from './types'; + +// Unlike sorting of view items (see card-controller/view/sort.ts), for browse +// media we often need to sort by most recent first to apply an item count +// cutoff from the most recent (this differs from how items may be sorted prior +// to presentation). +// +// See: https://github.com/dermotduffy/advanced-camera-card/issues/2078 + +export const sortMostRecentFirst = ( + media: RichBrowseMedia[], +): RichBrowseMedia[] => { + return orderBy(media, (media) => media._metadata?.startDate, 'desc'); +}; diff --git a/tests/ha/browse-media/sort.test.ts b/tests/ha/browse-media/sort.test.ts new file mode 100644 index 00000000..36d87e07 --- /dev/null +++ b/tests/ha/browse-media/sort.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest'; +import { + BrowseMediaMetadata, + RichBrowseMedia, +} from '../../../src/ha/browse-media/types'; +import { createBrowseMedia, createRichBrowseMedia } from '../../test-utils'; +import { sortMostRecentFirst } from '../../../src/ha/browse-media/sort'; + +const createMetadata = ( + metadata: Partial, +): BrowseMediaMetadata => ({ + cameraID: 'camera.office', + startDate: new Date('2025-05-10T20:22:00Z'), + endDate: new Date('2025-05-10T20:22:10Z'), + ...metadata, +}); + +describe('sortMostRecentFirst', () => { + it('should return an empty array when given an empty array', () => { + const media: RichBrowseMedia[] = []; + expect(sortMostRecentFirst(media)).toEqual([]); + }); + + it('should sort media by startDate in descending order', () => { + const media: RichBrowseMedia[] = [ + createRichBrowseMedia({ + title: 'Media 1', + _metadata: createMetadata({ startDate: new Date('2025-05-10T20:29:00.000Z') }), + }), + createRichBrowseMedia({ + title: 'Media 2', + _metadata: createMetadata({ startDate: new Date('2025-05-12T20:29:00.000Z') }), + }), + createRichBrowseMedia({ + title: 'Media 3', + _metadata: createMetadata({ startDate: new Date('2025-05-11T20:29:00.000Z') }), + }), + ]; + const expected: RichBrowseMedia[] = [ + createRichBrowseMedia({ + title: 'Media 2', + _metadata: createMetadata({ startDate: new Date('2025-05-12T20:29:00.000Z') }), + }), + createRichBrowseMedia({ + title: 'Media 3', + _metadata: createMetadata({ startDate: new Date('2025-05-11T20:29:00.000Z') }), + }), + createRichBrowseMedia({ + title: 'Media 1', + _metadata: createMetadata({ startDate: new Date('2025-05-10T20:29:00.000Z') }), + }), + ]; + expect(sortMostRecentFirst(media)).toEqual(expected); + }); + + it('should handle items with undefined startDate by placing them at the end', () => { + const media: RichBrowseMedia[] = [ + createRichBrowseMedia({ + title: 'Media 1', + _metadata: createMetadata({ startDate: new Date('2025-05-10T20:29:00.000Z') }), + }), + createBrowseMedia({ + title: 'Media 2', + }), + createRichBrowseMedia({ + title: 'Media 3', + _metadata: createMetadata({ startDate: new Date('2025-05-12T20:29:00.000Z') }), + }), + createBrowseMedia({ + title: 'Media 4', + }), + ]; + const expected: RichBrowseMedia[] = [ + createBrowseMedia({ + title: 'Media 2', + }), + createBrowseMedia({ + title: 'Media 4', + }), + createRichBrowseMedia({ + title: 'Media 3', + _metadata: createMetadata({ startDate: new Date('2025-05-12T20:29:00.000Z') }), + }), + createRichBrowseMedia({ + title: 'Media 1', + _metadata: createMetadata({ startDate: new Date('2025-05-10T20:29:00.000Z') }), + }), + ]; + + expect(sortMostRecentFirst(media)).toEqual(expected); + }); +});