From 3c35b467f4539be5c684be542fc902c308addf45 Mon Sep 17 00:00:00 2001 From: Chuckame Date: Thu, 2 Jan 2025 05:50:15 +0100 Subject: [PATCH] feat(server): use the earliest date between file creation and modification timestamps when missing exif tags (#14874) * feat(server): Use the earliest date between file creation and modification timestamps when missing exif tags * PR fixes * PR fixes * Switch log to debug * fix linter for min date * apply prettier --- server/src/services/metadata.service.spec.ts | 34 ++++++++++++++++++++ server/src/services/metadata.service.ts | 21 ++++++++---- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 390f18b777..619023a1c7 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -274,6 +274,40 @@ describe(MetadataService.name, () => { }); }); + it('should take the file modification date when missing exif and earliest than creation date', async () => { + const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z'); + const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z'); + assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, fileCreatedAt, fileModifiedAt }]); + mockReadTags(); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } }); + expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: fileModifiedAt })); + expect(assetMock.update).toHaveBeenCalledWith({ + id: assetStub.image.id, + duration: null, + fileCreatedAt: fileModifiedAt, + localDateTime: fileModifiedAt, + }); + }); + + it('should take the file creation date when missing exif and earliest than modification date', async () => { + const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z'); + const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z'); + assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, fileCreatedAt, fileModifiedAt }]); + mockReadTags(); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } }); + expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: fileCreatedAt })); + expect(assetMock.update).toHaveBeenCalledWith({ + id: assetStub.image.id, + duration: null, + fileCreatedAt, + localDateTime: fileCreatedAt, + }); + }); + it('should account for the server being in a non-UTC timezone', async () => { process.env.TZ = 'America/Los_Angeles'; assetMock.getByIds.mockResolvedValue([assetStub.sidecar]); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index e0566c84b7..e1b14e0b8b 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -450,14 +450,14 @@ export class MetadataService extends BaseService { } } else { const motionAssetId = this.cryptoRepository.randomUUID(); - const createdAt = asset.fileCreatedAt ?? asset.createdAt; + const dates = this.getDates(asset, tags); motionAsset = await this.assetRepository.create({ id: motionAssetId, libraryId: asset.libraryId, type: AssetType.VIDEO, - fileCreatedAt: createdAt, - fileModifiedAt: asset.fileModifiedAt, - localDateTime: createdAt, + fileCreatedAt: dates.dateTimeOriginal, + fileModifiedAt: dates.modifyDate, + localDateTime: dates.localDateTime, checksum, ownerId: asset.ownerId, originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId), @@ -589,9 +589,12 @@ export class MetadataService extends BaseService { let dateTimeOriginal = dateTime?.toDate(); let localDateTime = dateTime?.toDateTime().setZone('UTC', { keepLocalTime: true }).toJSDate(); if (!localDateTime || !dateTimeOriginal) { - this.logger.warn(`Asset ${asset.id} has no valid date, falling back to asset.fileCreatedAt`); - dateTimeOriginal = asset.fileCreatedAt; - localDateTime = asset.fileCreatedAt; + this.logger.debug( + `No valid date found in exif tags from asset ${asset.id}, falling back to earliest timestamp between file creation and file modification`, + ); + const earliestDate = this.earliestDate(asset.fileModifiedAt, asset.fileCreatedAt); + dateTimeOriginal = earliestDate; + localDateTime = earliestDate; } this.logger.verbose(`Asset ${asset.id} has a local time of ${localDateTime.toISOString()}`); @@ -609,6 +612,10 @@ export class MetadataService extends BaseService { }; } + private earliestDate(a: Date, b: Date) { + return new Date(Math.min(a.valueOf(), b.valueOf())); + } + private async getGeo(tags: ImmichTags, reverseGeocoding: SystemConfig['reverseGeocoding']) { let latitude = validate(tags.GPSLatitude); let longitude = validate(tags.GPSLongitude);