mirror of
https://github.com/immich-app/immich.git
synced 2024-11-16 02:18:50 -07: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…
Reference in New Issue
Block a user