diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index ab3252c40b..d4eee16232 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -473,6 +473,7 @@ describe('/asset', () => { id: user1Assets[0].id, exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00', + timeZone: 'UTC-7', }), }); expect(status).toEqual(200); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 23ba29e7b8..ed427684f1 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -46,6 +46,7 @@ import { onBeforeUnlink, } from 'src/utils/asset.util'; import { updateLockedColumns } from 'src/utils/database'; +import { extractTimeZone } from 'src/utils/date'; import { transformOcrBoundingBox } from 'src/utils/transform'; @Injectable() @@ -168,12 +169,13 @@ export class AssetService extends BaseService { }, _.isUndefined, ); - const extractedTimeZone = dateTimeOriginal ? DateTime.fromISO(dateTimeOriginal, { setZone: true }).zone : undefined; if (Object.keys(exifDto).length > 0) { await this.assetRepository.updateAllExif(ids, exifDto); } + const extractedTimeZone = extractTimeZone(dateTimeOriginal); + if ( (dateTimeRelative !== undefined && dateTimeRelative !== 0) || timeZone !== undefined || @@ -513,12 +515,11 @@ export class AssetService extends BaseService { rating?: number; }) { const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto; - const extractedTimeZone = dateTimeOriginal ? DateTime.fromISO(dateTimeOriginal, { setZone: true }).zone : undefined; const writes = _.omitBy( { description, dateTimeOriginal, - timeZone: extractedTimeZone?.type === 'fixed' ? extractedTimeZone.name : undefined, + timeZone: extractTimeZone(dateTimeOriginal)?.name, latitude, longitude, rating, diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 942817a213..eda4e1a063 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1766,13 +1766,14 @@ describe(MetadataService.name, () => { const asset = factory.jobAssets.sidecarWrite(); const description = 'this is a description'; const gps = 12; - const date = '2023-11-22T04:56:12.196Z'; + const date = '2023-11-21T22:56:12.196-06:00'; mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue([ 'description', 'latitude', 'longitude', 'dateTimeOriginal', + 'timeZone', ]); mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset); await expect( @@ -1792,6 +1793,7 @@ describe(MetadataService.name, () => { 'latitude', 'longitude', 'dateTimeOriginal', + 'timeZone', ]); }); }); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index f5af444a22..4113025914 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -32,6 +32,7 @@ import { BaseService } from 'src/services/base.service'; import { JobItem, JobOf } from 'src/types'; import { getAssetFiles } from 'src/utils/asset.util'; import { isAssetChecksumConstraint } from 'src/utils/database'; +import { mergeTimeZone } from 'src/utils/date'; import { mimeTypes } from 'src/utils/mime-types'; import { isFaceImportEnabled } from 'src/utils/misc'; import { upsertTags } from 'src/utils/tag'; @@ -431,14 +432,16 @@ export class MetadataService extends BaseService { const { sidecarFile } = getAssetFiles(asset.files); const sidecarPath = sidecarFile?.path || `${asset.originalPath}.xmp`; - const { description, dateTimeOriginal, latitude, longitude, rating, tags } = _.pick( + const { description, dateTimeOriginal, latitude, longitude, rating, tags, timeZone } = _.pick( { description: asset.exifInfo.description, - dateTimeOriginal: asset.exifInfo.dateTimeOriginal, + // the kysely type is wrong here; fixed in 0.28.3 + dateTimeOriginal: asset.exifInfo.dateTimeOriginal as string | null, latitude: asset.exifInfo.latitude, longitude: asset.exifInfo.longitude, rating: asset.exifInfo.rating, tags: asset.exifInfo.tags, + timeZone: asset.exifInfo.timeZone, }, lockedProperties, ); @@ -447,7 +450,7 @@ export class MetadataService extends BaseService { { Description: description, ImageDescription: description, - DateTimeOriginal: dateTimeOriginal ? String(dateTimeOriginal) : undefined, + DateTimeOriginal: mergeTimeZone(dateTimeOriginal, timeZone)?.toISO(), GPSLatitude: latitude, GPSLongitude: longitude, Rating: rating, diff --git a/server/src/utils/date.ts b/server/src/utils/date.ts index 67ce549050..6cef48ecf8 100644 --- a/server/src/utils/date.ts +++ b/server/src/utils/date.ts @@ -1,3 +1,16 @@ +import { DateTime } from 'luxon'; + export const asDateString = (x: Date | string | null): string | null => { return x instanceof Date ? x.toISOString().split('T')[0] : x; }; + +export const extractTimeZone = (dateTimeOriginal?: string | null) => { + const extractedTimeZone = dateTimeOriginal ? DateTime.fromISO(dateTimeOriginal, { setZone: true }).zone : undefined; + return extractedTimeZone?.type === 'fixed' ? extractedTimeZone : undefined; +}; + +export const mergeTimeZone = (dateTimeOriginal?: string | null, timeZone?: string | null) => { + return dateTimeOriginal + ? DateTime.fromISO(dateTimeOriginal, { zone: 'UTC' }).setZone(timeZone ?? undefined) + : undefined; +}; diff --git a/server/test/medium/specs/services/asset.service.spec.ts b/server/test/medium/specs/services/asset.service.spec.ts index 1e34544e38..29e7ea7039 100644 --- a/server/test/medium/specs/services/asset.service.spec.ts +++ b/server/test/medium/specs/services/asset.service.spec.ts @@ -456,6 +456,47 @@ describe(AssetService.name, () => { ); }); + it('should relatively update an assets with timezone', 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, dateTimeOriginal: '2023-11-19T18:11:00', timeZone: 'UTC+5' }); + + await sut.updateAll(auth, { ids: [asset.id], dateTimeRelative: -1441 }); + + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ + dateTimeOriginal: '2023-11-18T18:10:00+00:00', + timeZone: 'UTC+5', + lockedProperties: ['timeZone', 'dateTimeOriginal'], + }), + }), + ); + }); + + it('should relatively update an assets and set a timezone', 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, dateTimeOriginal: '2023-11-19T18:11:00' }); + + await sut.updateAll(auth, { ids: [asset.id], dateTimeRelative: -11, timeZone: 'UTC+5' }); + + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ + dateTimeOriginal: '2023-11-19T18:00:00+00:00', + timeZone: 'UTC+5', + }), + }), + ); + }); + it('should update dateTimeOriginal', async () => { const { sut, ctx } = setup(); ctx.getMock(JobRepository).queueAll.mockResolvedValue(); diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 2d29386d67..6ce4ad1f59 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -361,6 +361,7 @@ const assetSidecarWriteFactory = () => { latitude: 12, longitude: 12, dateTimeOriginal: '2023-11-22T04:56:12.196Z', + timeZone: 'UTC-6', } as unknown as Exif, }; };