Skip to content

Commit 7f080d0

Browse files
committed
feat(server): Use the earliest date between file creation and modification timestamps when missing exif tags
1 parent c3be74c commit 7f080d0

File tree

3 files changed

+49
-13
lines changed

3 files changed

+49
-13
lines changed

Diff for: docs/package-lock.json

-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

+15-6
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,
458+
fileCreatedAt: dates.dateTimeOriginal,
459459
fileModifiedAt: asset.fileModifiedAt,
460-
localDateTime: createdAt,
460+
localDateTime: dates.localDateTime,
461461
checksum,
462462
ownerId: asset.ownerId,
463463
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
@@ -589,9 +589,18 @@ 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+
// When a file is copied (but not moved) before being uploaded to immich, the target file creation
593+
// date is set at the current timestamp, while the modification date remains untouched, so if the
594+
// user copied the asset while he did not modified the file (like cropping, rotating and more), then
595+
// we use the modification timestamp as it's still the original date. If the user modified the asset,
596+
// then there is no other solution except a further manual fix.
597+
this.logger.warn(
598+
`No valid date found in exif tags from asset ${asset.id}, falling back to earliest timestamp between file creation and file modification`,
599+
);
600+
// eslint-disable-next-line unicorn/prefer-math-min-max
601+
const earliestDate = asset.fileModifiedAt < asset.fileCreatedAt ? asset.fileModifiedAt : asset.fileCreatedAt;
602+
dateTimeOriginal = earliestDate;
603+
localDateTime = earliestDate;
595604
}
596605

597606
this.logger.verbose(`Asset ${asset.id} has a local time of ${localDateTime.toISOString()}`);

0 commit comments

Comments
 (0)