Skip to content

Commit 5490c95

Browse files
Chuckameyosit
authored andcommitted
feat(server): use the earliest date between file creation and modification timestamps when missing exif tags (immich-app#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
1 parent e741543 commit 5490c95

File tree

2 files changed

+48
-7
lines changed

2 files changed

+48
-7
lines changed

Diff for: server/src/services/metadata.service.spec.ts

+34
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,40 @@ describe(MetadataService.name, () => {
274274
});
275275
});
276276

277+
it('should take the file modification date when missing exif and earliest than creation date', async () => {
278+
const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z');
279+
const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z');
280+
assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, fileCreatedAt, fileModifiedAt }]);
281+
mockReadTags();
282+
283+
await sut.handleMetadataExtraction({ id: assetStub.image.id });
284+
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
285+
expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: fileModifiedAt }));
286+
expect(assetMock.update).toHaveBeenCalledWith({
287+
id: assetStub.image.id,
288+
duration: null,
289+
fileCreatedAt: fileModifiedAt,
290+
localDateTime: fileModifiedAt,
291+
});
292+
});
293+
294+
it('should take the file creation date when missing exif and earliest than modification date', async () => {
295+
const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z');
296+
const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z');
297+
assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, fileCreatedAt, fileModifiedAt }]);
298+
mockReadTags();
299+
300+
await sut.handleMetadataExtraction({ id: assetStub.image.id });
301+
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
302+
expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: fileCreatedAt }));
303+
expect(assetMock.update).toHaveBeenCalledWith({
304+
id: assetStub.image.id,
305+
duration: null,
306+
fileCreatedAt,
307+
localDateTime: fileCreatedAt,
308+
});
309+
});
310+
277311
it('should account for the server being in a non-UTC timezone', async () => {
278312
process.env.TZ = 'America/Los_Angeles';
279313
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);

Diff for: server/src/services/metadata.service.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -450,14 +450,14 @@ export class MetadataService extends BaseService {
450450
}
451451
} else {
452452
const motionAssetId = this.cryptoRepository.randomUUID();
453-
const createdAt = asset.fileCreatedAt ?? asset.createdAt;
453+
const dates = this.getDates(asset, tags);
454454
motionAsset = await this.assetRepository.create({
455455
id: motionAssetId,
456456
libraryId: asset.libraryId,
457457
type: AssetType.VIDEO,
458-
fileCreatedAt: createdAt,
459-
fileModifiedAt: asset.fileModifiedAt,
460-
localDateTime: createdAt,
458+
fileCreatedAt: dates.dateTimeOriginal,
459+
fileModifiedAt: dates.modifyDate,
460+
localDateTime: dates.localDateTime,
461461
checksum,
462462
ownerId: asset.ownerId,
463463
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
@@ -589,9 +589,12 @@ export class MetadataService extends BaseService {
589589
let dateTimeOriginal = dateTime?.toDate();
590590
let localDateTime = dateTime?.toDateTime().setZone('UTC', { keepLocalTime: true }).toJSDate();
591591
if (!localDateTime || !dateTimeOriginal) {
592-
this.logger.warn(`Asset ${asset.id} has no valid date, falling back to asset.fileCreatedAt`);
593-
dateTimeOriginal = asset.fileCreatedAt;
594-
localDateTime = asset.fileCreatedAt;
592+
this.logger.debug(
593+
`No valid date found in exif tags from asset ${asset.id}, falling back to earliest timestamp between file creation and file modification`,
594+
);
595+
const earliestDate = this.earliestDate(asset.fileModifiedAt, asset.fileCreatedAt);
596+
dateTimeOriginal = earliestDate;
597+
localDateTime = earliestDate;
595598
}
596599

597600
this.logger.verbose(`Asset ${asset.id} has a local time of ${localDateTime.toISOString()}`);
@@ -609,6 +612,10 @@ export class MetadataService extends BaseService {
609612
};
610613
}
611614

615+
private earliestDate(a: Date, b: Date) {
616+
return new Date(Math.min(a.valueOf(), b.valueOf()));
617+
}
618+
612619
private async getGeo(tags: ImmichTags, reverseGeocoding: SystemConfig['reverseGeocoding']) {
613620
let latitude = validate(tags.GPSLatitude);
614621
let longitude = validate(tags.GPSLongitude);

0 commit comments

Comments
 (0)