mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05: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
					
				@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user