mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:37:11 -04: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; |  | ||||||
| 
 |  | ||||||
|     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 { |  | ||||||
|         newExif.timeZone = tz_lookup(newExif.latitude, newExif.longitude); |  | ||||||
|       } catch (error: any) { |  | ||||||
|         this.logger.warn(`Error while calculating timezone from gps coordinates: ${error}`, error?.stack); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     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( |  | ||||||
|           `Unable to run reverse geocoding due to ${error} for asset ${asset.id} at ${asset.originalPath}`, |  | ||||||
|           error?.stack, |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   private async applyMotionPhotos(asset: AssetEntity, data: MotionPhotosData) { |  | ||||||
|     if (asset.livePhotoVideoId) { |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const { isMotionPhoto, isMicroVideo, directory, videoOffset } = data; |     try { | ||||||
|  |       const { city, state, country } = await this.geocodingRepository.reverseGeocode({ latitude, longitude }); | ||||||
|  |       Object.assign(exifData, { city, state, country }); | ||||||
|  |     } catch (error: Error | any) { | ||||||
|  |       this.logger.warn( | ||||||
|  |         `Unable to run reverse geocoding due to ${error} for asset ${asset.id} at ${asset.originalPath}`, | ||||||
|  |         error?.stack, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private async applyMotionPhotos(asset: AssetEntity, tags: ImmichTags) { | ||||||
|  |     if (asset.type !== AssetType.IMAGE || asset.livePhotoVideoId) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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