fix(server): handle NaN in metadata extraction (#4221)

Fallback to null in event of invalid number.
This commit is contained in:
David Johnson 2023-09-27 15:17:18 -04:00 committed by GitHub
parent 3a44e8f8d3
commit 85efbc6984
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 46 additions and 11 deletions

View File

@ -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 {

View File

@ -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,
}; };

View File

@ -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
}
}

View File

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