diff --git a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte index e59897018f..0f1f744a9e 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte @@ -105,7 +105,8 @@ {/if} {/await} - - {getExifCount(asset)} {$t('exif')} + {getExifCount(asset)} + {$t('exif')} diff --git a/web/src/lib/utils/duplicate-utils.spec.ts b/web/src/lib/utils/duplicate-utils.spec.ts index 641b8b697c..4fa427989a 100644 --- a/web/src/lib/utils/duplicate-utils.spec.ts +++ b/web/src/lib/utils/duplicate-utils.spec.ts @@ -1,45 +1,37 @@ -import { suggestDuplicate } from '$lib/utils/duplicate-utils'; -import type { AssetResponseDto } from '@immich/sdk'; - -describe('choosing a duplicate', () => { - - it('picks the asset with the largest file size', () => { - const assets = [ - { exifInfo: { fileSizeInByte: 300 } }, - { exifInfo: { fileSizeInByte: 200 } }, - { exifInfo: { fileSizeInByte: 100 } }, - ]; - expect(suggestDuplicate(assets as AssetResponseDto[])).toEqual(assets[0]); - }); - - it('picks the asset with the most exif data if multiple assets have the same file size', () => { - const assets = [ - { exifInfo: { fileSizeInByte: 200, rating: 5, fNumber: 1 } }, - { exifInfo: { fileSizeInByte: 200, rating: 5 } }, - { exifInfo: { fileSizeInByte: 100, rating: 5 } }, - ]; - expect(suggestDuplicate(assets as AssetResponseDto[])).toEqual(assets[0]); - }); - - it('returns undefined for an empty array', () => { - const assets: AssetResponseDto[] = []; - expect(suggestDuplicate(assets)).toBeUndefined(); - }); - - it('handles assets with no exifInfo', () => { - const assets = [ - { exifInfo: { fileSizeInByte: 200 } }, - {}, - ]; - expect(suggestDuplicate(assets as AssetResponseDto[])).toEqual(assets[0]); - }); - - it('handles assets with exifInfo but no fileSizeInByte', () => { - const assets = [ - { exifInfo: { rating: 5, fNumber: 1 } }, - { exifInfo: { rating: 5 } }, - ]; - expect(suggestDuplicate(assets as AssetResponseDto[])).toEqual(assets[0]); - }); - -}); +import { suggestDuplicate } from '$lib/utils/duplicate-utils'; +import type { AssetResponseDto } from '@immich/sdk'; + +describe('choosing a duplicate', () => { + it('picks the asset with the largest file size', () => { + const assets = [ + { exifInfo: { fileSizeInByte: 300 } }, + { exifInfo: { fileSizeInByte: 200 } }, + { exifInfo: { fileSizeInByte: 100 } }, + ]; + expect(suggestDuplicate(assets as AssetResponseDto[])).toEqual(assets[0]); + }); + + it('picks the asset with the most exif data if multiple assets have the same file size', () => { + const assets = [ + { exifInfo: { fileSizeInByte: 200, rating: 5, fNumber: 1 } }, + { exifInfo: { fileSizeInByte: 200, rating: 5 } }, + { exifInfo: { fileSizeInByte: 100, rating: 5 } }, + ]; + expect(suggestDuplicate(assets as AssetResponseDto[])).toEqual(assets[0]); + }); + + it('returns undefined for an empty array', () => { + const assets: AssetResponseDto[] = []; + expect(suggestDuplicate(assets)).toBeUndefined(); + }); + + it('handles assets with no exifInfo', () => { + const assets = [{ exifInfo: { fileSizeInByte: 200 } }, {}]; + expect(suggestDuplicate(assets as AssetResponseDto[])).toEqual(assets[0]); + }); + + it('handles assets with exifInfo but no fileSizeInByte', () => { + const assets = [{ exifInfo: { rating: 5, fNumber: 1 } }, { exifInfo: { rating: 5 } }]; + expect(suggestDuplicate(assets as AssetResponseDto[])).toEqual(assets[0]); + }); +}); diff --git a/web/src/lib/utils/duplicate-utils.ts b/web/src/lib/utils/duplicate-utils.ts index a085518d64..12b5a03268 100644 --- a/web/src/lib/utils/duplicate-utils.ts +++ b/web/src/lib/utils/duplicate-utils.ts @@ -1,29 +1,31 @@ -import type { AssetResponseDto } from '@immich/sdk'; -import { sortBy } from 'lodash-es'; -import { getExifCount } from '$lib/utils/exif-utils'; - -/** - * Suggests the best duplicate asset to keep from a list of duplicates. - * - * The best asset is determined by the following criteria: - * - The asset with the largest file size - * - If there are multiple assets with the same file size, the asset with the most exif data - * - * @param assets List of duplicate assets - * @returns The best asset to keep - */ -export const suggestDuplicate = (assets: AssetResponseDto[]): AssetResponseDto | undefined => { - const assetsBySize = sortBy(assets, (asset) => asset.exifInfo?.fileSizeInByte ?? 0); - - // All assets with the same file size as the largest asset - const highestSizeAssets = assetsBySize.filter((asset) => asset.exifInfo?.fileSizeInByte === assetsBySize.at(-1)?.exifInfo?.fileSizeInByte); - - // If there are multiple assets with the same file size, return the one with the most exif data - if(highestSizeAssets.length >= 2) { - const assetsByExifCount = sortBy(highestSizeAssets, getExifCount); - return assetsByExifCount.pop(); - } - - // Return the asset with the largest file size - return assetsBySize.pop(); -}; +import { getExifCount } from '$lib/utils/exif-utils'; +import type { AssetResponseDto } from '@immich/sdk'; +import { sortBy } from 'lodash-es'; + +/** + * Suggests the best duplicate asset to keep from a list of duplicates. + * + * The best asset is determined by the following criteria: + * - The asset with the largest file size + * - If there are multiple assets with the same file size, the asset with the most exif data + * + * @param assets List of duplicate assets + * @returns The best asset to keep + */ +export const suggestDuplicate = (assets: AssetResponseDto[]): AssetResponseDto | undefined => { + const assetsBySize = sortBy(assets, (asset) => asset.exifInfo?.fileSizeInByte ?? 0); + + // All assets with the same file size as the largest asset + const highestSizeAssets = assetsBySize.filter( + (asset) => asset.exifInfo?.fileSizeInByte === assetsBySize.at(-1)?.exifInfo?.fileSizeInByte, + ); + + // If there are multiple assets with the same file size, return the one with the most exif data + if (highestSizeAssets.length >= 2) { + const assetsByExifCount = sortBy(highestSizeAssets, getExifCount); + return assetsByExifCount.pop(); + } + + // Return the asset with the largest file size + return assetsBySize.pop(); +}; diff --git a/web/src/lib/utils/exif-utils.spec.ts b/web/src/lib/utils/exif-utils.spec.ts index 6c3a3a962a..7ce2e88d6f 100644 --- a/web/src/lib/utils/exif-utils.spec.ts +++ b/web/src/lib/utils/exif-utils.spec.ts @@ -1,31 +1,29 @@ -import { getExifCount } from '$lib/utils/exif-utils'; -import type { AssetResponseDto } from '@immich/sdk'; - -describe('getting the exif count', () => { - - it('returns 0 when exifInfo is undefined', () => { - const asset = {}; - expect(getExifCount(asset as AssetResponseDto)).toBe(0); - }); - - it('returns 0 when exifInfo is empty', () => { - const asset = { exifInfo: {} }; - expect(getExifCount(asset as AssetResponseDto)).toBe(0); - }); - - it('returns the correct count of non-null exifInfo properties', () => { - const asset = { exifInfo: { fileSizeInByte: 200, rating: 5, fNumber: null } }; - expect(getExifCount(asset as AssetResponseDto)).toBe(2); - }); - - it('ignores null, undefined and empty properties in exifInfo', () => { - const asset = { exifInfo: { fileSizeInByte: 200, rating: null, fNumber: undefined, description: '' } }; - expect(getExifCount(asset as AssetResponseDto)).toBe(1); - }); - - it('returns the correct count when all exifInfo properties are non-null', () => { - const asset = { exifInfo: { fileSizeInByte: 200, rating: 5, fNumber: 1, description: 'test' } }; - expect(getExifCount(asset as AssetResponseDto)).toBe(4); - }); - -}); +import { getExifCount } from '$lib/utils/exif-utils'; +import type { AssetResponseDto } from '@immich/sdk'; + +describe('getting the exif count', () => { + it('returns 0 when exifInfo is undefined', () => { + const asset = {}; + expect(getExifCount(asset as AssetResponseDto)).toBe(0); + }); + + it('returns 0 when exifInfo is empty', () => { + const asset = { exifInfo: {} }; + expect(getExifCount(asset as AssetResponseDto)).toBe(0); + }); + + it('returns the correct count of non-null exifInfo properties', () => { + const asset = { exifInfo: { fileSizeInByte: 200, rating: 5, fNumber: null } }; + expect(getExifCount(asset as AssetResponseDto)).toBe(2); + }); + + it('ignores null, undefined and empty properties in exifInfo', () => { + const asset = { exifInfo: { fileSizeInByte: 200, rating: null, fNumber: undefined, description: '' } }; + expect(getExifCount(asset as AssetResponseDto)).toBe(1); + }); + + it('returns the correct count when all exifInfo properties are non-null', () => { + const asset = { exifInfo: { fileSizeInByte: 200, rating: 5, fNumber: 1, description: 'test' } }; + expect(getExifCount(asset as AssetResponseDto)).toBe(4); + }); +}); diff --git a/web/src/lib/utils/exif-utils.ts b/web/src/lib/utils/exif-utils.ts index 46fe7aaf90..75a6bbd055 100644 --- a/web/src/lib/utils/exif-utils.ts +++ b/web/src/lib/utils/exif-utils.ts @@ -1,5 +1,5 @@ -import type { AssetResponseDto } from '@immich/sdk'; - -export const getExifCount = (asset: AssetResponseDto) => { - return Object.values(asset.exifInfo ?? {}).filter(Boolean).length; -}; +import type { AssetResponseDto } from '@immich/sdk'; + +export const getExifCount = (asset: AssetResponseDto) => { + return Object.values(asset.exifInfo ?? {}).filter(Boolean).length; +};