diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 282d74a9b1..c584cf134f 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -144,14 +144,28 @@ export class AssetService extends BaseService { await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids }); const assetDto = _.omitBy({ isFavorite, visibility, duplicateId }, _.isUndefined); - const exifDto = _.omitBy({ latitude, longitude, rating, description, dateTimeOriginal }, _.isUndefined); + const exifDto = _.omitBy( + { + latitude, + longitude, + rating, + description, + dateTimeOriginal, + }, + _.isUndefined, + ); + const extractedTimeZone = dateTimeOriginal ? DateTime.fromISO(dateTimeOriginal, { setZone: true }).zone : undefined; if (Object.keys(exifDto).length > 0) { await this.assetRepository.updateAllExif(ids, exifDto); } - if ((dateTimeRelative !== undefined && dateTimeRelative !== 0) || timeZone !== undefined) { - await this.assetRepository.updateDateTimeOriginal(ids, dateTimeRelative, timeZone); + if ( + (dateTimeRelative !== undefined && dateTimeRelative !== 0) || + timeZone !== undefined || + extractedTimeZone?.type === 'fixed' + ) { + await this.assetRepository.updateDateTimeOriginal(ids, dateTimeRelative, timeZone ?? extractedTimeZone?.name); } if (Object.keys(assetDto).length > 0) { @@ -436,7 +450,19 @@ export class AssetService extends BaseService { rating?: number; }) { const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto; - const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined); + const extractedTimeZone = dateTimeOriginal ? DateTime.fromISO(dateTimeOriginal, { setZone: true }).zone : undefined; + const writes = _.omitBy( + { + description, + dateTimeOriginal, + timeZone: extractedTimeZone?.type === 'fixed' ? extractedTimeZone.name : undefined, + latitude, + longitude, + rating, + }, + _.isUndefined, + ); + if (Object.keys(writes).length > 0) { await this.assetRepository.upsertExif( updateLockedColumns({ diff --git a/server/test/medium/specs/services/asset.service.spec.ts b/server/test/medium/specs/services/asset.service.spec.ts index 8b54019fcf..a7c6470599 100644 --- a/server/test/medium/specs/services/asset.service.spec.ts +++ b/server/test/medium/specs/services/asset.service.spec.ts @@ -285,7 +285,7 @@ describe(AssetService.name, () => { .where('assetId', '=', asset.id) .executeTakeFirstOrThrow(), ).resolves.toEqual({ lockedProperties: null }); - await sut.update(auth, asset.id, { dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' }); + await sut.update(auth, asset.id, { dateTimeOriginal: '2023-11-19T18:11:00' }); await expect( ctx.database @@ -296,7 +296,38 @@ describe(AssetService.name, () => { ).resolves.toEqual({ lockedProperties: ['dateTimeOriginal'] }); await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( expect.objectContaining({ - exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00' }), + exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-19T18:11:00+00:00', timeZone: null }), + }), + ); + }); + + it('should update dateTimeOriginal with time zone', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queue.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: 'test' }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: null }); + await sut.update(auth, asset.id, { dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['timeZone', 'dateTimeOriginal'] }); + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00', timeZone: 'UTC-7' }), }), ); }); @@ -329,4 +360,65 @@ describe(AssetService.name, () => { ); }); }); + + it('should update dateTimeOriginal', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queueAll.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: 'test' }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: null }); + await sut.updateAll(auth, { ids: [asset.id], dateTimeOriginal: '2023-11-19T18:11:00' }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['dateTimeOriginal'] }); + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-19T18:11:00+00:00', timeZone: null }), + }), + ); + }); + + it('should update dateTimeOriginal with time zone', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queueAll.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: 'test' }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: null }); + await sut.updateAll(auth, { ids: [asset.id], dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' }); + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['timeZone', 'dateTimeOriginal'] }); + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00', timeZone: 'UTC-7' }), + }), + ); + }); });