fix(server): skip stacked assets in duplicate detection (#16380)

* skip stacked assets in duplicate detection

* update sql

* handle stacking after duplicate detection runs
This commit is contained in:
Mert 2025-02-27 19:16:13 +03:00 committed by GitHub
parent a808b8610e
commit a708649504
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 34 additions and 0 deletions

View File

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UnsetStackedAssetsFromDuplicateStatus1740654480319 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
update assets
set "duplicateId" = null
where "stackId" is not null`);
}
public async down(): Promise<void> {
// No need to revert this migration
}
}

View File

@ -333,6 +333,7 @@ with
and "assets"."duplicateId" is not null and "assets"."duplicateId" is not null
and "assets"."deletedAt" is null and "assets"."deletedAt" is null
and "assets"."isVisible" = $2 and "assets"."isVisible" = $2
and "assets"."stackId" is null
group by group by
"assets"."duplicateId" "assets"."duplicateId"
), ),

View File

@ -112,6 +112,7 @@ with
and "assets"."isVisible" = $3 and "assets"."isVisible" = $3
and "assets"."type" = $4 and "assets"."type" = $4
and "assets"."id" != $5::uuid and "assets"."id" != $5::uuid
and "assets"."stackId" is null
order by order by
smart_search.embedding <=> $6 smart_search.embedding <=> $6
limit limit

View File

@ -794,6 +794,7 @@ export class AssetRepository {
.where('assets.duplicateId', 'is not', null) .where('assets.duplicateId', 'is not', null)
.where('assets.deletedAt', 'is', null) .where('assets.deletedAt', 'is', null)
.where('assets.isVisible', '=', true) .where('assets.isVisible', '=', true)
.where('assets.stackId', 'is', null)
.groupBy('assets.duplicateId'), .groupBy('assets.duplicateId'),
) )
.with('unique', (qb) => .with('unique', (qb) =>

View File

@ -318,6 +318,7 @@ export class SearchRepository {
.where('assets.isVisible', '=', true) .where('assets.isVisible', '=', true)
.where('assets.type', '=', type) .where('assets.type', '=', type)
.where('assets.id', '!=', asUuid(assetId)) .where('assets.id', '!=', asUuid(assetId))
.where('assets.stackId', 'is', null)
.orderBy(sql`smart_search.embedding <=> ${embedding}`) .orderBy(sql`smart_search.embedding <=> ${embedding}`)
.limit(64), .limit(64),
) )

View File

@ -173,6 +173,16 @@ describe(SearchService.name, () => {
expect(mocks.logger.error).toHaveBeenCalledWith(`Asset ${assetStub.image.id} not found`); 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 () => { it('should skip if asset is not visible', async () => {
const id = assetStub.livePhotoMotionAsset.id; const id = assetStub.livePhotoMotionAsset.id;
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset); mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);

View File

@ -59,6 +59,11 @@ export class DuplicateService extends BaseService {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
if (asset.stackId) {
this.logger.debug(`Asset ${id} is part of a stack, skipping`);
return JobStatus.SKIPPED;
}
if (!asset.isVisible) { if (!asset.isVisible) {
this.logger.debug(`Asset ${id} is not visible, skipping`); this.logger.debug(`Asset ${id} is not visible, skipping`);
return JobStatus.SKIPPED; return JobStatus.SKIPPED;

View File

@ -184,6 +184,7 @@ export const assetStub = {
exifImageHeight: 1000, exifImageHeight: 1000,
exifImageWidth: 1000, exifImageWidth: 1000,
} as ExifEntity, } as ExifEntity,
stackId: 'stack-1',
stack: stackStub('stack-1', [ stack: stackStub('stack-1', [
{ id: 'primary-asset-id' } as AssetEntity, { id: 'primary-asset-id' } as AssetEntity,
{ id: 'stack-child-asset-1' } as AssetEntity, { id: 'stack-child-asset-1' } as AssetEntity,