diff --git a/server/src/domain/metadata/metadata.repository.ts b/server/src/domain/metadata/metadata.repository.ts index bd82e8b64e..a037964f47 100644 --- a/server/src/domain/metadata/metadata.repository.ts +++ b/server/src/domain/metadata/metadata.repository.ts @@ -14,13 +14,14 @@ export interface ReverseGeocodeResult { city: string | null; } -export interface ImmichTags extends Tags { +export interface ImmichTags extends Omit { ContentIdentifier?: string; MotionPhoto?: number; MotionPhotoVersion?: number; MotionPhotoPresentationTimestampUs?: number; MediaGroupUUID?: string; ImagePixelDepth?: string; + FocalLength?: number; } export interface IMetadataRepository { diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 0e136de656..71bfc77a0d 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -1,6 +1,6 @@ import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; 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 { constants } from 'fs/promises'; import { Duration } from 'luxon'; @@ -24,9 +24,25 @@ interface DirectoryEntry { Item: DirectoryItem; } +type ExifEntityWithoutGeocodeAndTypeOrm = Omit< + ExifEntity, + 'city' | 'state' | 'country' | 'description' | 'exifTextSearchableColumn' +>; + 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 = (value: T): T | null => (typeof value === 'string' ? null : value ?? null); + +const validate = (value: T): NonNullable | 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() export class MetadataService { @@ -184,7 +200,7 @@ export class MetadataService { return true; } - private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntity) { + private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) { const { latitude, longitude } = exifData; if (!(await this.configCore.hasFeature(FeatureFlag.REVERSE_GEOCODING)) || !longitude || !latitude) { 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 mediaTags = await this.repository.getExifTags(asset.originalPath); const sidecarTags = asset.sidecarPath ? await this.repository.getExifTags(asset.sidecarPath) : null; @@ -284,12 +302,12 @@ export class MetadataService { this.logger.verbose('Exif Tags', tags); return { - exifData: { + exifData: { // altitude: tags.GPSAltitude ?? null, assetId: asset.id, bitsPerSample: this.getBitsPerSample(tags), colorspace: tags.ColorSpace ?? null, - dateTimeOriginal: exifDate(firstDateTime(tags)) ?? asset.fileCreatedAt, + dateTimeOriginal: exifDate(firstDateTime(tags as Tags)) ?? asset.fileCreatedAt, exifImageHeight: validate(tags.ImageHeight), exifImageWidth: validate(tags.ImageWidth), exposureTime: tags.ExposureTime ?? null, @@ -308,7 +326,7 @@ export class MetadataService { orientation: validate(tags.Orientation)?.toString() ?? null, profileDescription: tags.ProfileDescription || tags.ProfileName || null, projectionType: tags.ProjectionType ? String(tags.ProjectionType).toUpperCase() : null, - timeZone: tags.tz, + timeZone: tags.tz ?? null, }, tags, }; diff --git a/server/src/infra/migrations/1695660378655-RemoveInvalidCoordinates.ts b/server/src/infra/migrations/1695660378655-RemoveInvalidCoordinates.ts new file mode 100644 index 0000000000..20b179ceb6 --- /dev/null +++ b/server/src/infra/migrations/1695660378655-RemoveInvalidCoordinates.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveInvalidCoordinates1695660378655 implements MigrationInterface { + name = 'RemoveInvalidCoordinates1695660378655'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + // Empty, data cannot be restored + } +} diff --git a/server/src/infra/repositories/metadata.repository.ts b/server/src/infra/repositories/metadata.repository.ts index 60c9e6bdb1..ae4bd6a3e6 100644 --- a/server/src/infra/repositories/metadata.repository.ts +++ b/server/src/infra/repositories/metadata.repository.ts @@ -74,7 +74,7 @@ export class MetadataRepository implements IMetadataRepository { getExifTags(path: string): Promise { return exiftool - .read(path, undefined, { + .read(path, undefined, { ...DefaultReadTaskOptions, defaultVideosToUTC: true, @@ -87,6 +87,6 @@ export class MetadataRepository implements IMetadataRepository { .catch((error) => { this.logger.warn(`Error reading exif data (${path}): ${error}`, error?.stack); return null; - }); + }) as Promise; } }