|
@@ -1,4 +1,4 @@
|
|
|
-import { AssetEntity, ExifEntity } from '@app/infra';
|
|
|
+import { AssetEntity, AssetType, ExifEntity } from '@app/infra';
|
|
|
import {
|
|
|
IExifExtractionProcessor,
|
|
|
IReverseGeocodingProcessor,
|
|
@@ -18,7 +18,12 @@ import { Repository } from 'typeorm/repository/Repository';
|
|
|
import geocoder, { InitOptions } from 'local-reverse-geocoder';
|
|
|
import { getName } from 'i18n-iso-countries';
|
|
|
import fs from 'node:fs';
|
|
|
-import { ExifDateTime, exiftool } from 'exiftool-vendored';
|
|
|
+import { ExifDateTime, exiftool, Tags } from 'exiftool-vendored';
|
|
|
+import { IsNull, Not } from 'typeorm';
|
|
|
+
|
|
|
+interface ImmichTags extends Tags {
|
|
|
+ ContentIdentifier?: string;
|
|
|
+}
|
|
|
|
|
|
function geocoderInit(init: InitOptions) {
|
|
|
return new Promise<void>(function (resolve) {
|
|
@@ -139,7 +144,7 @@ export class MetadataExtractionProcessor {
|
|
|
async extractExifInfo(job: Job<IExifExtractionProcessor>) {
|
|
|
try {
|
|
|
const { asset, fileName }: { asset: AssetEntity; fileName: string } = job.data;
|
|
|
- const exifData = await exiftool.read(asset.originalPath).catch((e) => {
|
|
|
+ const exifData = await exiftool.read<ImmichTags>(asset.originalPath).catch((e) => {
|
|
|
this.logger.warn(`The exifData parsing failed due to: ${e} on file ${asset.originalPath}`);
|
|
|
return null;
|
|
|
});
|
|
@@ -177,12 +182,33 @@ export class MetadataExtractionProcessor {
|
|
|
newExif.iso = exifData?.ISO || null;
|
|
|
newExif.latitude = exifData?.GPSLatitude || null;
|
|
|
newExif.longitude = exifData?.GPSLongitude || null;
|
|
|
+ newExif.livePhotoCID = exifData?.MediaGroupUUID || null;
|
|
|
|
|
|
await this.assetRepository.save({
|
|
|
id: asset.id,
|
|
|
createdAt: createdAt?.toISOString(),
|
|
|
});
|
|
|
|
|
|
+ if (newExif.livePhotoCID && !asset.livePhotoVideoId) {
|
|
|
+ const motionAsset = await this.assetRepository.findOne({
|
|
|
+ where: {
|
|
|
+ id: Not(asset.id),
|
|
|
+ type: AssetType.VIDEO,
|
|
|
+ exifInfo: {
|
|
|
+ livePhotoCID: newExif.livePhotoCID,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ relations: {
|
|
|
+ exifInfo: true,
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ if (motionAsset) {
|
|
|
+ await this.assetRepository.update(asset.id, { livePhotoVideoId: motionAsset.id });
|
|
|
+ await this.assetRepository.update(motionAsset.id, { isVisible: false });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Reverse Geocoding
|
|
|
*
|
|
@@ -266,6 +292,11 @@ export class MetadataExtractionProcessor {
|
|
|
createdAt = asset.createdAt;
|
|
|
}
|
|
|
|
|
|
+ const exifData = await exiftool.read<ImmichTags>(asset.originalPath).catch((e) => {
|
|
|
+ this.logger.warn(`The exifData parsing failed due to: ${e} on file ${asset.originalPath}`);
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+
|
|
|
const newExif = new ExifEntity();
|
|
|
newExif.assetId = asset.id;
|
|
|
newExif.description = '';
|
|
@@ -279,6 +310,25 @@ export class MetadataExtractionProcessor {
|
|
|
newExif.state = null;
|
|
|
newExif.country = null;
|
|
|
newExif.fps = null;
|
|
|
+ newExif.livePhotoCID = exifData?.ContentIdentifier || null;
|
|
|
+
|
|
|
+ if (newExif.livePhotoCID) {
|
|
|
+ const photoAsset = await this.assetRepository.findOne({
|
|
|
+ where: {
|
|
|
+ id: Not(asset.id),
|
|
|
+ type: AssetType.IMAGE,
|
|
|
+ livePhotoVideoId: IsNull(),
|
|
|
+ exifInfo: {
|
|
|
+ livePhotoCID: newExif.livePhotoCID,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ if (photoAsset) {
|
|
|
+ await this.assetRepository.update(photoAsset.id, { livePhotoVideoId: asset.id });
|
|
|
+ await this.assetRepository.update(asset.id, { isVisible: false });
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
if (videoTags && videoTags['location']) {
|
|
|
const location = videoTags['location'] as string;
|