diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index f036264915..0cfdc759bc 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -188,16 +188,12 @@ export class AssetService { isVisible: boolean, livePhotoAssetEntity?: AssetEntity, ): Promise { - // Check valid time. - const createdAt = createAssetDto.createdAt; - const modifiedAt = createAssetDto.modifiedAt; - - if (!timeUtils.checkValidTimestamp(createdAt)) { - createAssetDto.createdAt = await timeUtils.getTimestampFromExif(originalPath); + if (!timeUtils.checkValidTimestamp(createAssetDto.createdAt)) { + createAssetDto.createdAt = new Date().toISOString(); } - if (!timeUtils.checkValidTimestamp(modifiedAt)) { - createAssetDto.modifiedAt = await timeUtils.getTimestampFromExif(originalPath); + if (!timeUtils.checkValidTimestamp(createAssetDto.modifiedAt)) { + createAssetDto.modifiedAt = new Date().toISOString(); } const assetEntity = await this._assetRepository.create( diff --git a/server/apps/microservices/src/processors/metadata-extraction.processor.ts b/server/apps/microservices/src/processors/metadata-extraction.processor.ts index 7cf4aad07e..e9b42017de 100644 --- a/server/apps/microservices/src/processors/metadata-extraction.processor.ts +++ b/server/apps/microservices/src/processors/metadata-extraction.processor.ts @@ -18,8 +18,7 @@ import { Repository } from 'typeorm/repository/Repository'; import geocoder, { InitOptions } from 'local-reverse-geocoder'; import { getName } from 'i18n-iso-countries'; import fs from 'node:fs'; -import { ExifDateTime, ExifTool } from 'exiftool-vendored'; -import { timeUtils } from '@app/common'; +import { ExifDateTime, exiftool } from 'exiftool-vendored'; function geocoderInit(init: InitOptions) { return new Promise(function (resolve) { @@ -140,43 +139,52 @@ export class MetadataExtractionProcessor { async extractExifInfo(job: Job) { try { const { asset, fileName }: { asset: AssetEntity; fileName: string } = job.data; - const exiftool = new ExifTool(); + const exifData = await exiftool.read(asset.originalPath).catch((e) => { this.logger.warn(`The exifData parsing failed due to: ${e} on file ${asset.originalPath}`); + return null; }); - const exifToDate = (exifDate: string | ExifDateTime | undefined) => - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - exifDate ? new Date(exifDate.toString()!) : null; + const exifToDate = (exifDate: string | ExifDateTime | undefined) => { + if (!exifDate) return null; - let createdAt = exifToDate(asset.createdAt); - const newExif = new ExifEntity(); - if (exifData) { - createdAt = exifToDate(exifData.DateTimeOriginal ?? exifData.CreateDate ?? asset.createdAt); - const modifyDate = exifToDate(exifData.ModifyDate); - newExif.make = exifData['Make'] || null; - newExif.model = exifData['Model'] || null; - newExif.exifImageHeight = exifData['ExifImageHeight'] || exifData['ImageHeight'] || null; - newExif.exifImageWidth = exifData['ExifImageWidth'] || exifData['ImageWidth'] || null; - newExif.exposureTime = (await timeUtils.parseStringToNumber(exifData['ExposureTime'])) || null; - newExif.orientation = exifData['Orientation']?.toString() || null; - newExif.dateTimeOriginal = createdAt; - newExif.modifyDate = modifyDate || null; - newExif.lensModel = exifData['LensModel'] || null; - newExif.fNumber = exifData['FNumber'] || null; - newExif.focalLength = (await timeUtils.parseStringToNumber(exifData['FocalLength'])) || null; - newExif.iso = exifData['ISO'] || null; - newExif.latitude = exifData['GPSLatitude'] || null; - newExif.longitude = exifData['GPSLongitude'] || null; - } else { - newExif.dateTimeOriginal = createdAt; - newExif.modifyDate = exifToDate(asset.modifiedAt); - } + if (typeof exifDate === 'string') { + return new Date(exifDate); + } + + return exifDate.toDate(); + }; + + const getExposureTimeDenominator = (exposureTime: string | undefined) => { + if (!exposureTime) return null; + + const exposureTimeSplit = exposureTime.split('/'); + return exposureTimeSplit.length === 2 ? parseInt(exposureTimeSplit[1]) : null; + }; + + const createdAt = exifToDate(exifData?.DateTimeOriginal ?? exifData?.CreateDate ?? asset.createdAt); + const modifyDate = exifToDate(exifData?.ModifyDate ?? asset.modifiedAt); const fileStats = fs.statSync(asset.originalPath); const fileSizeInBytes = fileStats.size; + + const newExif = new ExifEntity(); newExif.assetId = asset.id; - newExif.imageName = path.parse(fileName).name || null; - newExif.fileSizeInByte = fileSizeInBytes || null; + newExif.imageName = path.parse(fileName).name; + newExif.fileSizeInByte = fileSizeInBytes; + newExif.make = exifData?.Make || null; + newExif.model = exifData?.Model || null; + newExif.exifImageHeight = exifData?.ExifImageHeight || exifData?.ImageHeight || null; + newExif.exifImageWidth = exifData?.ExifImageWidth || exifData?.ImageWidth || null; + newExif.exposureTime = getExposureTimeDenominator(exifData?.ExposureTime); + newExif.orientation = exifData?.Orientation?.toString() || null; + newExif.dateTimeOriginal = createdAt; + newExif.modifyDate = modifyDate; + newExif.lensModel = exifData?.LensModel || null; + newExif.fNumber = exifData?.FNumber || null; + newExif.focalLength = exifData?.FocalLength ? parseFloat(exifData.FocalLength) : null; + newExif.iso = exifData?.ISO || null; + newExif.latitude = exifData?.GPSLatitude || null; + newExif.longitude = exifData?.GPSLongitude || null; await this.assetRepository.save({ id: asset.id, @@ -217,7 +225,6 @@ export class MetadataExtractionProcessor { } await this.exifRepository.save(newExif); - await exiftool.end(); } catch (error: any) { this.logger.error(`Error extracting EXIF ${error}`, error?.stack); } diff --git a/server/libs/common/src/utils/time-utils.ts b/server/libs/common/src/utils/time-utils.ts index 1f7c5e2b8d..1f75f5c29d 100644 --- a/server/libs/common/src/utils/time-utils.ts +++ b/server/libs/common/src/utils/time-utils.ts @@ -1,12 +1,4 @@ -// This is needed as resolving for the vendored -// exiftool fails in tests otherwise but as it's not meant to be a requirement -// of a project directly I had to include the line below the comment. -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import { exiftool } from 'exiftool-vendored.pl'; - function createTimeUtils() { - const floatRegex = /[+-]?([0-9]*[.])?[0-9]+/; const checkValidTimestamp = (timestamp: string): boolean => { const parsedTimestamp = Date.parse(timestamp); @@ -23,32 +15,7 @@ function createTimeUtils() { return date.getFullYear() > 0; }; - const getTimestampFromExif = async (originalPath: string): Promise => { - try { - const exifData = await exiftool.read(originalPath); - - if (exifData && exifData['DateTimeOriginal']) { - await exiftool.end(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return exifData['DateTimeOriginal'].toString()!; - } else { - return new Date().toISOString(); - } - } catch (error) { - return new Date().toISOString(); - } - }; - - const parseStringToNumber = async (original: string | undefined): Promise => { - const match = original?.match(floatRegex)?.[0]; - if (match) { - return parseFloat(match); - } else { - return null; - } - }; - - return { checkValidTimestamp, getTimestampFromExif, parseStringToNumber }; + return { checkValidTimestamp }; } export const timeUtils = createTimeUtils(); diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 0ad2979894..59c0c95348 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -152,7 +152,7 @@

{`ƒ/${asset.exifInfo.fNumber.toLocaleString(locale)}` || ''}

{#if asset.exifInfo.exposureTime} -

{`1/${Math.floor(1 / asset.exifInfo.exposureTime)}`}

+

{`1/${asset.exifInfo.exposureTime}`}

{/if} {#if asset.exifInfo.focalLength}