mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	fix(server): use exiftool decoded values and unify metadata extraction (#2908)
* refactor(server): metadata extraction * chore: upgrade exiftool * chore: remove log * fix: other rotation cases --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
		
							parent
							
								
									b6c6a7e403
								
							
						
					
					
						commit
						49ef86173f
					
				
							
								
								
									
										389
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										389
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -28,9 +28,10 @@
 | 
				
			|||||||
        "class-transformer": "^0.5.1",
 | 
					        "class-transformer": "^0.5.1",
 | 
				
			||||||
        "class-validator": "^0.14.0",
 | 
					        "class-validator": "^0.14.0",
 | 
				
			||||||
        "cookie-parser": "^1.4.6",
 | 
					        "cookie-parser": "^1.4.6",
 | 
				
			||||||
        "exiftool-vendored": "^22.0.0",
 | 
					        "exiftool-vendored": "^23.0.0",
 | 
				
			||||||
        "exiftool-vendored.pl": "^12.62.0",
 | 
					        "exiftool-vendored.pl": "^12.62.0",
 | 
				
			||||||
        "fluent-ffmpeg": "^2.1.2",
 | 
					        "fluent-ffmpeg": "^2.1.2",
 | 
				
			||||||
 | 
					        "geo-tz": "^7.0.7",
 | 
				
			||||||
        "handlebars": "^4.7.8",
 | 
					        "handlebars": "^4.7.8",
 | 
				
			||||||
        "i18n-iso-countries": "^7.6.0",
 | 
					        "i18n-iso-countries": "^7.6.0",
 | 
				
			||||||
        "immich": "^0.41.0",
 | 
					        "immich": "^0.41.0",
 | 
				
			||||||
@ -2669,9 +2670,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@photostructure/tz-lookup": {
 | 
					    "node_modules/@photostructure/tz-lookup": {
 | 
				
			||||||
      "version": "7.0.0",
 | 
					      "version": "8.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-7.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-8.0.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-pTRsZz7Sn4yAtItC7I4+0segDHosMyOtJgAXg+xvDOolT0Xz4IFWqBV33OMCWoaNd3oQb60wbWhLeCQgJCyZAA=="
 | 
					      "integrity": "sha512-D5ggPEWSNGEKzKTx6+Gn0NZXHQ4ywgRd2p2h7i/tjEmkv/uJ9SzQd0o7v7FzEAt4bP3dxDoWm43aPfUs9qMOGg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@pkgjs/parseargs": {
 | 
					    "node_modules/@pkgjs/parseargs": {
 | 
				
			||||||
      "version": "0.11.0",
 | 
					      "version": "0.11.0",
 | 
				
			||||||
@ -3101,6 +3102,37 @@
 | 
				
			|||||||
      "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
 | 
					      "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
 | 
				
			||||||
      "devOptional": true
 | 
					      "devOptional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@turf/boolean-point-in-polygon": {
 | 
				
			||||||
 | 
					      "version": "6.5.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@turf/helpers": "^6.5.0",
 | 
				
			||||||
 | 
					        "@turf/invariant": "^6.5.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/turf"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@turf/helpers": {
 | 
				
			||||||
 | 
					      "version": "6.5.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/turf"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@turf/invariant": {
 | 
				
			||||||
 | 
					      "version": "6.5.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@turf/helpers": "^6.5.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/turf"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/archiver": {
 | 
					    "node_modules/@types/archiver": {
 | 
				
			||||||
      "version": "5.3.2",
 | 
					      "version": "5.3.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.2.tgz",
 | 
				
			||||||
@ -3393,9 +3425,9 @@
 | 
				
			|||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/luxon": {
 | 
					    "node_modules/@types/luxon": {
 | 
				
			||||||
      "version": "3.3.0",
 | 
					      "version": "3.3.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg=="
 | 
					      "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/mime": {
 | 
					    "node_modules/@types/mime": {
 | 
				
			||||||
      "version": "1.3.2",
 | 
					      "version": "1.3.2",
 | 
				
			||||||
@ -4229,6 +4261,11 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
 | 
					      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/array-source": {
 | 
				
			||||||
 | 
					      "version": "0.0.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/array-source/-/array-source-0.0.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/array-timsort": {
 | 
					    "node_modules/array-timsort": {
 | 
				
			||||||
      "version": "1.0.3",
 | 
					      "version": "1.0.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz",
 | 
				
			||||||
@ -6676,34 +6713,34 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/exiftool-vendored": {
 | 
					    "node_modules/exiftool-vendored": {
 | 
				
			||||||
      "version": "22.0.0",
 | 
					      "version": "23.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-22.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-23.0.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-gBOQ4C2GLjxKPDPRuUbMOz91mG6IFA22L+Z/IQzFotFu20vc7YroqHALf/ophCbANA5sNSArbVDPijP7n/20Jg==",
 | 
					      "integrity": "sha512-QHAKZ+M6IqWl/b5UHdG6eF+uwqGZY5Jr9JcPeQB4s5H7vPyK8KvUWsnY1NbRo824OIOd4e6kn4tNhhWpYRNnCw==",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@photostructure/tz-lookup": "^7.0.0",
 | 
					        "@photostructure/tz-lookup": "^8.0.0",
 | 
				
			||||||
        "@types/luxon": "^3.3.0",
 | 
					        "@types/luxon": "^3.3.2",
 | 
				
			||||||
        "batch-cluster": "^12.1.0",
 | 
					        "batch-cluster": "^12.1.0",
 | 
				
			||||||
        "he": "^1.2.0",
 | 
					        "he": "^1.2.0",
 | 
				
			||||||
        "luxon": "^3.3.0"
 | 
					        "luxon": "^3.4.3"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "optionalDependencies": {
 | 
					      "optionalDependencies": {
 | 
				
			||||||
        "exiftool-vendored.exe": "12.62.0",
 | 
					        "exiftool-vendored.exe": "12.65.0",
 | 
				
			||||||
        "exiftool-vendored.pl": "12.62.0"
 | 
					        "exiftool-vendored.pl": "12.65.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/exiftool-vendored.exe": {
 | 
					    "node_modules/exiftool-vendored.exe": {
 | 
				
			||||||
      "version": "12.62.0",
 | 
					      "version": "12.65.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.62.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.65.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-xNFkvmjwnMg6ivtmkc67w1qD23fIy06nRpMpGuBpTwTqAVatHV+vk7T75zyvLoXRRpd1rKID9XAVLGJCE/iiMQ==",
 | 
					      "integrity": "sha512-VDTSW3/u5bdLlg516g1oTypq2Sxd3I2pWTzdd5EmDtSjmvvBCLyDlMpv4Gnz8dnlQTRsEqwIgv/TAtdWykwEBg==",
 | 
				
			||||||
      "optional": true,
 | 
					      "optional": true,
 | 
				
			||||||
      "os": [
 | 
					      "os": [
 | 
				
			||||||
        "win32"
 | 
					        "win32"
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/exiftool-vendored.pl": {
 | 
					    "node_modules/exiftool-vendored.pl": {
 | 
				
			||||||
      "version": "12.62.0",
 | 
					      "version": "12.65.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.62.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.65.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-e0JqrihAs6s7B4Zm/rrTg/pzPB7uUVnK6fwEpTik6PE8N84SGDn3ht4snDqwejZ2mKdmEk8upSygeUe33+yVlw==",
 | 
					      "integrity": "sha512-BpR+rwKLWqUAPbsW17fw+8FAmyijkMhjaLu3fWU2QX6rpBJnOfn+lQp4Txkq44avL1LDV+eQ8pbWXyimjkPw0Q==",
 | 
				
			||||||
      "os": [
 | 
					      "os": [
 | 
				
			||||||
        "!win32"
 | 
					        "!win32"
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
@ -7025,6 +7062,14 @@
 | 
				
			|||||||
        "node": "^10.12.0 || >=12.0.0"
 | 
					        "node": "^10.12.0 || >=12.0.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/file-source": {
 | 
				
			||||||
 | 
					      "version": "0.6.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/file-source/-/file-source-0.6.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "stream-source": "0.3"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/file-type": {
 | 
					    "node_modules/file-type": {
 | 
				
			||||||
      "version": "17.1.6",
 | 
					      "version": "17.1.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz",
 | 
				
			||||||
@ -7443,6 +7488,49 @@
 | 
				
			|||||||
        "node": ">=6.9.0"
 | 
					        "node": ">=6.9.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/geo-tz": {
 | 
				
			||||||
 | 
					      "version": "7.0.7",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-7.0.7.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Aq0sRSO1y4w62D5muRqzDmN4SWfFYnt703BLiqiHAvunlwsJs4qd3Fkl1pKSUa0bwuBmPFxIA8M1E+ilg2PSjw==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@turf/boolean-point-in-polygon": "^6.5.0",
 | 
				
			||||||
 | 
					        "@turf/helpers": "^6.5.0",
 | 
				
			||||||
 | 
					        "geobuf": "^3.0.2",
 | 
				
			||||||
 | 
					        "pbf": "^3.2.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=12"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/geobuf": {
 | 
				
			||||||
 | 
					      "version": "3.0.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/geobuf/-/geobuf-3.0.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "concat-stream": "^2.0.0",
 | 
				
			||||||
 | 
					        "pbf": "^3.2.1",
 | 
				
			||||||
 | 
					        "shapefile": "~0.6.6"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "bin": {
 | 
				
			||||||
 | 
					        "geobuf2json": "bin/geobuf2json",
 | 
				
			||||||
 | 
					        "json2geobuf": "bin/json2geobuf",
 | 
				
			||||||
 | 
					        "shp2geobuf": "bin/shp2geobuf"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/geobuf/node_modules/concat-stream": {
 | 
				
			||||||
 | 
					      "version": "2.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
 | 
				
			||||||
 | 
					      "engines": [
 | 
				
			||||||
 | 
					        "node >= 6.0"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "buffer-from": "^1.0.0",
 | 
				
			||||||
 | 
					        "inherits": "^2.0.3",
 | 
				
			||||||
 | 
					        "readable-stream": "^3.0.2",
 | 
				
			||||||
 | 
					        "typedarray": "^0.0.6"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/get-caller-file": {
 | 
					    "node_modules/get-caller-file": {
 | 
				
			||||||
      "version": "2.0.5",
 | 
					      "version": "2.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
 | 
				
			||||||
@ -9390,9 +9478,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/luxon": {
 | 
					    "node_modules/luxon": {
 | 
				
			||||||
      "version": "3.4.2",
 | 
					      "version": "3.4.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-uBoAVCVcajsrqy3pv7eo5jEUz1oeLmCcnMv8n4AJpT5hbpN9lUssAXibNElpbLce3Mhm9dyBzwYLs9zctM/0tA==",
 | 
					      "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==",
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": ">=12"
 | 
					        "node": ">=12"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -10366,6 +10454,15 @@
 | 
				
			|||||||
        "node": "14 || >=16.14"
 | 
					        "node": "14 || >=16.14"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/path-source": {
 | 
				
			||||||
 | 
					      "version": "0.1.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/path-source/-/path-source-0.1.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "array-source": "0.0",
 | 
				
			||||||
 | 
					        "file-source": "0.6"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/path-to-regexp": {
 | 
					    "node_modules/path-to-regexp": {
 | 
				
			||||||
      "version": "3.2.0",
 | 
					      "version": "3.2.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz",
 | 
				
			||||||
@ -10379,6 +10476,18 @@
 | 
				
			|||||||
        "node": ">=8"
 | 
					        "node": ">=8"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/pbf": {
 | 
				
			||||||
 | 
					      "version": "3.2.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "ieee754": "^1.1.12",
 | 
				
			||||||
 | 
					        "resolve-protobuf-schema": "^2.1.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "bin": {
 | 
				
			||||||
 | 
					        "pbf": "bin/pbf"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/peek-readable": {
 | 
					    "node_modules/peek-readable": {
 | 
				
			||||||
      "version": "5.0.0",
 | 
					      "version": "5.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
 | 
				
			||||||
@ -10783,6 +10892,11 @@
 | 
				
			|||||||
        "node": ">=10"
 | 
					        "node": ">=10"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/protocol-buffers-schema": {
 | 
				
			||||||
 | 
					      "version": "3.6.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/proxy-addr": {
 | 
					    "node_modules/proxy-addr": {
 | 
				
			||||||
      "version": "2.0.7",
 | 
					      "version": "2.0.7",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
 | 
				
			||||||
@ -11146,6 +11260,14 @@
 | 
				
			|||||||
        "node": ">=4"
 | 
					        "node": ">=4"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/resolve-protobuf-schema": {
 | 
				
			||||||
 | 
					      "version": "2.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "protocol-buffers-schema": "^3.3.1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/resolve.exports": {
 | 
					    "node_modules/resolve.exports": {
 | 
				
			||||||
      "version": "2.0.2",
 | 
					      "version": "2.0.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz",
 | 
				
			||||||
@ -11546,6 +11668,28 @@
 | 
				
			|||||||
        "sha.js": "bin.js"
 | 
					        "sha.js": "bin.js"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/shapefile": {
 | 
				
			||||||
 | 
					      "version": "0.6.6",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/shapefile/-/shapefile-0.6.6.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "array-source": "0.0",
 | 
				
			||||||
 | 
					        "commander": "2",
 | 
				
			||||||
 | 
					        "path-source": "0.1",
 | 
				
			||||||
 | 
					        "slice-source": "0.4",
 | 
				
			||||||
 | 
					        "stream-source": "0.3",
 | 
				
			||||||
 | 
					        "text-encoding": "^0.6.4"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "bin": {
 | 
				
			||||||
 | 
					        "dbf2json": "bin/dbf2json",
 | 
				
			||||||
 | 
					        "shp2json": "bin/shp2json"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/shapefile/node_modules/commander": {
 | 
				
			||||||
 | 
					      "version": "2.20.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/sharp": {
 | 
					    "node_modules/sharp": {
 | 
				
			||||||
      "version": "0.31.3",
 | 
					      "version": "0.31.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.3.tgz",
 | 
				
			||||||
@ -11695,6 +11839,11 @@
 | 
				
			|||||||
        "node": ">=8"
 | 
					        "node": ">=8"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/slice-source": {
 | 
				
			||||||
 | 
					      "version": "0.4.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/socket.io": {
 | 
					    "node_modules/socket.io": {
 | 
				
			||||||
      "version": "4.7.2",
 | 
					      "version": "4.7.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz",
 | 
				
			||||||
@ -11886,6 +12035,11 @@
 | 
				
			|||||||
        "node": ">= 0.8"
 | 
					        "node": ">= 0.8"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/stream-source": {
 | 
				
			||||||
 | 
					      "version": "0.3.5",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/stream-source/-/stream-source-0.3.5.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/streamsearch": {
 | 
					    "node_modules/streamsearch": {
 | 
				
			||||||
      "version": "1.1.0",
 | 
					      "version": "1.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
 | 
				
			||||||
@ -12408,6 +12562,12 @@
 | 
				
			|||||||
        "node": ">=8.17.0"
 | 
					        "node": ">=8.17.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/text-encoding": {
 | 
				
			||||||
 | 
					      "version": "0.6.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==",
 | 
				
			||||||
 | 
					      "deprecated": "no longer maintained"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/text-table": {
 | 
					    "node_modules/text-table": {
 | 
				
			||||||
      "version": "0.2.0",
 | 
					      "version": "0.2.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
 | 
				
			||||||
@ -15439,9 +15599,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@photostructure/tz-lookup": {
 | 
					    "@photostructure/tz-lookup": {
 | 
				
			||||||
      "version": "7.0.0",
 | 
					      "version": "8.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-7.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-8.0.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-pTRsZz7Sn4yAtItC7I4+0segDHosMyOtJgAXg+xvDOolT0Xz4IFWqBV33OMCWoaNd3oQb60wbWhLeCQgJCyZAA=="
 | 
					      "integrity": "sha512-D5ggPEWSNGEKzKTx6+Gn0NZXHQ4ywgRd2p2h7i/tjEmkv/uJ9SzQd0o7v7FzEAt4bP3dxDoWm43aPfUs9qMOGg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@pkgjs/parseargs": {
 | 
					    "@pkgjs/parseargs": {
 | 
				
			||||||
      "version": "0.11.0",
 | 
					      "version": "0.11.0",
 | 
				
			||||||
@ -15721,6 +15881,28 @@
 | 
				
			|||||||
      "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
 | 
					      "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
 | 
				
			||||||
      "devOptional": true
 | 
					      "devOptional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "@turf/boolean-point-in-polygon": {
 | 
				
			||||||
 | 
					      "version": "6.5.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "@turf/helpers": "^6.5.0",
 | 
				
			||||||
 | 
					        "@turf/invariant": "^6.5.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "@turf/helpers": {
 | 
				
			||||||
 | 
					      "version": "6.5.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "@turf/invariant": {
 | 
				
			||||||
 | 
					      "version": "6.5.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "@turf/helpers": "^6.5.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "@types/archiver": {
 | 
					    "@types/archiver": {
 | 
				
			||||||
      "version": "5.3.2",
 | 
					      "version": "5.3.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.2.tgz",
 | 
				
			||||||
@ -16013,9 +16195,9 @@
 | 
				
			|||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@types/luxon": {
 | 
					    "@types/luxon": {
 | 
				
			||||||
      "version": "3.3.0",
 | 
					      "version": "3.3.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg=="
 | 
					      "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@types/mime": {
 | 
					    "@types/mime": {
 | 
				
			||||||
      "version": "1.3.2",
 | 
					      "version": "1.3.2",
 | 
				
			||||||
@ -16682,6 +16864,11 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
 | 
					      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "array-source": {
 | 
				
			||||||
 | 
					      "version": "0.0.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/array-source/-/array-source-0.0.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "array-timsort": {
 | 
					    "array-timsort": {
 | 
				
			||||||
      "version": "1.0.3",
 | 
					      "version": "1.0.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz",
 | 
				
			||||||
@ -18490,29 +18677,29 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "exiftool-vendored": {
 | 
					    "exiftool-vendored": {
 | 
				
			||||||
      "version": "22.0.0",
 | 
					      "version": "23.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-22.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-23.0.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-gBOQ4C2GLjxKPDPRuUbMOz91mG6IFA22L+Z/IQzFotFu20vc7YroqHALf/ophCbANA5sNSArbVDPijP7n/20Jg==",
 | 
					      "integrity": "sha512-QHAKZ+M6IqWl/b5UHdG6eF+uwqGZY5Jr9JcPeQB4s5H7vPyK8KvUWsnY1NbRo824OIOd4e6kn4tNhhWpYRNnCw==",
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "@photostructure/tz-lookup": "^7.0.0",
 | 
					        "@photostructure/tz-lookup": "^8.0.0",
 | 
				
			||||||
        "@types/luxon": "^3.3.0",
 | 
					        "@types/luxon": "^3.3.2",
 | 
				
			||||||
        "batch-cluster": "^12.1.0",
 | 
					        "batch-cluster": "^12.1.0",
 | 
				
			||||||
        "exiftool-vendored.exe": "12.62.0",
 | 
					        "exiftool-vendored.exe": "12.65.0",
 | 
				
			||||||
        "exiftool-vendored.pl": "12.62.0",
 | 
					        "exiftool-vendored.pl": "12.65.0",
 | 
				
			||||||
        "he": "^1.2.0",
 | 
					        "he": "^1.2.0",
 | 
				
			||||||
        "luxon": "^3.3.0"
 | 
					        "luxon": "^3.4.3"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "exiftool-vendored.exe": {
 | 
					    "exiftool-vendored.exe": {
 | 
				
			||||||
      "version": "12.62.0",
 | 
					      "version": "12.65.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.62.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.65.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-xNFkvmjwnMg6ivtmkc67w1qD23fIy06nRpMpGuBpTwTqAVatHV+vk7T75zyvLoXRRpd1rKID9XAVLGJCE/iiMQ==",
 | 
					      "integrity": "sha512-VDTSW3/u5bdLlg516g1oTypq2Sxd3I2pWTzdd5EmDtSjmvvBCLyDlMpv4Gnz8dnlQTRsEqwIgv/TAtdWykwEBg==",
 | 
				
			||||||
      "optional": true
 | 
					      "optional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "exiftool-vendored.pl": {
 | 
					    "exiftool-vendored.pl": {
 | 
				
			||||||
      "version": "12.62.0",
 | 
					      "version": "12.65.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.62.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.65.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-e0JqrihAs6s7B4Zm/rrTg/pzPB7uUVnK6fwEpTik6PE8N84SGDn3ht4snDqwejZ2mKdmEk8upSygeUe33+yVlw=="
 | 
					      "integrity": "sha512-BpR+rwKLWqUAPbsW17fw+8FAmyijkMhjaLu3fWU2QX6rpBJnOfn+lQp4Txkq44avL1LDV+eQ8pbWXyimjkPw0Q=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "exit": {
 | 
					    "exit": {
 | 
				
			||||||
      "version": "0.1.2",
 | 
					      "version": "0.1.2",
 | 
				
			||||||
@ -18769,6 +18956,14 @@
 | 
				
			|||||||
        "flat-cache": "^3.0.4"
 | 
					        "flat-cache": "^3.0.4"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "file-source": {
 | 
				
			||||||
 | 
					      "version": "0.6.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/file-source/-/file-source-0.6.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "stream-source": "0.3"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "file-type": {
 | 
					    "file-type": {
 | 
				
			||||||
      "version": "17.1.6",
 | 
					      "version": "17.1.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz",
 | 
				
			||||||
@ -19075,6 +19270,40 @@
 | 
				
			|||||||
      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
 | 
					      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "geo-tz": {
 | 
				
			||||||
 | 
					      "version": "7.0.7",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-7.0.7.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Aq0sRSO1y4w62D5muRqzDmN4SWfFYnt703BLiqiHAvunlwsJs4qd3Fkl1pKSUa0bwuBmPFxIA8M1E+ilg2PSjw==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "@turf/boolean-point-in-polygon": "^6.5.0",
 | 
				
			||||||
 | 
					        "@turf/helpers": "^6.5.0",
 | 
				
			||||||
 | 
					        "geobuf": "^3.0.2",
 | 
				
			||||||
 | 
					        "pbf": "^3.2.1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "geobuf": {
 | 
				
			||||||
 | 
					      "version": "3.0.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/geobuf/-/geobuf-3.0.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "concat-stream": "^2.0.0",
 | 
				
			||||||
 | 
					        "pbf": "^3.2.1",
 | 
				
			||||||
 | 
					        "shapefile": "~0.6.6"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "concat-stream": {
 | 
				
			||||||
 | 
					          "version": "2.0.0",
 | 
				
			||||||
 | 
					          "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
 | 
				
			||||||
 | 
					          "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
 | 
				
			||||||
 | 
					          "requires": {
 | 
				
			||||||
 | 
					            "buffer-from": "^1.0.0",
 | 
				
			||||||
 | 
					            "inherits": "^2.0.3",
 | 
				
			||||||
 | 
					            "readable-stream": "^3.0.2",
 | 
				
			||||||
 | 
					            "typedarray": "^0.0.6"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "get-caller-file": {
 | 
					    "get-caller-file": {
 | 
				
			||||||
      "version": "2.0.5",
 | 
					      "version": "2.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
 | 
				
			||||||
@ -20535,9 +20764,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "luxon": {
 | 
					    "luxon": {
 | 
				
			||||||
      "version": "3.4.2",
 | 
					      "version": "3.4.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-uBoAVCVcajsrqy3pv7eo5jEUz1oeLmCcnMv8n4AJpT5hbpN9lUssAXibNElpbLce3Mhm9dyBzwYLs9zctM/0tA=="
 | 
					      "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "macos-release": {
 | 
					    "macos-release": {
 | 
				
			||||||
      "version": "2.5.1",
 | 
					      "version": "2.5.1",
 | 
				
			||||||
@ -21254,6 +21483,15 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "path-source": {
 | 
				
			||||||
 | 
					      "version": "0.1.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/path-source/-/path-source-0.1.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "array-source": "0.0",
 | 
				
			||||||
 | 
					        "file-source": "0.6"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "path-to-regexp": {
 | 
					    "path-to-regexp": {
 | 
				
			||||||
      "version": "3.2.0",
 | 
					      "version": "3.2.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz",
 | 
				
			||||||
@ -21264,6 +21502,15 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
 | 
					      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "pbf": {
 | 
				
			||||||
 | 
					      "version": "3.2.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "ieee754": "^1.1.12",
 | 
				
			||||||
 | 
					        "resolve-protobuf-schema": "^2.1.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "peek-readable": {
 | 
					    "peek-readable": {
 | 
				
			||||||
      "version": "5.0.0",
 | 
					      "version": "5.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
 | 
				
			||||||
@ -21546,6 +21793,11 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "protocol-buffers-schema": {
 | 
				
			||||||
 | 
					      "version": "3.6.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "proxy-addr": {
 | 
					    "proxy-addr": {
 | 
				
			||||||
      "version": "2.0.7",
 | 
					      "version": "2.0.7",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
 | 
				
			||||||
@ -21812,6 +22064,14 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
 | 
					      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "resolve-protobuf-schema": {
 | 
				
			||||||
 | 
					      "version": "2.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "protocol-buffers-schema": "^3.3.1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "resolve.exports": {
 | 
					    "resolve.exports": {
 | 
				
			||||||
      "version": "2.0.2",
 | 
					      "version": "2.0.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz",
 | 
				
			||||||
@ -22102,6 +22362,26 @@
 | 
				
			|||||||
        "safe-buffer": "^5.0.1"
 | 
					        "safe-buffer": "^5.0.1"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "shapefile": {
 | 
				
			||||||
 | 
					      "version": "0.6.6",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/shapefile/-/shapefile-0.6.6.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "array-source": "0.0",
 | 
				
			||||||
 | 
					        "commander": "2",
 | 
				
			||||||
 | 
					        "path-source": "0.1",
 | 
				
			||||||
 | 
					        "slice-source": "0.4",
 | 
				
			||||||
 | 
					        "stream-source": "0.3",
 | 
				
			||||||
 | 
					        "text-encoding": "^0.6.4"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "commander": {
 | 
				
			||||||
 | 
					          "version": "2.20.3",
 | 
				
			||||||
 | 
					          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
 | 
				
			||||||
 | 
					          "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "sharp": {
 | 
					    "sharp": {
 | 
				
			||||||
      "version": "0.31.3",
 | 
					      "version": "0.31.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.3.tgz",
 | 
				
			||||||
@ -22200,6 +22480,11 @@
 | 
				
			|||||||
      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
 | 
					      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "slice-source": {
 | 
				
			||||||
 | 
					      "version": "0.4.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "socket.io": {
 | 
					    "socket.io": {
 | 
				
			||||||
      "version": "4.7.2",
 | 
					      "version": "4.7.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz",
 | 
				
			||||||
@ -22361,6 +22646,11 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
 | 
					      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "stream-source": {
 | 
				
			||||||
 | 
					      "version": "0.3.5",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/stream-source/-/stream-source-0.3.5.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "streamsearch": {
 | 
					    "streamsearch": {
 | 
				
			||||||
      "version": "1.1.0",
 | 
					      "version": "1.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
 | 
				
			||||||
@ -22740,6 +23030,11 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "text-encoding": {
 | 
				
			||||||
 | 
					      "version": "0.6.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "text-table": {
 | 
					    "text-table": {
 | 
				
			||||||
      "version": "0.2.0",
 | 
					      "version": "0.2.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
 | 
				
			||||||
 | 
				
			|||||||
@ -58,9 +58,10 @@
 | 
				
			|||||||
    "class-transformer": "^0.5.1",
 | 
					    "class-transformer": "^0.5.1",
 | 
				
			||||||
    "class-validator": "^0.14.0",
 | 
					    "class-validator": "^0.14.0",
 | 
				
			||||||
    "cookie-parser": "^1.4.6",
 | 
					    "cookie-parser": "^1.4.6",
 | 
				
			||||||
    "exiftool-vendored": "^22.0.0",
 | 
					    "exiftool-vendored": "^23.0.0",
 | 
				
			||||||
    "exiftool-vendored.pl": "^12.62.0",
 | 
					    "exiftool-vendored.pl": "^12.62.0",
 | 
				
			||||||
    "fluent-ffmpeg": "^2.1.2",
 | 
					    "fluent-ffmpeg": "^2.1.2",
 | 
				
			||||||
 | 
					    "geo-tz": "^7.0.7",
 | 
				
			||||||
    "handlebars": "^4.7.8",
 | 
					    "handlebars": "^4.7.8",
 | 
				
			||||||
    "i18n-iso-countries": "^7.6.0",
 | 
					    "i18n-iso-countries": "^7.6.0",
 | 
				
			||||||
    "immich": "^0.41.0",
 | 
					    "immich": "^0.41.0",
 | 
				
			||||||
 | 
				
			|||||||
@ -18,27 +18,12 @@ import {
 | 
				
			|||||||
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
 | 
					import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
 | 
				
			||||||
import { Inject, Logger } from '@nestjs/common';
 | 
					import { Inject, Logger } from '@nestjs/common';
 | 
				
			||||||
import { ConfigService } from '@nestjs/config';
 | 
					import { ConfigService } from '@nestjs/config';
 | 
				
			||||||
import tz_lookup from '@photostructure/tz-lookup';
 | 
					import { DefaultReadTaskOptions, ExifDateTime, exiftool, ReadTaskOptions, Tags } from 'exiftool-vendored';
 | 
				
			||||||
import { exiftool, Tags } from 'exiftool-vendored';
 | 
					import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
 | 
				
			||||||
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
 | 
					import * as geotz from 'geo-tz';
 | 
				
			||||||
import { Duration } from 'luxon';
 | 
					import { Duration } from 'luxon';
 | 
				
			||||||
import fs from 'node:fs/promises';
 | 
					import fs from 'node:fs/promises';
 | 
				
			||||||
import path from 'node:path';
 | 
					import path from 'node:path';
 | 
				
			||||||
import sharp from 'sharp';
 | 
					 | 
				
			||||||
import { promisify } from 'util';
 | 
					 | 
				
			||||||
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);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface MotionPhotosData {
 | 
					 | 
				
			||||||
  isMotionPhoto: string | number | null;
 | 
					 | 
				
			||||||
  isMicroVideo: string | number | null;
 | 
					 | 
				
			||||||
  videoOffset: string | number | null;
 | 
					 | 
				
			||||||
  directory: DirectoryEntry[] | null;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface DirectoryItem {
 | 
					interface DirectoryItem {
 | 
				
			||||||
  Length?: number;
 | 
					  Length?: number;
 | 
				
			||||||
@ -56,8 +41,12 @@ interface ImmichTags extends Tags {
 | 
				
			|||||||
  MotionPhoto?: number;
 | 
					  MotionPhoto?: number;
 | 
				
			||||||
  MotionPhotoVersion?: number;
 | 
					  MotionPhotoVersion?: number;
 | 
				
			||||||
  MotionPhotoPresentationTimestampUs?: number;
 | 
					  MotionPhotoPresentationTimestampUs?: number;
 | 
				
			||||||
 | 
					  MediaGroupUUID?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const exifDate = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.toDate() : null);
 | 
				
			||||||
 | 
					const validate = <T>(value: T): T | null => (typeof value === 'string' ? null : value ?? null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class MetadataExtractionProcessor {
 | 
					export class MetadataExtractionProcessor {
 | 
				
			||||||
  private logger = new Logger(MetadataExtractionProcessor.name);
 | 
					  private logger = new Logger(MetadataExtractionProcessor.name);
 | 
				
			||||||
  private reverseGeocodingEnabled: boolean;
 | 
					  private reverseGeocodingEnabled: boolean;
 | 
				
			||||||
@ -153,249 +142,48 @@ export class MetadataExtractionProcessor {
 | 
				
			|||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (asset.type === AssetType.VIDEO) {
 | 
					    const [exifData, tags] = await this.exifData(asset);
 | 
				
			||||||
      return this.handleVideoMetadataExtraction(asset);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return this.handlePhotoMetadataExtraction(asset);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async handlePhotoMetadataExtraction(asset: AssetEntity) {
 | 
					    await this.applyMotionPhotos(asset, tags);
 | 
				
			||||||
    const mediaExifData = await exiftool.read<ImmichTags>(asset.originalPath).catch((error: any) => {
 | 
					    await this.applyReverseGeocoding(asset, exifData);
 | 
				
			||||||
      this.logger.warn(
 | 
					 | 
				
			||||||
        `The exifData parsing failed due to ${error} for asset ${asset.id} at ${asset.originalPath}`,
 | 
					 | 
				
			||||||
        error?.stack,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const sidecarExifData = asset.sidecarPath
 | 
					    await this.assetRepository.upsertExif(exifData);
 | 
				
			||||||
      ? await exiftool.read<ImmichTags>(asset.sidecarPath).catch((error: any) => {
 | 
					 | 
				
			||||||
          this.logger.warn(
 | 
					 | 
				
			||||||
            `The exifData parsing failed due to ${error} for asset ${asset.id} at ${asset.originalPath}`,
 | 
					 | 
				
			||||||
            error?.stack,
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          return 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;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const timeZone = exifTimeZone(getExifProperty('DateTimeOriginal', 'CreateDate') ?? asset.fileCreatedAt);
 | 
					 | 
				
			||||||
    const fileCreatedAt = exifToDate(getExifProperty('DateTimeOriginal', 'CreateDate') ?? asset.fileCreatedAt);
 | 
					 | 
				
			||||||
    const fileModifiedAt = exifToDate(getExifProperty('ModifyDate') ?? asset.fileModifiedAt);
 | 
					 | 
				
			||||||
    const fileStats = await fs.stat(asset.originalPath);
 | 
					 | 
				
			||||||
    const fileSizeInBytes = fileStats.size;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const newExif = new ExifEntity();
 | 
					 | 
				
			||||||
    newExif.assetId = asset.id;
 | 
					 | 
				
			||||||
    newExif.fileSizeInByte = fileSizeInBytes;
 | 
					 | 
				
			||||||
    newExif.make = getExifProperty('Make');
 | 
					 | 
				
			||||||
    newExif.model = getExifProperty('Model');
 | 
					 | 
				
			||||||
    newExif.exifImageHeight = toNumberOrNull(getExifProperty('ExifImageHeight', 'ImageHeight'));
 | 
					 | 
				
			||||||
    newExif.exifImageWidth = toNumberOrNull(getExifProperty('ExifImageWidth', 'ImageWidth'));
 | 
					 | 
				
			||||||
    newExif.exposureTime = getExifProperty('ExposureTime');
 | 
					 | 
				
			||||||
    newExif.orientation = getExifProperty('Orientation')?.toString() ?? null;
 | 
					 | 
				
			||||||
    newExif.dateTimeOriginal = fileCreatedAt;
 | 
					 | 
				
			||||||
    newExif.modifyDate = fileModifiedAt;
 | 
					 | 
				
			||||||
    newExif.timeZone = timeZone;
 | 
					 | 
				
			||||||
    newExif.lensModel = getExifProperty('LensModel');
 | 
					 | 
				
			||||||
    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');
 | 
					 | 
				
			||||||
    const lat = parseLatitude(latitude);
 | 
					 | 
				
			||||||
    const lon = parseLongitude(longitude);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (lat === 0 && lon === 0) {
 | 
					 | 
				
			||||||
      this.logger.warn(`Latitude & Longitude were on Null Island (${lat},${lon}), not assigning coordinates`);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      newExif.latitude = lat;
 | 
					 | 
				
			||||||
      newExif.longitude = lon;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const projectionType = getExifProperty('ProjectionType');
 | 
					 | 
				
			||||||
    if (projectionType) {
 | 
					 | 
				
			||||||
      newExif.projectionType = String(projectionType).toUpperCase();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    newExif.livePhotoCID = getExifProperty('MediaGroupUUID');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const rawDirectory = getExifProperty('Directory');
 | 
					 | 
				
			||||||
    await this.applyMotionPhotos(asset, {
 | 
					 | 
				
			||||||
      isMotionPhoto: getExifProperty('MotionPhoto'),
 | 
					 | 
				
			||||||
      isMicroVideo: getExifProperty('MicroVideo'),
 | 
					 | 
				
			||||||
      videoOffset: getExifProperty('MicroVideoOffset'),
 | 
					 | 
				
			||||||
      directory: Array.isArray(rawDirectory) ? (rawDirectory as DirectoryEntry[]) : null,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await this.applyReverseGeocoding(asset, newExif);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * IF the EXIF doesn't contain the width and height of the image,
 | 
					 | 
				
			||||||
     * We will use Sharpjs to get the information.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    if (!newExif.exifImageHeight || !newExif.exifImageWidth || !newExif.orientation) {
 | 
					 | 
				
			||||||
      const metadata = await sharp(asset.originalPath).metadata();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (newExif.exifImageHeight === null) {
 | 
					 | 
				
			||||||
        newExif.exifImageHeight = metadata.height || null;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (newExif.exifImageWidth === null) {
 | 
					 | 
				
			||||||
        newExif.exifImageWidth = metadata.width || null;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (newExif.orientation === null) {
 | 
					 | 
				
			||||||
        newExif.orientation = metadata.orientation !== undefined ? `${metadata.orientation}` : null;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await this.assetRepository.upsertExif(newExif);
 | 
					 | 
				
			||||||
    await this.assetRepository.save({
 | 
					    await this.assetRepository.save({
 | 
				
			||||||
      id: asset.id,
 | 
					      id: asset.id,
 | 
				
			||||||
      fileCreatedAt: fileCreatedAt || undefined,
 | 
					      duration: tags.Duration ? Duration.fromObject({ seconds: tags.Duration }).toFormat('hh:mm:ss.SSS') : null,
 | 
				
			||||||
      updatedAt: new Date(),
 | 
					      fileCreatedAt: exifData.dateTimeOriginal ?? undefined,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async handleVideoMetadataExtraction(asset: AssetEntity) {
 | 
					  private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntity) {
 | 
				
			||||||
    const data = await ffprobe(asset.originalPath);
 | 
					    const { latitude, longitude } = exifData;
 | 
				
			||||||
    const durationString = this.extractDuration(data.format.duration || asset.duration);
 | 
					    if (!this.reverseGeocodingEnabled || !longitude || !latitude) {
 | 
				
			||||||
    let fileCreatedAt = asset.fileCreatedAt;
 | 
					      return;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const videoTags = data.format.tags;
 | 
					 | 
				
			||||||
    if (videoTags) {
 | 
					 | 
				
			||||||
      if (videoTags['com.apple.quicktime.creationdate']) {
 | 
					 | 
				
			||||||
        fileCreatedAt = new Date(videoTags['com.apple.quicktime.creationdate']);
 | 
					 | 
				
			||||||
      } else if (videoTags['creation_time']) {
 | 
					 | 
				
			||||||
        fileCreatedAt = new Date(videoTags['creation_time']);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const exifData = await exiftool.read<ImmichTags>(asset.sidecarPath || asset.originalPath).catch((error: any) => {
 | 
					 | 
				
			||||||
      this.logger.warn(
 | 
					 | 
				
			||||||
        `The exifData parsing failed due to ${error} for asset ${asset.id} at ${asset.originalPath}`,
 | 
					 | 
				
			||||||
        error?.stack,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const newExif = new ExifEntity();
 | 
					 | 
				
			||||||
    newExif.assetId = asset.id;
 | 
					 | 
				
			||||||
    newExif.fileSizeInByte = data.format.size || null;
 | 
					 | 
				
			||||||
    newExif.dateTimeOriginal = fileCreatedAt ? new Date(fileCreatedAt) : null;
 | 
					 | 
				
			||||||
    newExif.modifyDate = null;
 | 
					 | 
				
			||||||
    newExif.timeZone = null;
 | 
					 | 
				
			||||||
    newExif.latitude = null;
 | 
					 | 
				
			||||||
    newExif.longitude = null;
 | 
					 | 
				
			||||||
    newExif.city = null;
 | 
					 | 
				
			||||||
    newExif.state = null;
 | 
					 | 
				
			||||||
    newExif.country = null;
 | 
					 | 
				
			||||||
    newExif.fps = null;
 | 
					 | 
				
			||||||
    newExif.livePhotoCID = exifData?.ContentIdentifier || null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (videoTags && videoTags['location']) {
 | 
					 | 
				
			||||||
      const location = videoTags['location'] as string;
 | 
					 | 
				
			||||||
      const locationRegex = /([+-][0-9]+\.[0-9]+)([+-][0-9]+\.[0-9]+)\/$/;
 | 
					 | 
				
			||||||
      const match = location.match(locationRegex);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (match?.length === 3) {
 | 
					 | 
				
			||||||
        newExif.latitude = parseLatitude(match[1]);
 | 
					 | 
				
			||||||
        newExif.longitude = parseLongitude(match[2]);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else if (videoTags && videoTags['com.apple.quicktime.location.ISO6709']) {
 | 
					 | 
				
			||||||
      const location = videoTags['com.apple.quicktime.location.ISO6709'] as string;
 | 
					 | 
				
			||||||
      const locationRegex = /([+-][0-9]+\.[0-9]+)([+-][0-9]+\.[0-9]+)([+-][0-9]+\.[0-9]+)\/$/;
 | 
					 | 
				
			||||||
      const match = location.match(locationRegex);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (match?.length === 4) {
 | 
					 | 
				
			||||||
        newExif.latitude = parseLatitude(match[1]);
 | 
					 | 
				
			||||||
        newExif.longitude = parseLongitude(match[2]);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (newExif.longitude && newExif.latitude) {
 | 
					 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        newExif.timeZone = tz_lookup(newExif.latitude, newExif.longitude);
 | 
					      const { city, state, country } = await this.geocodingRepository.reverseGeocode({ latitude, longitude });
 | 
				
			||||||
      } catch (error: any) {
 | 
					      Object.assign(exifData, { city, state, country });
 | 
				
			||||||
        this.logger.warn(`Error while calculating timezone from gps coordinates: ${error}`, error?.stack);
 | 
					    } catch (error: Error | any) {
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await this.applyReverseGeocoding(asset, newExif);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const stream of data.streams) {
 | 
					 | 
				
			||||||
      if (stream.codec_type === 'video') {
 | 
					 | 
				
			||||||
        newExif.exifImageWidth = stream.width || null;
 | 
					 | 
				
			||||||
        newExif.exifImageHeight = stream.height || null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (typeof stream.rotation === 'string') {
 | 
					 | 
				
			||||||
          newExif.orientation = stream.rotation;
 | 
					 | 
				
			||||||
        } else if (typeof stream.rotation === 'number') {
 | 
					 | 
				
			||||||
          newExif.orientation = `${stream.rotation}`;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          newExif.orientation = null;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (stream.r_frame_rate) {
 | 
					 | 
				
			||||||
          const fpsParts = stream.r_frame_rate.split('/');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (fpsParts.length === 2) {
 | 
					 | 
				
			||||||
            newExif.fps = Math.round(parseInt(fpsParts[0]) / parseInt(fpsParts[1]));
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await this.assetRepository.upsertExif(newExif);
 | 
					 | 
				
			||||||
    await this.assetRepository.save({ id: asset.id, duration: durationString, fileCreatedAt });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private async applyReverseGeocoding(asset: AssetEntity, newExif: ExifEntity) {
 | 
					 | 
				
			||||||
    const { latitude, longitude } = newExif;
 | 
					 | 
				
			||||||
    if (this.reverseGeocodingEnabled && longitude && latitude) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const { country, state, city } = await this.geocodingRepository.reverseGeocode({ latitude, longitude });
 | 
					 | 
				
			||||||
        newExif.country = country;
 | 
					 | 
				
			||||||
        newExif.state = state;
 | 
					 | 
				
			||||||
        newExif.city = city;
 | 
					 | 
				
			||||||
      } catch (error: any) {
 | 
					 | 
				
			||||||
      this.logger.warn(
 | 
					      this.logger.warn(
 | 
				
			||||||
        `Unable to run reverse geocoding due to ${error} for asset ${asset.id} at ${asset.originalPath}`,
 | 
					        `Unable to run reverse geocoding due to ${error} for asset ${asset.id} at ${asset.originalPath}`,
 | 
				
			||||||
        error?.stack,
 | 
					        error?.stack,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async applyMotionPhotos(asset: AssetEntity, data: MotionPhotosData) {
 | 
					  private async applyMotionPhotos(asset: AssetEntity, tags: ImmichTags) {
 | 
				
			||||||
    if (asset.livePhotoVideoId) {
 | 
					    if (asset.type !== AssetType.IMAGE || asset.livePhotoVideoId) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { isMotionPhoto, isMicroVideo, directory, videoOffset } = data;
 | 
					    const rawDirectory = tags.Directory;
 | 
				
			||||||
 | 
					    const isMotionPhoto = tags.MotionPhoto;
 | 
				
			||||||
 | 
					    const isMicroVideo = tags.MicroVideo;
 | 
				
			||||||
 | 
					    const videoOffset = tags.MicroVideoOffset;
 | 
				
			||||||
 | 
					    const directory = Array.isArray(rawDirectory) ? (rawDirectory as DirectoryEntry[]) : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let length = 0;
 | 
					    let length = 0;
 | 
				
			||||||
    let padding = 0;
 | 
					    let padding = 0;
 | 
				
			||||||
@ -464,12 +252,63 @@ export class MetadataExtractionProcessor {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private extractDuration(duration: number | string | null) {
 | 
					  private async exifData(asset: AssetEntity): Promise<[ExifEntity, ImmichTags]> {
 | 
				
			||||||
    const videoDurationInSecond = Number(duration);
 | 
					    const readTaskOptions: ReadTaskOptions = {
 | 
				
			||||||
    if (!videoDurationInSecond) {
 | 
					      ...DefaultReadTaskOptions,
 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Duration.fromObject({ seconds: videoDurationInSecond }).toFormat('hh:mm:ss.SSS');
 | 
					      defaultVideosToUTC: true,
 | 
				
			||||||
 | 
					      backfillTimezones: true,
 | 
				
			||||||
 | 
					      inferTimezoneFromDatestamps: true,
 | 
				
			||||||
 | 
					      useMWG: true,
 | 
				
			||||||
 | 
					      numericTags: DefaultReadTaskOptions.numericTags.concat(['FocalLength']),
 | 
				
			||||||
 | 
					      geoTz: (lat: number, lon: number): string => geotz.find(lat, lon)[0],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const mediaTags = await exiftool
 | 
				
			||||||
 | 
					      .read<ImmichTags>(asset.originalPath, undefined, readTaskOptions)
 | 
				
			||||||
 | 
					      .catch((error: any) => {
 | 
				
			||||||
 | 
					        this.logger.warn(`error reading exif data (${asset.id} at ${asset.originalPath}): ${error}`, error?.stack);
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const sidecarTags = asset.sidecarPath
 | 
				
			||||||
 | 
					      ? await exiftool.read<ImmichTags>(asset.sidecarPath, undefined, readTaskOptions).catch((error: any) => {
 | 
				
			||||||
 | 
					          this.logger.warn(`error reading exif data (${asset.id} at ${asset.sidecarPath}): ${error}`, error?.stack);
 | 
				
			||||||
 | 
					          return null;
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const stats = await fs.stat(asset.originalPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const tags = { ...mediaTags, ...sidecarTags };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.logger.verbose('Exif Tags', tags);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					      <ExifEntity>{
 | 
				
			||||||
 | 
					        // altitude: tags.GPSAltitude ?? null,
 | 
				
			||||||
 | 
					        assetId: asset.id,
 | 
				
			||||||
 | 
					        dateTimeOriginal: exifDate(firstDateTime(tags)) ?? asset.fileCreatedAt,
 | 
				
			||||||
 | 
					        exifImageHeight: validate(tags.ImageHeight),
 | 
				
			||||||
 | 
					        exifImageWidth: validate(tags.ImageWidth),
 | 
				
			||||||
 | 
					        exposureTime: tags.ExposureTime ?? null,
 | 
				
			||||||
 | 
					        fileSizeInByte: stats.size,
 | 
				
			||||||
 | 
					        fNumber: validate(tags.FNumber),
 | 
				
			||||||
 | 
					        focalLength: validate(tags.FocalLength),
 | 
				
			||||||
 | 
					        fps: validate(tags.VideoFrameRate),
 | 
				
			||||||
 | 
					        iso: validate(tags.ISO),
 | 
				
			||||||
 | 
					        latitude: validate(tags.GPSLatitude),
 | 
				
			||||||
 | 
					        lensModel: tags.LensModel ?? null,
 | 
				
			||||||
 | 
					        livePhotoCID: (asset.type === AssetType.VIDEO ? tags.ContentIdentifier : tags.MediaGroupUUID) ?? null,
 | 
				
			||||||
 | 
					        longitude: validate(tags.GPSLongitude),
 | 
				
			||||||
 | 
					        make: tags.Make ?? null,
 | 
				
			||||||
 | 
					        model: tags.Model ?? null,
 | 
				
			||||||
 | 
					        modifyDate: exifDate(tags.ModifyDate) ?? asset.fileModifiedAt,
 | 
				
			||||||
 | 
					        orientation: validate(tags.Orientation)?.toString() ?? null,
 | 
				
			||||||
 | 
					        projectionType: tags.ProjectionType ? String(tags.ProjectionType).toUpperCase() : null,
 | 
				
			||||||
 | 
					        timeZone: tags.tz,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      tags,
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,36 +0,0 @@
 | 
				
			|||||||
import { describe, expect, it } from '@jest/globals';
 | 
					 | 
				
			||||||
import { ExifDateTime } from 'exiftool-vendored';
 | 
					 | 
				
			||||||
import { exifTimeZone, exifToDate } from './date-time';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
describe('converts exif date to JS date', () => {
 | 
					 | 
				
			||||||
  it('returns null for invalid inputs', () => {
 | 
					 | 
				
			||||||
    expect(exifToDate(undefined)).toBeNull();
 | 
					 | 
				
			||||||
    expect(exifToDate('invalid')).toBeNull();
 | 
					 | 
				
			||||||
    expect(exifToDate(new Date('invalid'))).toBeNull();
 | 
					 | 
				
			||||||
    expect(exifToDate(ExifDateTime.fromEXIF('invalid'))).toBeNull();
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  it('returns a valid date object for valid inputs', () => {
 | 
					 | 
				
			||||||
    const date = new Date('2023');
 | 
					 | 
				
			||||||
    expect(exifToDate(date)).toBeInstanceOf(Date);
 | 
					 | 
				
			||||||
    expect(exifToDate(date)?.toISOString()).toBe('2023-01-01T00:00:00.000Z');
 | 
					 | 
				
			||||||
    expect(exifToDate('2023')).toBeInstanceOf(Date);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const exifDateTime = ExifDateTime.fromISO('2023-01-01T00:00:00.000Z');
 | 
					 | 
				
			||||||
    expect(exifToDate(exifDateTime)).toBeInstanceOf(Date);
 | 
					 | 
				
			||||||
    expect(exifToDate(exifDateTime)?.toISOString()).toBe('2023-01-01T00:00:00.000Z');
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
describe('extracts the timezone from a date', () => {
 | 
					 | 
				
			||||||
  it('returns null for invalid inputs', () => {
 | 
					 | 
				
			||||||
    expect(exifTimeZone(undefined)).toBeNull();
 | 
					 | 
				
			||||||
    expect(exifTimeZone('')).toBeNull();
 | 
					 | 
				
			||||||
    expect(exifTimeZone(new Date('2023'))).toBeNull();
 | 
					 | 
				
			||||||
    expect(exifTimeZone(ExifDateTime.fromEXIF('invalid'))).toBeNull();
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  it('returns the timezone for valid inputs', () => {
 | 
					 | 
				
			||||||
    expect(exifTimeZone(ExifDateTime.fromEXIF('2020:12:29 14:24:45.700-05:00'))).toBe('UTC-5');
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@ -1,24 +0,0 @@
 | 
				
			|||||||
import { ExifDateTime } from 'exiftool-vendored';
 | 
					 | 
				
			||||||
import { isDecimalNumber } from '../numbers';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function exifToDate(exifDate: string | Date | ExifDateTime | undefined): Date | null {
 | 
					 | 
				
			||||||
  if (!exifDate) {
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const date = exifDate instanceof ExifDateTime ? exifDate.toDate() : new Date(exifDate);
 | 
					 | 
				
			||||||
  if (!isDecimalNumber(date.valueOf())) {
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return date;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function exifTimeZone(exifDate: string | Date | ExifDateTime | undefined): string | null {
 | 
					 | 
				
			||||||
  const isExifDate = exifDate instanceof ExifDateTime;
 | 
					 | 
				
			||||||
  if (!isExifDate) {
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return exifDate.zone ?? null;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,24 +0,0 @@
 | 
				
			|||||||
import { describe, expect, it } from '@jest/globals';
 | 
					 | 
				
			||||||
import { parseISO } from './iso';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
describe('parsing ISO values', () => {
 | 
					 | 
				
			||||||
  it('returns null for invalid values', () => {
 | 
					 | 
				
			||||||
    expect(parseISO('')).toBeNull();
 | 
					 | 
				
			||||||
    expect(parseISO(',,,')).toBeNull();
 | 
					 | 
				
			||||||
    expect(parseISO('invalid')).toBeNull();
 | 
					 | 
				
			||||||
    expect(parseISO('-5')).toBeNull();
 | 
					 | 
				
			||||||
    expect(parseISO('99999999999999')).toBeNull();
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  it('returns the ISO number for valid inputs', () => {
 | 
					 | 
				
			||||||
    expect(parseISO('0.0')).toBe(0);
 | 
					 | 
				
			||||||
    expect(parseISO('32000.9')).toBe(32000);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  it('returns the first valid ISO number in a comma separated list', () => {
 | 
					 | 
				
			||||||
    expect(parseISO('400, 200, 100')).toBe(400);
 | 
					 | 
				
			||||||
    expect(parseISO('-1600,800')).toBe(800);
 | 
					 | 
				
			||||||
    expect(parseISO('-1,   a., 1200')).toBe(1200);
 | 
					 | 
				
			||||||
    expect(parseISO('NaN,50,100')).toBe(50);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@ -1,14 +0,0 @@
 | 
				
			|||||||
import { isNumberInRange } from '../numbers';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function parseISO(input: string): number | null {
 | 
					 | 
				
			||||||
  const values = input.split(',');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  for (const value of values) {
 | 
					 | 
				
			||||||
    const iso = Number.parseInt(value, 10);
 | 
					 | 
				
			||||||
    if (isNumberInRange(iso, 0, 2 ** 32)) {
 | 
					 | 
				
			||||||
      return iso;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return null;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -152,11 +152,11 @@ export function getAssetFilename(asset: AssetResponseDto): string {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function isRotated90CW(orientation: number) {
 | 
					function isRotated90CW(orientation: number) {
 | 
				
			||||||
  return orientation == 6 || orientation == 90;
 | 
					  return orientation === 5 || orientation === 6 || orientation === 90;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function isRotated270CW(orientation: number) {
 | 
					function isRotated270CW(orientation: number) {
 | 
				
			||||||
  return orientation == 8 || orientation == -90;
 | 
					  return orientation === 7 || orientation === 8 || orientation === -90;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user