Skip to content

Commit

Permalink
feat(server): Use the earliest date between file creation and modific…
Browse files Browse the repository at this point in the history
…ation timestamps when missing exif tags
  • Loading branch information
Chuckame committed Dec 23, 2024
1 parent c3be74c commit 7e2009f
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 14 deletions.
7 changes: 0 additions & 7 deletions docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions server/src/services/metadata.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
23 changes: 16 additions & 7 deletions server/src/services/metadata.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -589,9 +589,18 @@ 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;
// When a file is copied (but not moved) before being uploaded to immich, the target file creation
// date is set at the current timestamp, while the modification date remains untouched, so if the
// user copied the asset while he did not modified the file (like cropping, rotating and more), then
// we use the modification timestamp as it's still the original date. If the user modified the asset,
// then there is no other solution except a further manual fix.
this.logger.warn(
`No valid date found in exif tags from asset ${asset.id}, falling back to earliest timestamp between file creation and file modification`,
);
// eslint-disable-next-line unicorn/prefer-math-min-max
const earliestDate = asset.fileModifiedAt < asset.fileCreatedAt ? asset.fileModifiedAt : asset.fileCreatedAt;
dateTimeOriginal = earliestDate;
localDateTime = earliestDate;
}

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

0 comments on commit 7e2009f

Please sign in to comment.