mirror of
https://github.com/immich-app/immich.git
synced 2025-01-09 13:26:47 +01:00
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
This commit is contained in:
parent
5111ceffac
commit
3c35b467f4
2 changed files with 48 additions and 7 deletions
|
@ -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 () => {
|
it('should account for the server being in a non-UTC timezone', async () => {
|
||||||
process.env.TZ = 'America/Los_Angeles';
|
process.env.TZ = 'America/Los_Angeles';
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
||||||
|
|
|
@ -450,14 +450,14 @@ export class MetadataService extends BaseService {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const motionAssetId = this.cryptoRepository.randomUUID();
|
const motionAssetId = this.cryptoRepository.randomUUID();
|
||||||
const createdAt = asset.fileCreatedAt ?? asset.createdAt;
|
const dates = this.getDates(asset, tags);
|
||||||
motionAsset = await this.assetRepository.create({
|
motionAsset = await this.assetRepository.create({
|
||||||
id: motionAssetId,
|
id: motionAssetId,
|
||||||
libraryId: asset.libraryId,
|
libraryId: asset.libraryId,
|
||||||
type: AssetType.VIDEO,
|
type: AssetType.VIDEO,
|
||||||
fileCreatedAt: createdAt,
|
fileCreatedAt: dates.dateTimeOriginal,
|
||||||
fileModifiedAt: asset.fileModifiedAt,
|
fileModifiedAt: dates.modifyDate,
|
||||||
localDateTime: createdAt,
|
localDateTime: dates.localDateTime,
|
||||||
checksum,
|
checksum,
|
||||||
ownerId: asset.ownerId,
|
ownerId: asset.ownerId,
|
||||||
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
|
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
|
||||||
|
@ -589,9 +589,12 @@ export class MetadataService extends BaseService {
|
||||||
let dateTimeOriginal = dateTime?.toDate();
|
let dateTimeOriginal = dateTime?.toDate();
|
||||||
let localDateTime = dateTime?.toDateTime().setZone('UTC', { keepLocalTime: true }).toJSDate();
|
let localDateTime = dateTime?.toDateTime().setZone('UTC', { keepLocalTime: true }).toJSDate();
|
||||||
if (!localDateTime || !dateTimeOriginal) {
|
if (!localDateTime || !dateTimeOriginal) {
|
||||||
this.logger.warn(`Asset ${asset.id} has no valid date, falling back to asset.fileCreatedAt`);
|
this.logger.debug(
|
||||||
dateTimeOriginal = asset.fileCreatedAt;
|
`No valid date found in exif tags from asset ${asset.id}, falling back to earliest timestamp between file creation and file modification`,
|
||||||
localDateTime = asset.fileCreatedAt;
|
);
|
||||||
|
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()}`);
|
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']) {
|
private async getGeo(tags: ImmichTags, reverseGeocoding: SystemConfig['reverseGeocoding']) {
|
||||||
let latitude = validate(tags.GPSLatitude);
|
let latitude = validate(tags.GPSLatitude);
|
||||||
let longitude = validate(tags.GPSLongitude);
|
let longitude = validate(tags.GPSLongitude);
|
||||||
|
|
Loading…
Reference in a new issue