mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05: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