From 4812a2e2d83c2dee9ba022011ad9ecb5fa8db03b Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:17:32 +0100 Subject: [PATCH] fix(server): refresh unedited asset dimensions on metadata extraction (#27220) --- server/src/database.ts | 1 + server/src/queries/asset.job.repository.sql | 1 + server/src/services/metadata.service.spec.ts | 24 ++++++++++++++++++-- server/src/services/metadata.service.ts | 7 +++--- server/test/mappers.ts | 1 + 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/server/src/database.ts b/server/src/database.ts index eb558c6d28..f4878b2cee 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -345,6 +345,7 @@ export const columns = { 'asset.type', 'asset.width', 'asset.height', + 'asset.isEdited', ], assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type', 'asset_file.isEdited'], assetFilesForThumbnail: [ diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index cebb9fe95e..cf5b8f02dc 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -264,6 +264,7 @@ select "asset"."type", "asset"."width", "asset"."height", + "asset"."isEdited", ( select coalesce(json_agg(agg), '[]') diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 7cb42990ea..cb35e21d0a 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1641,12 +1641,32 @@ describe(MetadataService.name, () => { ); }); - it('should not overwrite existing width/height if they already exist', async () => { - const asset = AssetFactory.create({ width: 1920, height: 1080 }); + it('should overwrite existing width/height for unedited assets', async () => { + const asset = AssetFactory.create({ width: 1920, height: 1080, isEdited: false }); mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset)); mockReadTags({ ImageWidth: 1280, ImageHeight: 720 }); await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.asset.update).toHaveBeenCalledWith( + expect.objectContaining({ + width: 1280, + height: 720, + }), + ); + }); + + it('should not overwrite existing width/height for edited assets', async () => { + const asset = AssetFactory.create({ width: 1920, height: 1080, isEdited: true }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset)); + mockReadTags({ ImageWidth: 1280, ImageHeight: 720 }); + + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.asset.update).toHaveBeenCalledWith( + expect.objectContaining({ + width: undefined, + height: undefined, + }), + ); expect(mocks.asset.update).not.toHaveBeenCalledWith( expect.objectContaining({ width: 1280, diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 7b87ea06a6..bfbcb413c0 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -327,10 +327,9 @@ export class MetadataService extends BaseService { fileCreatedAt: dates.dateTimeOriginal ?? undefined, fileModifiedAt: stats.mtime, - // only update the dimensions if they don't already exist - // we don't want to overwrite width/height that are modified by edits - width: asset.width == null ? assetWidth : undefined, - height: asset.height == null ? assetHeight : undefined, + // Keep unedited assets in sync with the file on disk, but don't overwrite edited dimensions. + width: !asset.isEdited || asset.width == null ? assetWidth : undefined, + height: !asset.isEdited || asset.height == null ? assetHeight : undefined, }), async () => { await this.assetRepository.upsertExif(exifData, { lockedPropertiesBehavior: 'skip' }); diff --git a/server/test/mappers.ts b/server/test/mappers.ts index 7f324663be..2f3b248576 100644 --- a/server/test/mappers.ts +++ b/server/test/mappers.ts @@ -138,6 +138,7 @@ export const getForMetadataExtraction = (asset: ReturnType getDehydrated(face)),