-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(web): Improve duplicate suggestion (#14947)
* feat: Improve duplicate suggestion * format * feat(web): Add deduplication info popup * fix: lint * fmt --------- Co-authored-by: Alex <[email protected]>
- Loading branch information
1 parent
23f3e73
commit b4c1304
Showing
11 changed files
with
160 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
web/src/lib/components/shared-components/duplicates-modal.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<script lang="ts"> | ||
import { t } from 'svelte-i18n'; | ||
import FullScreenModal from './full-screen-modal.svelte'; | ||
interface Props { | ||
onClose: () => void; | ||
} | ||
let { onClose }: Props = $props(); | ||
</script> | ||
|
||
<FullScreenModal title={$t('deduplication_info')} width="auto" {onClose}> | ||
<div class="text-sm dark:text-white"> | ||
<p>{$t('deduplication_info_description')}</p> | ||
<ol class="ml-8 mt-2" style="list-style: decimal"> | ||
<li>{$t('deduplication_criteria_1')}</li> | ||
<li>{$t('deduplication_criteria_2')}</li> | ||
</ol> | ||
</div> | ||
</FullScreenModal> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
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: | ||
* - Largest image file size in bytes | ||
* - Largest count of exif data | ||
* | ||
* @param assets List of duplicate assets | ||
* @returns The best asset to keep | ||
*/ | ||
export const suggestDuplicate = (assets: AssetResponseDto[]): AssetResponseDto | undefined => { | ||
let duplicateAssets = sortBy(assets, (asset) => asset.exifInfo?.fileSizeInByte ?? 0); | ||
|
||
// Update the list to only include assets with the largest file size | ||
duplicateAssets = duplicateAssets.filter( | ||
(asset) => asset.exifInfo?.fileSizeInByte === duplicateAssets.at(-1)?.exifInfo?.fileSizeInByte, | ||
); | ||
|
||
// If there are multiple assets with the same file size, sort the list by the count of exif data | ||
if (duplicateAssets.length >= 2) { | ||
duplicateAssets = sortBy(duplicateAssets, getExifCount); | ||
} | ||
|
||
// Return the last asset in the list | ||
return duplicateAssets.pop(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type { AssetResponseDto } from '@immich/sdk'; | ||
|
||
export const getExifCount = (asset: AssetResponseDto) => { | ||
return Object.values(asset.exifInfo ?? {}).filter(Boolean).length; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters