Explorar o código

Added schedule job to perform reverse geocoding if key is added after backing up assets (#305)

Alex %!s(int64=3) %!d(string=hai) anos
pai
achega
357f7d1c31

+ 16 - 2
server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts

@@ -3,11 +3,16 @@ import { Module } from '@nestjs/common';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { AssetEntity } from '@app/database/entities/asset.entity';
 import { ScheduleTasksService } from './schedule-tasks.service';
-import { thumbnailGeneratorQueueName, videoConversionQueueName } from '@app/job/constants/queue-name.constant';
+import {
+  metadataExtractionQueueName,
+  thumbnailGeneratorQueueName,
+  videoConversionQueueName,
+} from '@app/job/constants/queue-name.constant';
+import { ExifEntity } from '@app/database/entities/exif.entity';
 
 @Module({
   imports: [
-    TypeOrmModule.forFeature([AssetEntity]),
+    TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
     BullModule.registerQueue({
       name: videoConversionQueueName,
       defaultJobOptions: {
@@ -24,6 +29,15 @@ import { thumbnailGeneratorQueueName, videoConversionQueueName } from '@app/job/
         removeOnFail: false,
       },
     }),
+
+    BullModule.registerQueue({
+      name: metadataExtractionQueueName,
+      defaultJobOptions: {
+        attempts: 3,
+        removeOnComplete: true,
+        removeOnFail: false,
+      },
+    }),
   ],
   providers: [ScheduleTasksService],
 })

+ 40 - 6
server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts

@@ -1,14 +1,23 @@
 import { Injectable, Logger } from '@nestjs/common';
 import { Cron, CronExpression } from '@nestjs/schedule';
 import { InjectRepository } from '@nestjs/typeorm';
-import { Repository } from 'typeorm';
+import { IsNull, Not, Repository } from 'typeorm';
 import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
 import { InjectQueue } from '@nestjs/bull';
 import { Queue } from 'bull';
 import { randomUUID } from 'crypto';
-import { generateWEBPThumbnailProcessorName, mp4ConversionProcessorName } from '@app/job/constants/job-name.constant';
-import { thumbnailGeneratorQueueName, videoConversionQueueName } from '@app/job/constants/queue-name.constant';
-import { IVideoTranscodeJob } from '@app/job/interfaces/video-transcode.interface';
+import { ExifEntity } from '@app/database/entities/exif.entity';
+import {
+  IMetadataExtractionJob,
+  IVideoTranscodeJob,
+  metadataExtractionQueueName,
+  thumbnailGeneratorQueueName,
+  videoConversionQueueName,
+  generateWEBPThumbnailProcessorName,
+  mp4ConversionProcessorName,
+  reverseGeocodingProcessorName,
+} from '@app/job';
+import { ConfigService } from '@nestjs/config';
 
 @Injectable()
 export class ScheduleTasksService {
@@ -16,17 +25,23 @@ export class ScheduleTasksService {
     @InjectRepository(AssetEntity)
     private assetRepository: Repository<AssetEntity>,
 
+    @InjectRepository(ExifEntity)
+    private exifRepository: Repository<ExifEntity>,
+
     @InjectQueue(thumbnailGeneratorQueueName)
     private thumbnailGeneratorQueue: Queue,
 
     @InjectQueue(videoConversionQueueName)
     private videoConversionQueue: Queue<IVideoTranscodeJob>,
+
+    @InjectQueue(metadataExtractionQueueName)
+    private metadataExtractionQueue: Queue<IMetadataExtractionJob>,
+
+    private configService: ConfigService,
   ) {}
 
   @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
   async webpConversion() {
-    Logger.log('Starting Schedule Webp Conversion Tasks', 'CronjobWebpGenerator');
-
     const assets = await this.assetRepository.find({
       where: {
         webpPath: '',
@@ -64,4 +79,23 @@ export class ScheduleTasksService {
       await this.videoConversionQueue.add(mp4ConversionProcessorName, { asset }, { jobId: randomUUID() });
     }
   }
+
+  @Cron(CronExpression.EVERY_5_SECONDS)
+  async reverseGeocoding() {
+    const isMapboxEnable = this.configService.get('ENABLE_MAPBOX');
+
+    if (isMapboxEnable) {
+      const exifInfo = await this.exifRepository.find({
+        where: {
+          city: IsNull(),
+          longitude: Not(IsNull()),
+          latitude: Not(IsNull()),
+        },
+      });
+
+      for (const exif of exifInfo) {
+        await this.metadataExtractionQueue.add(reverseGeocodingProcessorName, { exif }, { jobId: randomUUID() });
+      }
+    }
+  }
 }

+ 24 - 0
server/apps/microservices/src/processors/metadata-extraction.processor.ts

@@ -21,6 +21,8 @@ import {
   objectDetectionProcessorName,
   videoMetadataExtractionProcessorName,
   metadataExtractionQueueName,
+  reverseGeocodingProcessorName,
+  IReverseGeocodingProcessor,
 } from '@app/job';
 
 @Processor(metadataExtractionQueueName)
@@ -98,6 +100,28 @@ export class MetadataExtractionProcessor {
     }
   }
 
+  @Process({ name: reverseGeocodingProcessorName })
+  async reverseGeocoding(job: Job<IReverseGeocodingProcessor>) {
+    const { exif } = job.data;
+
+    if (this.geocodingClient) {
+      const geoCodeInfo: MapiResponse = await this.geocodingClient
+        .reverseGeocode({
+          query: [Number(exif.longitude), Number(exif.latitude)],
+          types: ['country', 'region', 'place'],
+        })
+        .send();
+
+      const res: [] = geoCodeInfo.body['features'];
+
+      const city = res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]['text'];
+      const state = res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]['text'];
+      const country = res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]['text'];
+
+      await this.exifRepository.update({ id: exif.id }, { city, state, country });
+    }
+  }
+
   @Process({ name: imageTaggingProcessorName, concurrency: 2 })
   async tagImage(job: Job) {
     const { asset }: { asset: AssetEntity } = job.data;

+ 1 - 0
server/libs/job/src/constants/job-name.constant.ts

@@ -19,5 +19,6 @@ export const generateWEBPThumbnailProcessorName = 'generate-webp-thumbnail';
  */
 export const exifExtractionProcessorName = 'exif-extraction';
 export const videoMetadataExtractionProcessorName = 'extract-video-metadata';
+export const reverseGeocodingProcessorName = 'reverse-geocoding';
 export const objectDetectionProcessorName = 'detect-object';
 export const imageTaggingProcessorName = 'tag-image';

+ 12 - 1
server/libs/job/src/interfaces/metadata-extraction.interface.ts

@@ -1,4 +1,5 @@
 import { AssetEntity } from '@app/database/entities/asset.entity';
+import { ExifEntity } from '@app/database/entities/exif.entity';
 
 export interface IExifExtractionProcessor {
   /**
@@ -24,4 +25,14 @@ export interface IVideoLengthExtractionProcessor {
   asset: AssetEntity;
 }
 
-export type IMetadataExtractionJob = IExifExtractionProcessor | IVideoLengthExtractionProcessor;
+export interface IReverseGeocodingProcessor {
+  /**
+   * The Asset entity that was saved in the database
+   */
+  exif: ExifEntity;
+}
+
+export type IMetadataExtractionJob =
+  | IExifExtractionProcessor
+  | IVideoLengthExtractionProcessor
+  | IReverseGeocodingProcessor;