diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/domain/metadata/metadata.service.spec.ts index f31605b121..bb2d706224 100644 --- a/server/src/domain/metadata/metadata.service.spec.ts +++ b/server/src/domain/metadata/metadata.service.spec.ts @@ -14,6 +14,7 @@ import { import { randomBytes } from 'crypto'; import { Stats } from 'fs'; import { constants } from 'fs/promises'; +import { when } from 'jest-when'; import { JobName, QueueName } from '../job'; import { IAlbumRepository, @@ -248,6 +249,30 @@ describe(MetadataService.name, () => { expect(assetMock.save).not.toHaveBeenCalled(); }); + it('should handle a date in a sidecar file', async () => { + const originalDate = new Date('2023-11-21T16:13:17.517Z'); + const sidecarDate = new Date('2022-01-01T00:00:00.000Z'); + assetMock.getByIds.mockResolvedValue([assetStub.sidecar]); + when(metadataMock.getExifTags) + .calledWith(assetStub.sidecar.originalPath) + // higher priority tag + .mockResolvedValue({ CreationDate: originalDate.toISOString() }); + when(metadataMock.getExifTags) + .calledWith(assetStub.sidecar.sidecarPath as string) + // lower priority tag, but in sidecar + .mockResolvedValue({ CreateDate: sidecarDate.toISOString() }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id]); + expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate })); + expect(assetMock.save).toHaveBeenCalledWith({ + id: assetStub.image.id, + duration: null, + fileCreatedAt: sidecarDate, + localDateTime: sidecarDate, + }); + }); + it('should handle lists of numbers', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); metadataMock.getExifTags.mockResolvedValue({ ISO: [160] as any }); diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 53c6668f3a..f600f75a9b 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -25,6 +25,18 @@ import { import { StorageCore } from '../storage'; import { FeatureFlag, SystemConfigCore } from '../system-config'; +/** look for a date from these tags (in order) */ +const EXIF_DATE_TAGS: Array = [ + 'SubSecDateTimeOriginal', + 'DateTimeOriginal', + 'SubSecCreateDate', + 'CreationDate', + 'CreateDate', + 'SubSecMediaCreateDate', + 'MediaCreateDate', + 'DateTimeCreated', +]; + interface DirectoryItem { Length?: number; Mime: string; @@ -340,6 +352,15 @@ export class MetadataService { const stats = await this.storageRepository.stat(asset.originalPath); const mediaTags = await this.repository.getExifTags(asset.originalPath); const sidecarTags = asset.sidecarPath ? await this.repository.getExifTags(asset.sidecarPath) : null; + + // ensure date from sidecar is used if present + const hasDateOverride = !!this.getDateTimeOriginal(sidecarTags); + if (mediaTags && hasDateOverride) { + for (const tag of EXIF_DATE_TAGS) { + delete mediaTags[tag]; + } + } + const tags = { ...mediaTags, ...sidecarTags }; this.logger.verbose('Exif Tags', tags); @@ -350,19 +371,7 @@ export class MetadataService { assetId: asset.id, bitsPerSample: this.getBitsPerSample(tags), colorspace: tags.ColorSpace ?? null, - dateTimeOriginal: - exifDate( - firstDateTime(tags as Tags, [ - 'SubSecDateTimeOriginal', - 'DateTimeOriginal', - 'SubSecCreateDate', - 'CreationDate', - 'CreateDate', - 'SubSecMediaCreateDate', - 'MediaCreateDate', - 'DateTimeCreated', - ]), - ) ?? asset.fileCreatedAt, + dateTimeOriginal: this.getDateTimeOriginal(tags) ?? asset.fileCreatedAt, exifImageHeight: validate(tags.ImageHeight), exifImageWidth: validate(tags.ImageWidth), exposureTime: tags.ExposureTime ?? null, @@ -387,6 +396,13 @@ export class MetadataService { }; } + private getDateTimeOriginal(tags: ImmichTags | Tags | null) { + if (!tags) { + return null; + } + return exifDate(firstDateTime(tags as Tags, EXIF_DATE_TAGS)); + } + private getBitsPerSample(tags: ImmichTags): number | null { const bitDepthTags = [ tags.BitsPerSample,