mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-26 08:24:27 -04:00 
			
		
		
		
	feat(server): Fix exif data parsing (#1326)
* Trying to get exifdata working with different lib. * Got the new library working. * Addressing PR comments. * Removed not used vars and proper place for the eslint disable. * Fix time-utils to use the exiftool-vendored lib. Fixed also one test, as that would be valid. * Using filename for timestamp as well if possible. * Add new tests for time-utils. * Remember to gracefully terminate the exiftool instance when not needed. * eslint ignore... * Apperantly Dockerfile changes were not pushed. * feat(dockerfile): Tweak the Server Dockerfile * feat(server): getTimestampFromFilename should return string or undefined. * feat(server): If we don't have exifData or timestamp from filename, raise an error. * Apparently test was already right, but my local system disagrees. * More utilities for parsing and fix the timestampFromFilename. It was returning an incorrect date as the regex doesn't seem to be the best for this as files named `IMG_0115.HEIC` will want to get parsed incorrectly due to it. * feat(server/docker): Install perl as it seems to be required. * feat(server): remember to include exposureTime and focalLength in new exif data. * feat(server): Remove the parsing from filename as requested. * feat(server): Import exiftool differently in time-utils. * feat(server): Error handling when there is no exifData. * feat(server): Fixes for the error handling when there is no exifData. * feat(server): Remember to include modifyDate despite no exif. * feat(server): Remember to include model of Camera. * feat(server): Fixing up Exiftool usage. Including proper logging for it, which had to be done in wrapped fashion due to it expecting all the logging levels which NextJS logger doesn't implement. * feat(server): Do not use a wrapper for ExifTool logging. * fix merge conflicts in metadata-extractor
This commit is contained in:
		
							parent
							
								
									693adf8488
								
							
						
					
					
						commit
						dff10e89fe
					
				| @ -2,7 +2,7 @@ FROM node:16-alpine3.14 as builder | |||||||
| 
 | 
 | ||||||
| WORKDIR /usr/src/app | WORKDIR /usr/src/app | ||||||
| 
 | 
 | ||||||
| RUN apk add --update-cache build-base python3 libheif vips-dev ffmpeg | RUN apk add --update-cache build-base python3 libheif vips-dev ffmpeg exiftool perl | ||||||
| 
 | 
 | ||||||
| COPY package.json package-lock.json ./ | COPY package.json package-lock.json ./ | ||||||
| 
 | 
 | ||||||
| @ -21,7 +21,7 @@ FROM node:16-alpine3.14 | |||||||
| 
 | 
 | ||||||
| WORKDIR /usr/src/app | WORKDIR /usr/src/app | ||||||
| 
 | 
 | ||||||
| RUN apk add --no-cache libheif vips ffmpeg | RUN apk add --no-cache libheif vips ffmpeg exiftool perl | ||||||
| 
 | 
 | ||||||
| COPY --from=prod /usr/src/app/node_modules ./node_modules | COPY --from=prod /usr/src/app/node_modules ./node_modules | ||||||
| COPY --from=prod /usr/src/app/dist ./dist | COPY --from=prod /usr/src/app/dist ./dist | ||||||
|  | |||||||
| @ -1,8 +1,8 @@ | |||||||
| import { AssetEntity, ExifEntity } from '@app/infra'; | import { AssetEntity, ExifEntity } from '@app/infra'; | ||||||
| import { | import { | ||||||
|   IExifExtractionProcessor, |   IExifExtractionProcessor, | ||||||
|   IVideoLengthExtractionProcessor, |  | ||||||
|   IReverseGeocodingProcessor, |   IReverseGeocodingProcessor, | ||||||
|  |   IVideoLengthExtractionProcessor, | ||||||
|   QueueName, |   QueueName, | ||||||
|   JobName, |   JobName, | ||||||
| } from '@app/job'; | } from '@app/job'; | ||||||
| @ -11,16 +11,15 @@ import { Logger } from '@nestjs/common'; | |||||||
| import { ConfigService } from '@nestjs/config'; | import { ConfigService } from '@nestjs/config'; | ||||||
| import { InjectRepository } from '@nestjs/typeorm'; | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
| import { Job } from 'bull'; | import { Job } from 'bull'; | ||||||
| import exifr from 'exifr'; |  | ||||||
| import ffmpeg from 'fluent-ffmpeg'; | import ffmpeg from 'fluent-ffmpeg'; | ||||||
| import path from 'path'; | import path from 'path'; | ||||||
| import sharp from 'sharp'; | import sharp from 'sharp'; | ||||||
| import { Repository } from 'typeorm/repository/Repository'; | import { Repository } from 'typeorm/repository/Repository'; | ||||||
| import geocoder, { InitOptions } from 'local-reverse-geocoder'; | import geocoder, { InitOptions } from 'local-reverse-geocoder'; | ||||||
| import { getName } from 'i18n-iso-countries'; | import { getName } from 'i18n-iso-countries'; | ||||||
| import { find } from 'geo-tz'; |  | ||||||
| import * as luxon from 'luxon'; |  | ||||||
| import fs from 'node:fs'; | import fs from 'node:fs'; | ||||||
|  | import { ExifDateTime, ExifTool } from 'exiftool-vendored'; | ||||||
|  | import { timeUtils } from '@app/common'; | ||||||
| 
 | 
 | ||||||
| function geocoderInit(init: InitOptions) { | function geocoderInit(init: InitOptions) { | ||||||
|   return new Promise<void>(function (resolve) { |   return new Promise<void>(function (resolve) { | ||||||
| @ -75,7 +74,6 @@ export type GeoData = { | |||||||
| export class MetadataExtractionProcessor { | export class MetadataExtractionProcessor { | ||||||
|   private logger = new Logger(MetadataExtractionProcessor.name); |   private logger = new Logger(MetadataExtractionProcessor.name); | ||||||
|   private isGeocodeInitialized = false; |   private isGeocodeInitialized = false; | ||||||
| 
 |  | ||||||
|   constructor( |   constructor( | ||||||
|     @InjectRepository(AssetEntity) |     @InjectRepository(AssetEntity) | ||||||
|     private assetRepository: Repository<AssetEntity>, |     private assetRepository: Repository<AssetEntity>, | ||||||
| @ -102,7 +100,7 @@ export class MetadataExtractionProcessor { | |||||||
|           configService.get('REVERSE_GEOCODING_DUMP_DIRECTORY') || process.cwd() + '/.reverse-geocoding-dump/', |           configService.get('REVERSE_GEOCODING_DUMP_DIRECTORY') || process.cwd() + '/.reverse-geocoding-dump/', | ||||||
|       }).then(() => { |       }).then(() => { | ||||||
|         this.isGeocodeInitialized = true; |         this.isGeocodeInitialized = true; | ||||||
|         Logger.log('Reverse Geocoding Initialised'); |         this.logger.log('Reverse Geocoding Initialised'); | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -142,84 +140,48 @@ export class MetadataExtractionProcessor { | |||||||
|   async extractExifInfo(job: Job<IExifExtractionProcessor>) { |   async extractExifInfo(job: Job<IExifExtractionProcessor>) { | ||||||
|     try { |     try { | ||||||
|       const { asset, fileName }: { asset: AssetEntity; fileName: string } = job.data; |       const { asset, fileName }: { asset: AssetEntity; fileName: string } = job.data; | ||||||
|       const exifData = await exifr.parse(asset.originalPath, { |       const exiftool = new ExifTool(); | ||||||
|         tiff: true, |       const exifData = await exiftool.read(asset.originalPath).catch((e) => { | ||||||
|         ifd0: true as any, |         this.logger.warn(`The exifData parsing failed due to: ${e} on file ${asset.originalPath}`); | ||||||
|         ifd1: true, |  | ||||||
|         exif: true, |  | ||||||
|         gps: true, |  | ||||||
|         interop: true, |  | ||||||
|         xmp: true, |  | ||||||
|         icc: true, |  | ||||||
|         iptc: true, |  | ||||||
|         jfif: true, |  | ||||||
|         ihdr: true, |  | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       if (!exifData) { |       const exifToDate = (exifDate: string | ExifDateTime | undefined) => | ||||||
|         throw new Error(`can not parse exif data from file ${asset.originalPath}`); |         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||||
|       } |         exifDate ? new Date(exifDate.toString()!) : null; | ||||||
| 
 |  | ||||||
|       const createdAt = new Date(exifData.DateTimeOriginal || exifData.CreateDate || new Date(asset.createdAt)); |  | ||||||
| 
 |  | ||||||
|       const fileStats = fs.statSync(asset.originalPath); |  | ||||||
|       const fileSizeInBytes = fileStats.size; |  | ||||||
| 
 | 
 | ||||||
|  |       let createdAt = exifToDate(asset.createdAt); | ||||||
|       const newExif = new ExifEntity(); |       const newExif = new ExifEntity(); | ||||||
|       newExif.assetId = asset.id; |       if (exifData) { | ||||||
|  |         createdAt = exifToDate(exifData.DateTimeOriginal ?? exifData.CreateDate ?? asset.createdAt); | ||||||
|  |         const modifyDate = exifToDate(exifData.ModifyDate); | ||||||
|         newExif.make = exifData['Make'] || null; |         newExif.make = exifData['Make'] || null; | ||||||
|         newExif.model = exifData['Model'] || null; |         newExif.model = exifData['Model'] || null; | ||||||
|       newExif.imageName = path.parse(fileName).name || null; |  | ||||||
|         newExif.exifImageHeight = exifData['ExifImageHeight'] || exifData['ImageHeight'] || null; |         newExif.exifImageHeight = exifData['ExifImageHeight'] || exifData['ImageHeight'] || null; | ||||||
|         newExif.exifImageWidth = exifData['ExifImageWidth'] || exifData['ImageWidth'] || null; |         newExif.exifImageWidth = exifData['ExifImageWidth'] || exifData['ImageWidth'] || null; | ||||||
|       newExif.fileSizeInByte = fileSizeInBytes || null; |         newExif.exposureTime = (await timeUtils.parseStringToNumber(exifData['ExposureTime'])) || null; | ||||||
|       newExif.orientation = exifData['Orientation'] || null; |         newExif.orientation = exifData['Orientation']?.toString() || null; | ||||||
|         newExif.dateTimeOriginal = createdAt; |         newExif.dateTimeOriginal = createdAt; | ||||||
|       newExif.modifyDate = exifData['ModifyDate'] || null; |         newExif.modifyDate = modifyDate || null; | ||||||
|         newExif.lensModel = exifData['LensModel'] || null; |         newExif.lensModel = exifData['LensModel'] || null; | ||||||
|         newExif.fNumber = exifData['FNumber'] || null; |         newExif.fNumber = exifData['FNumber'] || null; | ||||||
|       newExif.focalLength = exifData['FocalLength'] || null; |         newExif.focalLength = (await timeUtils.parseStringToNumber(exifData['FocalLength'])) || null; | ||||||
|         newExif.iso = exifData['ISO'] || null; |         newExif.iso = exifData['ISO'] || null; | ||||||
|       newExif.exposureTime = exifData['ExposureTime'] || null; |         newExif.latitude = exifData['GPSLatitude'] || null; | ||||||
|       newExif.latitude = exifData['latitude'] || null; |         newExif.longitude = exifData['GPSLongitude'] || null; | ||||||
|       newExif.longitude = exifData['longitude'] || null; |  | ||||||
| 
 |  | ||||||
|       /** |  | ||||||
|        * Correctly store UTC time based on timezone |  | ||||||
|        * The timestamp being extracted from EXIF is based on the timezone |  | ||||||
|        * of the container. We need to correct it to UTC time based on the |  | ||||||
|        * timezone of the location. |  | ||||||
|        * |  | ||||||
|        * The timezone of the location can be exracted from the lat/lon |  | ||||||
|        * GPS coordinates. |  | ||||||
|        * |  | ||||||
|        * Any assets that doesn't have this information will used the |  | ||||||
|        * createdAt timestamp of the asset instead. |  | ||||||
|        * |  | ||||||
|        * The updated/corrected timestamp will be used to update the |  | ||||||
|        * createdAt timestamp in the asset table. So that the information |  | ||||||
|        * is consistent across the database. |  | ||||||
|        *  */ |  | ||||||
|       if (newExif.longitude && newExif.latitude) { |  | ||||||
|         const tz = find(newExif.latitude, newExif.longitude)[0]; |  | ||||||
|         const localTimeWithTimezone = createdAt.toISOString(); |  | ||||||
| 
 |  | ||||||
|         if (localTimeWithTimezone.length == 24) { |  | ||||||
|           // Remove the last character
 |  | ||||||
|           const localTimeWithoutTimezone = localTimeWithTimezone.slice(0, -1); |  | ||||||
|           const correctUTCTime = luxon.DateTime.fromISO(localTimeWithoutTimezone, { zone: tz }).toUTC().toISO(); |  | ||||||
|           newExif.dateTimeOriginal = new Date(correctUTCTime); |  | ||||||
|           await this.assetRepository.save({ |  | ||||||
|             id: asset.id, |  | ||||||
|             createdAt: correctUTCTime, |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|       } else { |       } else { | ||||||
|  |         newExif.dateTimeOriginal = createdAt; | ||||||
|  |         newExif.modifyDate = exifToDate(asset.modifiedAt); | ||||||
|  |       } | ||||||
|  |       const fileStats = fs.statSync(asset.originalPath); | ||||||
|  |       const fileSizeInBytes = fileStats.size; | ||||||
|  |       newExif.assetId = asset.id; | ||||||
|  |       newExif.imageName = path.parse(fileName).name || null; | ||||||
|  |       newExif.fileSizeInByte = fileSizeInBytes || null; | ||||||
|  | 
 | ||||||
|       await this.assetRepository.save({ |       await this.assetRepository.save({ | ||||||
|         id: asset.id, |         id: asset.id, | ||||||
|           createdAt: createdAt.toISOString(), |         createdAt: createdAt?.toISOString(), | ||||||
|       }); |       }); | ||||||
|       } |  | ||||||
| 
 | 
 | ||||||
|       /** |       /** | ||||||
|        * Reverse Geocoding |        * Reverse Geocoding | ||||||
| @ -255,6 +217,7 @@ export class MetadataExtractionProcessor { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       await this.exifRepository.save(newExif); |       await this.exifRepository.save(newExif); | ||||||
|  |       await exiftool.end(); | ||||||
|     } catch (error: any) { |     } catch (error: any) { | ||||||
|       this.logger.error(`Error extracting EXIF ${error}`, error?.stack); |       this.logger.error(`Error extracting EXIF ${error}`, error?.stack); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,6 +1,12 @@ | |||||||
| import exifr from 'exifr'; | // 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() { | function createTimeUtils() { | ||||||
|  |   const floatRegex = /[+-]?([0-9]*[.])?[0-9]+/; | ||||||
|   const checkValidTimestamp = (timestamp: string): boolean => { |   const checkValidTimestamp = (timestamp: string): boolean => { | ||||||
|     const parsedTimestamp = Date.parse(timestamp); |     const parsedTimestamp = Date.parse(timestamp); | ||||||
| 
 | 
 | ||||||
| @ -19,22 +25,12 @@ function createTimeUtils() { | |||||||
| 
 | 
 | ||||||
|   const getTimestampFromExif = async (originalPath: string): Promise<string> => { |   const getTimestampFromExif = async (originalPath: string): Promise<string> => { | ||||||
|     try { |     try { | ||||||
|       const exifData = await exifr.parse(originalPath, { |       const exifData = await exiftool.read(originalPath); | ||||||
|         tiff: true, |  | ||||||
|         ifd0: true as any, |  | ||||||
|         ifd1: true, |  | ||||||
|         exif: true, |  | ||||||
|         gps: true, |  | ||||||
|         interop: true, |  | ||||||
|         xmp: true, |  | ||||||
|         icc: true, |  | ||||||
|         iptc: true, |  | ||||||
|         jfif: true, |  | ||||||
|         ihdr: true, |  | ||||||
|       }); |  | ||||||
| 
 | 
 | ||||||
|       if (exifData && exifData['DateTimeOriginal']) { |       if (exifData && exifData['DateTimeOriginal']) { | ||||||
|         return exifData['DateTimeOriginal']; |         await exiftool.end(); | ||||||
|  |         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||||
|  |         return exifData['DateTimeOriginal'].toString()!; | ||||||
|       } else { |       } else { | ||||||
|         return new Date().toISOString(); |         return new Date().toISOString(); | ||||||
|       } |       } | ||||||
| @ -42,7 +38,17 @@ function createTimeUtils() { | |||||||
|       return new Date().toISOString(); |       return new Date().toISOString(); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|   return { checkValidTimestamp, getTimestampFromExif }; | 
 | ||||||
|  |   const parseStringToNumber = async (original: string | undefined): Promise<number | null> => { | ||||||
|  |     const match = original?.match(floatRegex)?.[0]; | ||||||
|  |     if (match) { | ||||||
|  |       return parseFloat(match); | ||||||
|  |     } else { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return { checkValidTimestamp, getTimestampFromExif, parseStringToNumber }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const timeUtils = createTimeUtils(); | export const timeUtils = createTimeUtils(); | ||||||
|  | |||||||
							
								
								
									
										118
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										118
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -32,7 +32,7 @@ | |||||||
|         "cookie-parser": "^1.4.6", |         "cookie-parser": "^1.4.6", | ||||||
|         "diskusage": "^1.1.3", |         "diskusage": "^1.1.3", | ||||||
|         "dotenv": "^14.2.0", |         "dotenv": "^14.2.0", | ||||||
|         "exifr": "^7.1.3", |         "exiftool-vendored": "^19.0.0", | ||||||
|         "fdir": "^5.3.0", |         "fdir": "^5.3.0", | ||||||
|         "fluent-ffmpeg": "^2.1.2", |         "fluent-ffmpeg": "^2.1.2", | ||||||
|         "geo-tz": "^7.0.2", |         "geo-tz": "^7.0.2", | ||||||
| @ -2237,6 +2237,11 @@ | |||||||
|       "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", |       "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", | ||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/@photostructure/tz-lookup": { | ||||||
|  |       "version": "7.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-7.0.0.tgz", | ||||||
|  |       "integrity": "sha512-pTRsZz7Sn4yAtItC7I4+0segDHosMyOtJgAXg+xvDOolT0Xz4IFWqBV33OMCWoaNd3oQb60wbWhLeCQgJCyZAA==" | ||||||
|  |     }, | ||||||
|     "node_modules/@redis/bloom": { |     "node_modules/@redis/bloom": { | ||||||
|       "version": "1.1.0", |       "version": "1.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.1.0.tgz", |       "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.1.0.tgz", | ||||||
| @ -2724,10 +2729,9 @@ | |||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "node_modules/@types/luxon": { |     "node_modules/@types/luxon": { | ||||||
|       "version": "2.3.2", |       "version": "3.2.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.2.tgz", |       "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.2.0.tgz", | ||||||
|       "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==", |       "integrity": "sha512-lGmaGFoaXHuOLXFvuju2bfvZRqxAqkHPx9Y9IQdQABrinJJshJwfNCKV+u7rR3kJbiqfTF/NhOkcxxAFrObyaA==" | ||||||
|       "dev": true |  | ||||||
|     }, |     }, | ||||||
|     "node_modules/@types/mime": { |     "node_modules/@types/mime": { | ||||||
|       "version": "1.3.2", |       "version": "1.3.2", | ||||||
| @ -3775,6 +3779,14 @@ | |||||||
|         "node": "^4.5.0 || >= 5.9" |         "node": "^4.5.0 || >= 5.9" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/batch-cluster": { | ||||||
|  |       "version": "11.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-11.0.0.tgz", | ||||||
|  |       "integrity": "sha512-8iwqa+rKTaakOHkqdcXDT5L5117pa+FoP8/yAKpNdL44ZnC4V2NEA/sIg0ZO0O9NkpdjLk0A3efRFM5nVizqHw==", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/bcrypt": { |     "node_modules/bcrypt": { | ||||||
|       "version": "5.0.1", |       "version": "5.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", |       "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", | ||||||
| @ -5629,10 +5641,39 @@ | |||||||
|         "url": "https://github.com/sindresorhus/execa?sponsor=1" |         "url": "https://github.com/sindresorhus/execa?sponsor=1" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/exifr": { |     "node_modules/exiftool-vendored": { | ||||||
|       "version": "7.1.3", |       "version": "19.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz", |       "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-19.0.0.tgz", | ||||||
|       "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==" |       "integrity": "sha512-Zes7TZrYWxts92mbF2Gs3drtWZucm4qsaeYaE6A+OOqmeD9UGaGisqIbyh9MilJrLi+ZHzWEJZtDj37QFf6xsA==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@photostructure/tz-lookup": "^7.0.0", | ||||||
|  |         "@types/luxon": "^3.2.0", | ||||||
|  |         "batch-cluster": "^11.0.0", | ||||||
|  |         "he": "^1.2.0", | ||||||
|  |         "luxon": "^3.2.1" | ||||||
|  |       }, | ||||||
|  |       "optionalDependencies": { | ||||||
|  |         "exiftool-vendored.exe": "12.54.0", | ||||||
|  |         "exiftool-vendored.pl": "12.54.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/exiftool-vendored.exe": { | ||||||
|  |       "version": "12.54.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.54.0.tgz", | ||||||
|  |       "integrity": "sha512-Dc4W6e0NtQfYuJIYK4piHfDJnd2jvA04e0aaq9R3Q1oO34KC5e+L1D2C7lFuZXqPQLYC1x3GYc/GVv5e+SkkrQ==", | ||||||
|  |       "optional": true, | ||||||
|  |       "os": [ | ||||||
|  |         "win32" | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "node_modules/exiftool-vendored.pl": { | ||||||
|  |       "version": "12.54.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.54.0.tgz", | ||||||
|  |       "integrity": "sha512-RBBowsYcM6EvbWoBkg2dOqHpH3WIzN7bIzHc+o+LquqCTo3doZwECClD/6PNHVSMQsl2Z0fEf75sNq2msooMSg==", | ||||||
|  |       "optional": true, | ||||||
|  |       "os": [ | ||||||
|  |         "!win32" | ||||||
|  |       ] | ||||||
|     }, |     }, | ||||||
|     "node_modules/exit": { |     "node_modules/exit": { | ||||||
|       "version": "0.1.2", |       "version": "0.1.2", | ||||||
| @ -6481,6 +6522,14 @@ | |||||||
|       "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", |       "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", | ||||||
|       "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" |       "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/he": { | ||||||
|  |       "version": "1.2.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", | ||||||
|  |       "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", | ||||||
|  |       "bin": { | ||||||
|  |         "he": "bin/he" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/hexoid": { |     "node_modules/hexoid": { | ||||||
|       "version": "1.0.0", |       "version": "1.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", |       "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", | ||||||
| @ -13218,6 +13267,11 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "@photostructure/tz-lookup": { | ||||||
|  |       "version": "7.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-7.0.0.tgz", | ||||||
|  |       "integrity": "sha512-pTRsZz7Sn4yAtItC7I4+0segDHosMyOtJgAXg+xvDOolT0Xz4IFWqBV33OMCWoaNd3oQb60wbWhLeCQgJCyZAA==" | ||||||
|  |     }, | ||||||
|     "@redis/bloom": { |     "@redis/bloom": { | ||||||
|       "version": "1.1.0", |       "version": "1.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.1.0.tgz", |       "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.1.0.tgz", | ||||||
| @ -13676,10 +13730,9 @@ | |||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "@types/luxon": { |     "@types/luxon": { | ||||||
|       "version": "2.3.2", |       "version": "3.2.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.2.tgz", |       "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.2.0.tgz", | ||||||
|       "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==", |       "integrity": "sha512-lGmaGFoaXHuOLXFvuju2bfvZRqxAqkHPx9Y9IQdQABrinJJshJwfNCKV+u7rR3kJbiqfTF/NhOkcxxAFrObyaA==" | ||||||
|       "dev": true |  | ||||||
|     }, |     }, | ||||||
|     "@types/mime": { |     "@types/mime": { | ||||||
|       "version": "1.3.2", |       "version": "1.3.2", | ||||||
| @ -14528,6 +14581,11 @@ | |||||||
|       "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", |       "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", | ||||||
|       "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" |       "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" | ||||||
|     }, |     }, | ||||||
|  |     "batch-cluster": { | ||||||
|  |       "version": "11.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-11.0.0.tgz", | ||||||
|  |       "integrity": "sha512-8iwqa+rKTaakOHkqdcXDT5L5117pa+FoP8/yAKpNdL44ZnC4V2NEA/sIg0ZO0O9NkpdjLk0A3efRFM5nVizqHw==" | ||||||
|  |     }, | ||||||
|     "bcrypt": { |     "bcrypt": { | ||||||
|       "version": "5.0.1", |       "version": "5.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", |       "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", | ||||||
| @ -15929,10 +15987,31 @@ | |||||||
|         "strip-final-newline": "^2.0.0" |         "strip-final-newline": "^2.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "exifr": { |     "exiftool-vendored": { | ||||||
|       "version": "7.1.3", |       "version": "19.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz", |       "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-19.0.0.tgz", | ||||||
|       "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==" |       "integrity": "sha512-Zes7TZrYWxts92mbF2Gs3drtWZucm4qsaeYaE6A+OOqmeD9UGaGisqIbyh9MilJrLi+ZHzWEJZtDj37QFf6xsA==", | ||||||
|  |       "requires": { | ||||||
|  |         "@photostructure/tz-lookup": "^7.0.0", | ||||||
|  |         "@types/luxon": "^3.2.0", | ||||||
|  |         "batch-cluster": "^11.0.0", | ||||||
|  |         "exiftool-vendored.exe": "12.54.0", | ||||||
|  |         "exiftool-vendored.pl": "12.54.0", | ||||||
|  |         "he": "^1.2.0", | ||||||
|  |         "luxon": "^3.2.1" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "exiftool-vendored.exe": { | ||||||
|  |       "version": "12.54.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.54.0.tgz", | ||||||
|  |       "integrity": "sha512-Dc4W6e0NtQfYuJIYK4piHfDJnd2jvA04e0aaq9R3Q1oO34KC5e+L1D2C7lFuZXqPQLYC1x3GYc/GVv5e+SkkrQ==", | ||||||
|  |       "optional": true | ||||||
|  |     }, | ||||||
|  |     "exiftool-vendored.pl": { | ||||||
|  |       "version": "12.54.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.54.0.tgz", | ||||||
|  |       "integrity": "sha512-RBBowsYcM6EvbWoBkg2dOqHpH3WIzN7bIzHc+o+LquqCTo3doZwECClD/6PNHVSMQsl2Z0fEf75sNq2msooMSg==", | ||||||
|  |       "optional": true | ||||||
|     }, |     }, | ||||||
|     "exit": { |     "exit": { | ||||||
|       "version": "0.1.2", |       "version": "0.1.2", | ||||||
| @ -16577,6 +16656,11 @@ | |||||||
|       "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", |       "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", | ||||||
|       "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" |       "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" | ||||||
|     }, |     }, | ||||||
|  |     "he": { | ||||||
|  |       "version": "1.2.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", | ||||||
|  |       "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" | ||||||
|  |     }, | ||||||
|     "hexoid": { |     "hexoid": { | ||||||
|       "version": "1.0.0", |       "version": "1.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", |       "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", | ||||||
|  | |||||||
| @ -57,7 +57,7 @@ | |||||||
|     "cookie-parser": "^1.4.6", |     "cookie-parser": "^1.4.6", | ||||||
|     "diskusage": "^1.1.3", |     "diskusage": "^1.1.3", | ||||||
|     "dotenv": "^14.2.0", |     "dotenv": "^14.2.0", | ||||||
|     "exifr": "^7.1.3", |     "exiftool-vendored": "^19.0.0", | ||||||
|     "fdir": "^5.3.0", |     "fdir": "^5.3.0", | ||||||
|     "fluent-ffmpeg": "^2.1.2", |     "fluent-ffmpeg": "^2.1.2", | ||||||
|     "geo-tz": "^7.0.2", |     "geo-tz": "^7.0.2", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user