diff --git a/server/src/migrations/1740654480319-UnsetStackedAssetsFromDuplicateStatus.ts b/server/src/migrations/1740654480319-UnsetStackedAssetsFromDuplicateStatus.ts new file mode 100644 index 0000000000..5c735a60bb --- /dev/null +++ b/server/src/migrations/1740654480319-UnsetStackedAssetsFromDuplicateStatus.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UnsetStackedAssetsFromDuplicateStatus1740654480319 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + update assets + set "duplicateId" = null + where "stackId" is not null`); + } + + public async down(): Promise { + // No need to revert this migration + } +} diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 879152dc77..ce53bd1791 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -333,6 +333,7 @@ with and "assets"."duplicateId" is not null and "assets"."deletedAt" is null and "assets"."isVisible" = $2 + and "assets"."stackId" is null group by "assets"."duplicateId" ), diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index 9400700e56..06590dc817 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -112,6 +112,7 @@ with and "assets"."isVisible" = $3 and "assets"."type" = $4 and "assets"."id" != $5::uuid + and "assets"."stackId" is null order by smart_search.embedding <=> $6 limit diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 139e652f03..daefacef09 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -794,6 +794,7 @@ export class AssetRepository { .where('assets.duplicateId', 'is not', null) .where('assets.deletedAt', 'is', null) .where('assets.isVisible', '=', true) + .where('assets.stackId', 'is', null) .groupBy('assets.duplicateId'), ) .with('unique', (qb) => diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 2f313aa083..46f38db55f 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -318,6 +318,7 @@ export class SearchRepository { .where('assets.isVisible', '=', true) .where('assets.type', '=', type) .where('assets.id', '!=', asUuid(assetId)) + .where('assets.stackId', 'is', null) .orderBy(sql`smart_search.embedding <=> ${embedding}`) .limit(64), ) diff --git a/server/src/services/duplicate.service.spec.ts b/server/src/services/duplicate.service.spec.ts index 30b8cd3451..8be943eaf0 100644 --- a/server/src/services/duplicate.service.spec.ts +++ b/server/src/services/duplicate.service.spec.ts @@ -173,6 +173,16 @@ describe(SearchService.name, () => { expect(mocks.logger.error).toHaveBeenCalledWith(`Asset ${assetStub.image.id} not found`); }); + it('should skip if asset is part of stack', async () => { + const id = assetStub.primaryImage.id; + mocks.asset.getById.mockResolvedValue(assetStub.primaryImage); + + const result = await sut.handleSearchDuplicates({ id }); + + expect(result).toBe(JobStatus.SKIPPED); + expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is part of a stack, skipping`); + }); + it('should skip if asset is not visible', async () => { const id = assetStub.livePhotoMotionAsset.id; mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset); diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index 5600033b47..74b86f8e4e 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -59,6 +59,11 @@ export class DuplicateService extends BaseService { return JobStatus.FAILED; } + if (asset.stackId) { + this.logger.debug(`Asset ${id} is part of a stack, skipping`); + return JobStatus.SKIPPED; + } + if (!asset.isVisible) { this.logger.debug(`Asset ${id} is not visible, skipping`); return JobStatus.SKIPPED; diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 6c20a765c7..a0619f1a10 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -184,6 +184,7 @@ export const assetStub = { exifImageHeight: 1000, exifImageWidth: 1000, } as ExifEntity, + stackId: 'stack-1', stack: stackStub('stack-1', [ { id: 'primary-asset-id' } as AssetEntity, { id: 'stack-child-asset-1' } as AssetEntity,