mirror of
https://github.com/immich-app/immich.git
synced 2026-05-25 09:02:31 -04:00
fix(server): better metadata extraction for images (#2653)
This commit is contained in:
@@ -15,14 +15,17 @@ import { Inject, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import tz_lookup from '@photostructure/tz-lookup';
|
||||
import { ExifDateTime, exiftool, Tags } from 'exiftool-vendored';
|
||||
import { exiftool, Tags } from 'exiftool-vendored';
|
||||
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
|
||||
import { Duration } from 'luxon';
|
||||
import fs from 'node:fs';
|
||||
import sharp from 'sharp';
|
||||
import { Repository } from 'typeorm/repository/Repository';
|
||||
import { promisify } from 'util';
|
||||
import { parseLatitude, parseLongitude } from '../utils/coordinates';
|
||||
import { parseLatitude, parseLongitude } from '../utils/exif/coordinates';
|
||||
import { exifTimeZone, exifToDate } from '../utils/exif/date-time';
|
||||
import { parseISO } from '../utils/exif/iso';
|
||||
import { toNumberOrNull } from '../utils/numbers';
|
||||
|
||||
const ffprobe = promisify<string, FfprobeData>(ffmpeg.ffprobe);
|
||||
|
||||
@@ -116,32 +119,13 @@ export class MetadataExtractionProcessor {
|
||||
})
|
||||
: {};
|
||||
|
||||
const exifToDate = (exifDate: string | Date | ExifDateTime | undefined) => {
|
||||
if (!exifDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const date = exifDate instanceof ExifDateTime ? exifDate.toDate() : new Date(exifDate);
|
||||
if (isNaN(date.valueOf())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return date;
|
||||
};
|
||||
|
||||
const exifTimeZone = (exifDate: string | Date | ExifDateTime | undefined) => {
|
||||
const isExifDate = exifDate instanceof ExifDateTime;
|
||||
if (!isExifDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return exifDate.zone ?? null;
|
||||
};
|
||||
|
||||
const getExifProperty = <T extends keyof ImmichTags>(...properties: T[]): any | null => {
|
||||
const getExifProperty = <T extends keyof ImmichTags>(
|
||||
...properties: T[]
|
||||
): NonNullable<ImmichTags[T]> | string | null => {
|
||||
for (const property of properties) {
|
||||
const value = sidecarExifData?.[property] ?? mediaExifData?.[property];
|
||||
if (value !== null && value !== undefined) {
|
||||
// Can also be string when the value cannot be parsed
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -160,25 +144,27 @@ export class MetadataExtractionProcessor {
|
||||
newExif.fileSizeInByte = fileSizeInBytes;
|
||||
newExif.make = getExifProperty('Make');
|
||||
newExif.model = getExifProperty('Model');
|
||||
newExif.exifImageHeight = getExifProperty('ExifImageHeight', 'ImageHeight');
|
||||
newExif.exifImageWidth = getExifProperty('ExifImageWidth', 'ImageWidth');
|
||||
newExif.exifImageHeight = toNumberOrNull(getExifProperty('ExifImageHeight', 'ImageHeight'));
|
||||
newExif.exifImageWidth = toNumberOrNull(getExifProperty('ExifImageWidth', 'ImageWidth'));
|
||||
newExif.exposureTime = getExifProperty('ExposureTime');
|
||||
newExif.orientation = getExifProperty('Orientation')?.toString();
|
||||
newExif.orientation = getExifProperty('Orientation')?.toString() ?? null;
|
||||
newExif.dateTimeOriginal = fileCreatedAt;
|
||||
newExif.modifyDate = fileModifiedAt;
|
||||
newExif.timeZone = timeZone;
|
||||
newExif.lensModel = getExifProperty('LensModel');
|
||||
newExif.fNumber = getExifProperty('FNumber');
|
||||
const focalLength = getExifProperty('FocalLength');
|
||||
newExif.focalLength = focalLength ? parseFloat(focalLength) : null;
|
||||
// This is unusual - exifData.ISO should return a number, but experienced that sidecar XMP
|
||||
// files MAY return an array of numbers instead.
|
||||
const iso = getExifProperty('ISO');
|
||||
newExif.iso = Array.isArray(iso) ? iso[0] : iso || null;
|
||||
newExif.latitude = parseLatitude(getExifProperty('GPSLatitude'));
|
||||
newExif.longitude = parseLongitude(getExifProperty('GPSLongitude'));
|
||||
newExif.livePhotoCID = getExifProperty('MediaGroupUUID');
|
||||
newExif.fNumber = toNumberOrNull(getExifProperty('FNumber'));
|
||||
newExif.focalLength = toNumberOrNull(getExifProperty('FocalLength'));
|
||||
|
||||
// Handle array values by converting to string
|
||||
const iso = getExifProperty('ISO')?.toString();
|
||||
newExif.iso = iso ? parseISO(iso) : null;
|
||||
|
||||
const latitude = getExifProperty('GPSLatitude');
|
||||
const longitude = getExifProperty('GPSLongitude');
|
||||
newExif.latitude = latitude !== null ? parseLatitude(latitude) : null;
|
||||
newExif.longitude = longitude !== null ? parseLongitude(longitude) : null;
|
||||
|
||||
newExif.livePhotoCID = getExifProperty('MediaGroupUUID');
|
||||
if (newExif.livePhotoCID && !asset.livePhotoVideoId) {
|
||||
const motionAsset = await this.assetRepository.findLivePhotoMatch({
|
||||
livePhotoCID: newExif.livePhotoCID,
|
||||
|
||||
Reference in New Issue
Block a user