1
0
Fork 0
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:
Chuckame 2025-01-02 05:50:15 +01:00 committed by GitHub
parent 5111ceffac
commit 3c35b467f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 48 additions and 7 deletions

View file

@ -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]);

View file

@ -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);