mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:37:11 -04:00 
			
		
		
		
	fix(server): handle NaN in metadata extraction (#4221)
Fallback to null in event of invalid number.
This commit is contained in:
		
							parent
							
								
									3a44e8f8d3
								
							
						
					
					
						commit
						85efbc6984
					
				| @ -14,13 +14,14 @@ export interface ReverseGeocodeResult { | |||||||
|   city: string | null; |   city: string | null; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface ImmichTags extends Tags { | export interface ImmichTags extends Omit<Tags, 'FocalLength'> { | ||||||
|   ContentIdentifier?: string; |   ContentIdentifier?: string; | ||||||
|   MotionPhoto?: number; |   MotionPhoto?: number; | ||||||
|   MotionPhotoVersion?: number; |   MotionPhotoVersion?: number; | ||||||
|   MotionPhotoPresentationTimestampUs?: number; |   MotionPhotoPresentationTimestampUs?: number; | ||||||
|   MediaGroupUUID?: string; |   MediaGroupUUID?: string; | ||||||
|   ImagePixelDepth?: string; |   ImagePixelDepth?: string; | ||||||
|  |   FocalLength?: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface IMetadataRepository { | export interface IMetadataRepository { | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; | import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; | ||||||
| import { Inject, Injectable, Logger } from '@nestjs/common'; | import { Inject, Injectable, Logger } from '@nestjs/common'; | ||||||
| import { ExifDateTime } from 'exiftool-vendored'; | import { ExifDateTime, Tags } from 'exiftool-vendored'; | ||||||
| import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime'; | import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime'; | ||||||
| import { constants } from 'fs/promises'; | import { constants } from 'fs/promises'; | ||||||
| import { Duration } from 'luxon'; | import { Duration } from 'luxon'; | ||||||
| @ -24,9 +24,25 @@ interface DirectoryEntry { | |||||||
|   Item: DirectoryItem; |   Item: DirectoryItem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | type ExifEntityWithoutGeocodeAndTypeOrm = Omit< | ||||||
|  |   ExifEntity, | ||||||
|  |   'city' | 'state' | 'country' | 'description' | 'exifTextSearchableColumn' | ||||||
|  | >; | ||||||
|  | 
 | ||||||
| const exifDate = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.toDate() : null); | const exifDate = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.toDate() : null); | ||||||
| // exiftool returns strings when it fails to parse non-string values, so this is used where a string is not expected
 | 
 | ||||||
| const validate = <T>(value: T): T | null => (typeof value === 'string' ? null : value ?? null); | const validate = <T>(value: T): NonNullable<T> | null => { | ||||||
|  |   if (typeof value === 'string') { | ||||||
|  |     // string means a failure to parse a number, throw out result
 | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (typeof value === 'number' && (isNaN(value) || !isFinite(value))) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return value ?? null; | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| @Injectable() | @Injectable() | ||||||
| export class MetadataService { | export class MetadataService { | ||||||
| @ -184,7 +200,7 @@ export class MetadataService { | |||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntity) { |   private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) { | ||||||
|     const { latitude, longitude } = exifData; |     const { latitude, longitude } = exifData; | ||||||
|     if (!(await this.configCore.hasFeature(FeatureFlag.REVERSE_GEOCODING)) || !longitude || !latitude) { |     if (!(await this.configCore.hasFeature(FeatureFlag.REVERSE_GEOCODING)) || !longitude || !latitude) { | ||||||
|       return; |       return; | ||||||
| @ -275,7 +291,9 @@ export class MetadataService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async exifData(asset: AssetEntity): Promise<{ exifData: ExifEntity; tags: ImmichTags }> { |   private async exifData( | ||||||
|  |     asset: AssetEntity, | ||||||
|  |   ): Promise<{ exifData: ExifEntityWithoutGeocodeAndTypeOrm; tags: ImmichTags }> { | ||||||
|     const stats = await this.storageRepository.stat(asset.originalPath); |     const stats = await this.storageRepository.stat(asset.originalPath); | ||||||
|     const mediaTags = await this.repository.getExifTags(asset.originalPath); |     const mediaTags = await this.repository.getExifTags(asset.originalPath); | ||||||
|     const sidecarTags = asset.sidecarPath ? await this.repository.getExifTags(asset.sidecarPath) : null; |     const sidecarTags = asset.sidecarPath ? await this.repository.getExifTags(asset.sidecarPath) : null; | ||||||
| @ -284,12 +302,12 @@ export class MetadataService { | |||||||
|     this.logger.verbose('Exif Tags', tags); |     this.logger.verbose('Exif Tags', tags); | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|       exifData: <ExifEntity>{ |       exifData: { | ||||||
|         // altitude: tags.GPSAltitude ?? null,
 |         // altitude: tags.GPSAltitude ?? null,
 | ||||||
|         assetId: asset.id, |         assetId: asset.id, | ||||||
|         bitsPerSample: this.getBitsPerSample(tags), |         bitsPerSample: this.getBitsPerSample(tags), | ||||||
|         colorspace: tags.ColorSpace ?? null, |         colorspace: tags.ColorSpace ?? null, | ||||||
|         dateTimeOriginal: exifDate(firstDateTime(tags)) ?? asset.fileCreatedAt, |         dateTimeOriginal: exifDate(firstDateTime(tags as Tags)) ?? asset.fileCreatedAt, | ||||||
|         exifImageHeight: validate(tags.ImageHeight), |         exifImageHeight: validate(tags.ImageHeight), | ||||||
|         exifImageWidth: validate(tags.ImageWidth), |         exifImageWidth: validate(tags.ImageWidth), | ||||||
|         exposureTime: tags.ExposureTime ?? null, |         exposureTime: tags.ExposureTime ?? null, | ||||||
| @ -308,7 +326,7 @@ export class MetadataService { | |||||||
|         orientation: validate(tags.Orientation)?.toString() ?? null, |         orientation: validate(tags.Orientation)?.toString() ?? null, | ||||||
|         profileDescription: tags.ProfileDescription || tags.ProfileName || null, |         profileDescription: tags.ProfileDescription || tags.ProfileName || null, | ||||||
|         projectionType: tags.ProjectionType ? String(tags.ProjectionType).toUpperCase() : null, |         projectionType: tags.ProjectionType ? String(tags.ProjectionType).toUpperCase() : null, | ||||||
|         timeZone: tags.tz, |         timeZone: tags.tz ?? null, | ||||||
|       }, |       }, | ||||||
|       tags, |       tags, | ||||||
|     }; |     }; | ||||||
|  | |||||||
| @ -0,0 +1,16 @@ | |||||||
|  | import { MigrationInterface, QueryRunner } from 'typeorm'; | ||||||
|  | 
 | ||||||
|  | export class RemoveInvalidCoordinates1695660378655 implements MigrationInterface { | ||||||
|  |   name = 'RemoveInvalidCoordinates1695660378655'; | ||||||
|  | 
 | ||||||
|  |   public async up(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |     await queryRunner.query(`UPDATE "exif" SET "latitude" = NULL WHERE "latitude" IN ('NaN', 'Infinity', '-Infinity')`); | ||||||
|  |     await queryRunner.query( | ||||||
|  |       `UPDATE "exif" SET "longitude" = NULL WHERE "longitude" IN ('NaN', 'Infinity', '-Infinity')`, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async down(): Promise<void> { | ||||||
|  |     // Empty, data cannot be restored
 | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -74,7 +74,7 @@ export class MetadataRepository implements IMetadataRepository { | |||||||
| 
 | 
 | ||||||
|   getExifTags(path: string): Promise<ImmichTags | null> { |   getExifTags(path: string): Promise<ImmichTags | null> { | ||||||
|     return exiftool |     return exiftool | ||||||
|       .read<ImmichTags>(path, undefined, { |       .read(path, undefined, { | ||||||
|         ...DefaultReadTaskOptions, |         ...DefaultReadTaskOptions, | ||||||
| 
 | 
 | ||||||
|         defaultVideosToUTC: true, |         defaultVideosToUTC: true, | ||||||
| @ -87,6 +87,6 @@ export class MetadataRepository implements IMetadataRepository { | |||||||
|       .catch((error) => { |       .catch((error) => { | ||||||
|         this.logger.warn(`Error reading exif data (${path}): ${error}`, error?.stack); |         this.logger.warn(`Error reading exif data (${path}): ${error}`, error?.stack); | ||||||
|         return null; |         return null; | ||||||
|       }); |       }) as Promise<ImmichTags | null>; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user