Browse Source

Remove thumbnail generation on mobile app (#292)

* Remove thumbnail generation on mobile

* Remove tconditions for missing thumbnail on the backend

* Remove console.log

* Refactor queue systems

* Convert queue and processor name to constant

* Added corresponding interface to job queue
Alex 3 năm trước cách đây
mục cha
commit
76bf1c0379

+ 0 - 18
mobile/lib/modules/backup/services/backup.service.dart

@@ -69,21 +69,6 @@ class BackupService {
             ),
             ),
           );
           );
 
 
-          // Build thumbnail multipart data
-          var thumbnailData = await entity
-              .thumbnailDataWithSize(const ThumbnailSize(1440, 2560));
-          if (thumbnailData != null) {
-            thumbnailUploadData = http.MultipartFile.fromBytes(
-              "thumbnailData",
-              List.from(thumbnailData),
-              filename: fileNameWithoutPath,
-              contentType: MediaType(
-                "image",
-                "jpeg",
-              ),
-            );
-          }
-
           var box = Hive.box(userInfoBox);
           var box = Hive.box(userInfoBox);
 
 
           var req = MultipartRequest(
           var req = MultipartRequest(
@@ -101,9 +86,6 @@ class BackupService {
           req.fields['fileExtension'] = fileExtension;
           req.fields['fileExtension'] = fileExtension;
           req.fields['duration'] = entity.videoDuration.toString();
           req.fields['duration'] = entity.videoDuration.toString();
 
 
-          if (thumbnailUploadData != null) {
-            req.files.add(thumbnailUploadData);
-          }
           req.files.add(assetRawUploadData);
           req.files.add(assetRawUploadData);
 
 
           var res = await req.send(cancellationToken: cancelToken);
           var res = await req.send(cancellationToken: cancelToken);

+ 12 - 25
server/apps/immich/src/api-v1/asset/asset.controller.ts

@@ -31,6 +31,9 @@ import { SearchAssetDto } from './dto/search-asset.dto';
 import { CommunicationGateway } from '../communication/communication.gateway';
 import { CommunicationGateway } from '../communication/communication.gateway';
 import { InjectQueue } from '@nestjs/bull';
 import { InjectQueue } from '@nestjs/bull';
 import { Queue } from 'bull';
 import { Queue } from 'bull';
+import { IAssetUploadedJob } from '@app/job/index';
+import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
+import { assetUploadedProcessorName } from '@app/job/constants/job-name.constant';
 
 
 @UseGuards(JwtAuthGuard)
 @UseGuards(JwtAuthGuard)
 @Controller('asset')
 @Controller('asset')
@@ -40,8 +43,8 @@ export class AssetController {
     private assetService: AssetService,
     private assetService: AssetService,
     private backgroundTaskService: BackgroundTaskService,
     private backgroundTaskService: BackgroundTaskService,
 
 
-    @InjectQueue('asset-uploaded-queue')
-    private assetUploadedQueue: Queue,
+    @InjectQueue(assetUploadedQueueName)
+    private assetUploadedQueue: Queue<IAssetUploadedJob>,
   ) {}
   ) {}
 
 
   @Post('upload')
   @Post('upload')
@@ -56,7 +59,7 @@ export class AssetController {
   )
   )
   async uploadFile(
   async uploadFile(
     @GetAuthUser() authUser: AuthUserDto,
     @GetAuthUser() authUser: AuthUserDto,
-    @UploadedFiles() uploadFiles: { assetData: Express.Multer.File[]; thumbnailData?: Express.Multer.File[] },
+    @UploadedFiles() uploadFiles: { assetData: Express.Multer.File[] },
     @Body(ValidationPipe) assetInfo: CreateAssetDto,
     @Body(ValidationPipe) assetInfo: CreateAssetDto,
   ): Promise<'ok' | undefined> {
   ): Promise<'ok' | undefined> {
     for (const file of uploadFiles.assetData) {
     for (const file of uploadFiles.assetData) {
@@ -66,28 +69,12 @@ export class AssetController {
         if (!savedAsset) {
         if (!savedAsset) {
           return;
           return;
         }
         }
-        if (uploadFiles.thumbnailData != null) {
-          const assetWithThumbnail = await this.assetService.updateThumbnailInfo(
-            savedAsset,
-            uploadFiles.thumbnailData[0].path,
-          );
-
-          await this.assetUploadedQueue.add(
-            'asset-uploaded',
-            { asset: assetWithThumbnail, fileName: file.originalname, fileSize: file.size, hasThumbnail: true },
-            { jobId: savedAsset.id },
-          );
-
-          this.wsCommunicateionGateway.server
-            .to(savedAsset.userId)
-            .emit('on_upload_success', JSON.stringify(assetWithThumbnail));
-        } else {
-          await this.assetUploadedQueue.add(
-            'asset-uploaded',
-            { asset: savedAsset, fileName: file.originalname, fileSize: file.size, hasThumbnail: false },
-            { jobId: savedAsset.id },
-          );
-        }
+
+        await this.assetUploadedQueue.add(
+          assetUploadedProcessorName,
+          { asset: savedAsset, fileName: file.originalname, fileSize: file.size },
+          { jobId: savedAsset.id },
+        );
       } catch (e) {
       } catch (e) {
         Logger.error(`Error receiving upload file ${e}`);
         Logger.error(`Error receiving upload file ${e}`);
       }
       }

+ 2 - 1
server/apps/immich/src/api-v1/asset/asset.module.ts

@@ -7,6 +7,7 @@ import { BullModule } from '@nestjs/bull';
 import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
 import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
 import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
 import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
 import { CommunicationModule } from '../communication/communication.module';
 import { CommunicationModule } from '../communication/communication.module';
+import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
 
 
 @Module({
 @Module({
   imports: [
   imports: [
@@ -14,7 +15,7 @@ import { CommunicationModule } from '../communication/communication.module';
     BackgroundTaskModule,
     BackgroundTaskModule,
     TypeOrmModule.forFeature([AssetEntity]),
     TypeOrmModule.forFeature([AssetEntity]),
     BullModule.registerQueue({
     BullModule.registerQueue({
-      name: 'asset-uploaded-queue',
+      name: assetUploadedQueueName,
       defaultJobOptions: {
       defaultJobOptions: {
         attempts: 3,
         attempts: 3,
         removeOnComplete: true,
         removeOnComplete: true,

+ 8 - 23
server/apps/immich/src/config/asset-upload.config.ts

@@ -6,7 +6,6 @@ import { extname } from 'path';
 import { Request } from 'express';
 import { Request } from 'express';
 import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant';
 import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant';
 import { randomUUID } from 'crypto';
 import { randomUUID } from 'crypto';
-// import { CreateAssetDto } from '../api-v1/asset/dto/create-asset.dto';
 
 
 export const assetUploadOption: MulterOptions = {
 export const assetUploadOption: MulterOptions = {
   fileFilter: (req: Request, file: any, cb: any) => {
   fileFilter: (req: Request, file: any, cb: any) => {
@@ -30,34 +29,20 @@ export const assetUploadOption: MulterOptions = {
         return;
         return;
       }
       }
 
 
-      if (file.fieldname == 'assetData') {
-        const originalUploadFolder = `${basePath}/${req.user.id}/original/${req.body['deviceId']}`;
+      const originalUploadFolder = `${basePath}/${req.user.id}/original/${req.body['deviceId']}`;
 
 
-        if (!existsSync(originalUploadFolder)) {
-          mkdirSync(originalUploadFolder, { recursive: true });
-        }
-
-        // Save original to disk
-        cb(null, originalUploadFolder);
-      } else if (file.fieldname == 'thumbnailData') {
-        const thumbnailUploadFolder = `${basePath}/${req.user.id}/thumb/${req.body['deviceId']}`;
-
-        if (!existsSync(thumbnailUploadFolder)) {
-          mkdirSync(thumbnailUploadFolder, { recursive: true });
-        }
-
-        // Save thumbnail to disk
-        cb(null, thumbnailUploadFolder);
+      if (!existsSync(originalUploadFolder)) {
+        mkdirSync(originalUploadFolder, { recursive: true });
       }
       }
+
+      // Save original to disk
+      cb(null, originalUploadFolder);
     },
     },
 
 
     filename: (req: Request, file: Express.Multer.File, cb: any) => {
     filename: (req: Request, file: Express.Multer.File, cb: any) => {
       const fileNameUUID = randomUUID();
       const fileNameUUID = randomUUID();
-      if (file.fieldname == 'assetData') {
-        cb(null, `${fileNameUUID}${req.body['fileExtension'].toLowerCase()}`);
-      } else if (file.fieldname == 'thumbnailData') {
-        cb(null, `${fileNameUUID}.jpeg`);
-      }
+
+      cb(null, `${fileNameUUID}${req.body['fileExtension'].toLowerCase()}`);
     },
     },
   }),
   }),
 };
 };

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

@@ -3,12 +3,13 @@ import { Module } from '@nestjs/common';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { AssetEntity } from '@app/database/entities/asset.entity';
 import { AssetEntity } from '@app/database/entities/asset.entity';
 import { ScheduleTasksService } from './schedule-tasks.service';
 import { ScheduleTasksService } from './schedule-tasks.service';
+import { thumbnailGeneratorQueueName, videoConversionQueueName } from '@app/job/constants/queue-name.constant';
 
 
 @Module({
 @Module({
   imports: [
   imports: [
     TypeOrmModule.forFeature([AssetEntity]),
     TypeOrmModule.forFeature([AssetEntity]),
     BullModule.registerQueue({
     BullModule.registerQueue({
-      name: 'video-conversion-queue',
+      name: videoConversionQueueName,
       defaultJobOptions: {
       defaultJobOptions: {
         attempts: 3,
         attempts: 3,
         removeOnComplete: true,
         removeOnComplete: true,
@@ -16,7 +17,7 @@ import { ScheduleTasksService } from './schedule-tasks.service';
       },
       },
     }),
     }),
     BullModule.registerQueue({
     BullModule.registerQueue({
-      name: 'thumbnail-generator-queue',
+      name: thumbnailGeneratorQueueName,
       defaultJobOptions: {
       defaultJobOptions: {
         attempts: 3,
         attempts: 3,
         removeOnComplete: true,
         removeOnComplete: true,

+ 12 - 5
server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts

@@ -6,6 +6,9 @@ import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
 import { InjectQueue } from '@nestjs/bull';
 import { InjectQueue } from '@nestjs/bull';
 import { Queue } from 'bull';
 import { Queue } from 'bull';
 import { randomUUID } from 'crypto';
 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';
 
 
 @Injectable()
 @Injectable()
 export class ScheduleTasksService {
 export class ScheduleTasksService {
@@ -13,11 +16,11 @@ export class ScheduleTasksService {
     @InjectRepository(AssetEntity)
     @InjectRepository(AssetEntity)
     private assetRepository: Repository<AssetEntity>,
     private assetRepository: Repository<AssetEntity>,
 
 
-    @InjectQueue('thumbnail-generator-queue')
+    @InjectQueue(thumbnailGeneratorQueueName)
     private thumbnailGeneratorQueue: Queue,
     private thumbnailGeneratorQueue: Queue,
 
 
-    @InjectQueue('video-conversion-queue')
-    private videoConversionQueue: Queue,
+    @InjectQueue(videoConversionQueueName)
+    private videoConversionQueue: Queue<IVideoTranscodeJob>,
   ) {}
   ) {}
 
 
   @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
   @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
@@ -36,7 +39,11 @@ export class ScheduleTasksService {
     }
     }
 
 
     for (const asset of assets) {
     for (const asset of assets) {
-      await this.thumbnailGeneratorQueue.add('generate-webp-thumbnail', { asset: asset }, { jobId: randomUUID() });
+      await this.thumbnailGeneratorQueue.add(
+        generateWEBPThumbnailProcessorName,
+        { asset: asset },
+        { jobId: randomUUID() },
+      );
     }
     }
   }
   }
 
 
@@ -54,7 +61,7 @@ export class ScheduleTasksService {
     });
     });
 
 
     for (const asset of assets) {
     for (const asset of assets) {
-      await this.videoConversionQueue.add('mp4-conversion', { asset }, { jobId: randomUUID() });
+      await this.videoConversionQueue.add(mp4ConversionProcessorName, { asset }, { jobId: randomUUID() });
     }
     }
   }
   }
 }
 }

+ 10 - 4
server/apps/microservices/src/microservices.module.ts

@@ -12,6 +12,12 @@ import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor';
 import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor';
 import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor';
 import { VideoTranscodeProcessor } from './processors/video-transcode.processor';
 import { VideoTranscodeProcessor } from './processors/video-transcode.processor';
 import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module';
 import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module';
+import {
+  assetUploadedQueueName,
+  metadataExtractionQueueName,
+  thumbnailGeneratorQueueName,
+  videoConversionQueueName,
+} from '@app/job/constants/queue-name.constant';
 
 
 @Module({
 @Module({
   imports: [
   imports: [
@@ -26,7 +32,7 @@ import { CommunicationModule } from '../../immich/src/api-v1/communication/commu
       }),
       }),
     }),
     }),
     BullModule.registerQueue({
     BullModule.registerQueue({
-      name: 'thumbnail-generator-queue',
+      name: thumbnailGeneratorQueueName,
       defaultJobOptions: {
       defaultJobOptions: {
         attempts: 3,
         attempts: 3,
         removeOnComplete: true,
         removeOnComplete: true,
@@ -34,7 +40,7 @@ import { CommunicationModule } from '../../immich/src/api-v1/communication/commu
       },
       },
     }),
     }),
     BullModule.registerQueue({
     BullModule.registerQueue({
-      name: 'asset-uploaded-queue',
+      name: assetUploadedQueueName,
       defaultJobOptions: {
       defaultJobOptions: {
         attempts: 3,
         attempts: 3,
         removeOnComplete: true,
         removeOnComplete: true,
@@ -42,7 +48,7 @@ import { CommunicationModule } from '../../immich/src/api-v1/communication/commu
       },
       },
     }),
     }),
     BullModule.registerQueue({
     BullModule.registerQueue({
-      name: 'metadata-extraction-queue',
+      name: metadataExtractionQueueName,
       defaultJobOptions: {
       defaultJobOptions: {
         attempts: 3,
         attempts: 3,
         removeOnComplete: true,
         removeOnComplete: true,
@@ -50,7 +56,7 @@ import { CommunicationModule } from '../../immich/src/api-v1/communication/commu
       },
       },
     }),
     }),
     BullModule.registerQueue({
     BullModule.registerQueue({
-      name: 'video-conversion-queue',
+      name: videoConversionQueueName,
       defaultJobOptions: {
       defaultJobOptions: {
         attempts: 3,
         attempts: 3,
         removeOnComplete: true,
         removeOnComplete: true,

+ 32 - 35
server/apps/microservices/src/processors/asset-uploaded.processor.ts

@@ -1,61 +1,58 @@
 import { InjectQueue, Process, Processor } from '@nestjs/bull';
 import { InjectQueue, Process, Processor } from '@nestjs/bull';
 import { Job, Queue } from 'bull';
 import { Job, Queue } from 'bull';
-import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
-import { InjectRepository } from '@nestjs/typeorm';
-import { Repository } from 'typeorm';
+import { AssetType } from '@app/database/entities/asset.entity';
 import { randomUUID } from 'crypto';
 import { randomUUID } from 'crypto';
+import {
+  IAssetUploadedJob,
+  IMetadataExtractionJob,
+  IThumbnailGenerationJob,
+  IVideoTranscodeJob,
+  assetUploadedQueueName,
+  metadataExtractionQueueName,
+  thumbnailGeneratorQueueName,
+  videoConversionQueueName,
+  assetUploadedProcessorName,
+  exifExtractionProcessorName,
+  generateJPEGThumbnailProcessorName,
+  mp4ConversionProcessorName,
+  videoLengthExtractionProcessorName,
+} from '@app/job';
 
 
-@Processor('asset-uploaded-queue')
+@Processor(assetUploadedQueueName)
 export class AssetUploadedProcessor {
 export class AssetUploadedProcessor {
   constructor(
   constructor(
-    @InjectQueue('thumbnail-generator-queue')
-    private thumbnailGeneratorQueue: Queue,
+    @InjectQueue(thumbnailGeneratorQueueName)
+    private thumbnailGeneratorQueue: Queue<IThumbnailGenerationJob>,
 
 
-    @InjectQueue('metadata-extraction-queue')
-    private metadataExtractionQueue: Queue,
+    @InjectQueue(metadataExtractionQueueName)
+    private metadataExtractionQueue: Queue<IMetadataExtractionJob>,
 
 
-    @InjectQueue('video-conversion-queue')
-    private videoConversionQueue: Queue,
-
-    @InjectRepository(AssetEntity)
-    private assetRepository: Repository<AssetEntity>,
+    @InjectQueue(videoConversionQueueName)
+    private videoConversionQueue: Queue<IVideoTranscodeJob>,
   ) {}
   ) {}
 
 
   /**
   /**
    * Post processing uploaded asset to perform the following function if missing
    * Post processing uploaded asset to perform the following function if missing
    * 1. Generate JPEG Thumbnail
    * 1. Generate JPEG Thumbnail
-   * 2. Generate Webp Thumbnail <-> if JPEG thumbnail exist
+   * 2. Generate Webp Thumbnail
    * 3. EXIF extractor
    * 3. EXIF extractor
    * 4. Reverse Geocoding
    * 4. Reverse Geocoding
    *
    *
    * @param job asset-uploaded
    * @param job asset-uploaded
    */
    */
-  @Process('asset-uploaded')
-  async processUploadedVideo(job: Job) {
-    const {
-      asset,
-      fileName,
-      fileSize,
-      hasThumbnail,
-    }: { asset: AssetEntity; fileName: string; fileSize: number; hasThumbnail: boolean } = job.data;
+  @Process(assetUploadedProcessorName)
+  async processUploadedVideo(job: Job<IAssetUploadedJob>) {
+    const { asset, fileName, fileSize } = job.data;
 
 
-    if (hasThumbnail) {
-      // The jobs below depends on the existence of jpeg thumbnail
-      await this.thumbnailGeneratorQueue.add('generate-webp-thumbnail', { asset }, { jobId: randomUUID() });
-      await this.metadataExtractionQueue.add('tag-image', { asset }, { jobId: randomUUID() });
-      await this.metadataExtractionQueue.add('detect-object', { asset }, { jobId: randomUUID() });
-    } else {
-      // Generate Thumbnail -> Then generate webp, tag image and detect object
-      await this.thumbnailGeneratorQueue.add('generate-jpeg-thumbnail', { asset }, { jobId: randomUUID() });
-    }
+    await this.thumbnailGeneratorQueue.add(generateJPEGThumbnailProcessorName, { asset }, { jobId: randomUUID() });
 
 
     // Video Conversion
     // Video Conversion
     if (asset.type == AssetType.VIDEO) {
     if (asset.type == AssetType.VIDEO) {
-      await this.videoConversionQueue.add('mp4-conversion', { asset }, { jobId: randomUUID() });
+      await this.videoConversionQueue.add(mp4ConversionProcessorName, { asset }, { jobId: randomUUID() });
     } else {
     } else {
-      // Extract Metadata/Exif for Images - Currently the library cannot extract EXIF for video yet
+      // Extract Metadata/Exif for Images - Currently the EXIF library on the web cannot extract EXIF for video yet
       await this.metadataExtractionQueue.add(
       await this.metadataExtractionQueue.add(
-        'exif-extraction',
+        exifExtractionProcessorName,
         {
         {
           asset,
           asset,
           fileName,
           fileName,
@@ -67,7 +64,7 @@ export class AssetUploadedProcessor {
 
 
     // Extract video duration if uploaded from the web
     // Extract video duration if uploaded from the web
     if (asset.type == AssetType.VIDEO && asset.duration == '0:00:00.000000') {
     if (asset.type == AssetType.VIDEO && asset.duration == '0:00:00.000000') {
-      await this.metadataExtractionQueue.add('extract-video-length', { asset }, { jobId: randomUUID() });
+      await this.metadataExtractionQueue.add(videoLengthExtractionProcessorName, { asset }, { jobId: randomUUID() });
     }
     }
   }
   }
 }
 }

+ 18 - 9
server/apps/microservices/src/processors/metadata-extraction.processor.ts

@@ -13,8 +13,17 @@ import axios from 'axios';
 import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
 import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
 import ffmpeg from 'fluent-ffmpeg';
 import ffmpeg from 'fluent-ffmpeg';
 import path from 'path';
 import path from 'path';
-
-@Processor('metadata-extraction-queue')
+import {
+  IExifExtractionProcessor,
+  IVideoLengthExtractionProcessor,
+  exifExtractionProcessorName,
+  imageTaggingProcessorName,
+  objectDetectionProcessorName,
+  videoLengthExtractionProcessorName,
+  metadataExtractionQueueName,
+} from '@app/job';
+
+@Processor(metadataExtractionQueueName)
 export class MetadataExtractionProcessor {
 export class MetadataExtractionProcessor {
   private geocodingClient?: GeocodeService;
   private geocodingClient?: GeocodeService;
 
 
@@ -35,8 +44,8 @@ export class MetadataExtractionProcessor {
     }
     }
   }
   }
 
 
-  @Process('exif-extraction')
-  async extractExifInfo(job: Job) {
+  @Process(exifExtractionProcessorName)
+  async extractExifInfo(job: Job<IExifExtractionProcessor>) {
     try {
     try {
       const { asset, fileName, fileSize }: { asset: AssetEntity; fileName: string; fileSize: number } = job.data;
       const { asset, fileName, fileSize }: { asset: AssetEntity; fileName: string; fileSize: number } = job.data;
 
 
@@ -89,7 +98,7 @@ export class MetadataExtractionProcessor {
     }
     }
   }
   }
 
 
-  @Process({ name: 'tag-image', concurrency: 2 })
+  @Process({ name: imageTaggingProcessorName, concurrency: 2 })
   async tagImage(job: Job) {
   async tagImage(job: Job) {
     const { asset }: { asset: AssetEntity } = job.data;
     const { asset }: { asset: AssetEntity } = job.data;
 
 
@@ -108,7 +117,7 @@ export class MetadataExtractionProcessor {
     }
     }
   }
   }
 
 
-  @Process({ name: 'detect-object', concurrency: 2 })
+  @Process({ name: objectDetectionProcessorName, concurrency: 2 })
   async detectObject(job: Job) {
   async detectObject(job: Job) {
     try {
     try {
       const { asset }: { asset: AssetEntity } = job.data;
       const { asset }: { asset: AssetEntity } = job.data;
@@ -131,9 +140,9 @@ export class MetadataExtractionProcessor {
     }
     }
   }
   }
 
 
-  @Process({ name: 'extract-video-length', concurrency: 2 })
-  async extractVideoLength(job: Job) {
-    const { asset }: { asset: AssetEntity } = job.data;
+  @Process({ name: videoLengthExtractionProcessorName, concurrency: 2 })
+  async extractVideoLength(job: Job<IVideoLengthExtractionProcessor>) {
+    const { asset } = job.data;
 
 
     ffmpeg.ffprobe(asset.originalPath, async (err, data) => {
     ffmpeg.ffprobe(asset.originalPath, async (err, data) => {
       if (!err) {
       if (!err) {

+ 35 - 15
server/apps/microservices/src/processors/thumbnail.processor.ts

@@ -9,25 +9,35 @@ import { randomUUID } from 'node:crypto';
 import { CommunicationGateway } from '../../../immich/src/api-v1/communication/communication.gateway';
 import { CommunicationGateway } from '../../../immich/src/api-v1/communication/communication.gateway';
 import ffmpeg from 'fluent-ffmpeg';
 import ffmpeg from 'fluent-ffmpeg';
 import { Logger } from '@nestjs/common';
 import { Logger } from '@nestjs/common';
-
-@Processor('thumbnail-generator-queue')
+import {
+  WebpGeneratorProcessor,
+  generateJPEGThumbnailProcessorName,
+  generateWEBPThumbnailProcessorName,
+  imageTaggingProcessorName,
+  objectDetectionProcessorName,
+  metadataExtractionQueueName,
+  thumbnailGeneratorQueueName,
+  JpegGeneratorProcessor,
+} from '@app/job';
+
+@Processor(thumbnailGeneratorQueueName)
 export class ThumbnailGeneratorProcessor {
 export class ThumbnailGeneratorProcessor {
   constructor(
   constructor(
     @InjectRepository(AssetEntity)
     @InjectRepository(AssetEntity)
     private assetRepository: Repository<AssetEntity>,
     private assetRepository: Repository<AssetEntity>,
 
 
-    @InjectQueue('thumbnail-generator-queue')
+    @InjectQueue(thumbnailGeneratorQueueName)
     private thumbnailGeneratorQueue: Queue,
     private thumbnailGeneratorQueue: Queue,
 
 
     private wsCommunicateionGateway: CommunicationGateway,
     private wsCommunicateionGateway: CommunicationGateway,
 
 
-    @InjectQueue('metadata-extraction-queue')
+    @InjectQueue(metadataExtractionQueueName)
     private metadataExtractionQueue: Queue,
     private metadataExtractionQueue: Queue,
   ) {}
   ) {}
 
 
-  @Process('generate-jpeg-thumbnail')
-  async generateJPEGThumbnail(job: Job) {
-    const { asset }: { asset: AssetEntity } = job.data;
+  @Process({ name: generateJPEGThumbnailProcessorName, concurrency: 3 })
+  async generateJPEGThumbnail(job: Job<JpegGeneratorProcessor>) {
+    const { asset } = job.data;
 
 
     const resizePath = `upload/${asset.userId}/thumb/${asset.deviceId}/`;
     const resizePath = `upload/${asset.userId}/thumb/${asset.deviceId}/`;
 
 
@@ -43,6 +53,7 @@ export class ThumbnailGeneratorProcessor {
       sharp(asset.originalPath)
       sharp(asset.originalPath)
         .resize(1440, 2560, { fit: 'inside' })
         .resize(1440, 2560, { fit: 'inside' })
         .jpeg()
         .jpeg()
+        .rotate()
         .toFile(jpegThumbnailPath, async (err) => {
         .toFile(jpegThumbnailPath, async (err) => {
           if (!err) {
           if (!err) {
             await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
             await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
@@ -50,9 +61,13 @@ export class ThumbnailGeneratorProcessor {
             // Update resize path to send to generate webp queue
             // Update resize path to send to generate webp queue
             asset.resizePath = jpegThumbnailPath;
             asset.resizePath = jpegThumbnailPath;
 
 
-            await this.thumbnailGeneratorQueue.add('generate-webp-thumbnail', { asset }, { jobId: randomUUID() });
-            await this.metadataExtractionQueue.add('tag-image', { asset }, { jobId: randomUUID() });
-            await this.metadataExtractionQueue.add('detect-object', { asset }, { jobId: randomUUID() });
+            await this.thumbnailGeneratorQueue.add(
+              generateWEBPThumbnailProcessorName,
+              { asset },
+              { jobId: randomUUID() },
+            );
+            await this.metadataExtractionQueue.add(imageTaggingProcessorName, { asset }, { jobId: randomUUID() });
+            await this.metadataExtractionQueue.add(objectDetectionProcessorName, { asset }, { jobId: randomUUID() });
             this.wsCommunicateionGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(asset));
             this.wsCommunicateionGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(asset));
           }
           }
         });
         });
@@ -76,9 +91,13 @@ export class ThumbnailGeneratorProcessor {
           // Update resize path to send to generate webp queue
           // Update resize path to send to generate webp queue
           asset.resizePath = jpegThumbnailPath;
           asset.resizePath = jpegThumbnailPath;
 
 
-          await this.thumbnailGeneratorQueue.add('generate-webp-thumbnail', { asset }, { jobId: randomUUID() });
-          await this.metadataExtractionQueue.add('tag-image', { asset }, { jobId: randomUUID() });
-          await this.metadataExtractionQueue.add('detect-object', { asset }, { jobId: randomUUID() });
+          await this.thumbnailGeneratorQueue.add(
+            generateWEBPThumbnailProcessorName,
+            { asset },
+            { jobId: randomUUID() },
+          );
+          await this.metadataExtractionQueue.add(imageTaggingProcessorName, { asset }, { jobId: randomUUID() });
+          await this.metadataExtractionQueue.add(objectDetectionProcessorName, { asset }, { jobId: randomUUID() });
 
 
           this.wsCommunicateionGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(asset));
           this.wsCommunicateionGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(asset));
         })
         })
@@ -86,8 +105,8 @@ export class ThumbnailGeneratorProcessor {
     }
     }
   }
   }
 
 
-  @Process({ name: 'generate-webp-thumbnail', concurrency: 2 })
-  async generateWepbThumbnail(job: Job<{ asset: AssetEntity }>) {
+  @Process({ name: generateWEBPThumbnailProcessorName, concurrency: 3 })
+  async generateWepbThumbnail(job: Job<WebpGeneratorProcessor>) {
     const { asset } = job.data;
     const { asset } = job.data;
 
 
     if (!asset.resizePath) {
     if (!asset.resizePath) {
@@ -98,6 +117,7 @@ export class ThumbnailGeneratorProcessor {
     sharp(asset.resizePath)
     sharp(asset.resizePath)
       .resize(250)
       .resize(250)
       .webp()
       .webp()
+      .rotate()
       .toFile(webpPath, (err) => {
       .toFile(webpPath, (err) => {
         if (!err) {
         if (!err) {
           this.assetRepository.update({ id: asset.id }, { webpPath: webpPath });
           this.assetRepository.update({ id: asset.id }, { webpPath: webpPath });

+ 7 - 4
server/apps/microservices/src/processors/video-transcode.processor.ts

@@ -1,3 +1,6 @@
+import { mp4ConversionProcessorName } from '@app/job/constants/job-name.constant';
+import { videoConversionQueueName } from '@app/job/constants/queue-name.constant';
+import { IMp4ConversionProcessor } from '@app/job/interfaces/video-transcode.interface';
 import { Process, Processor } from '@nestjs/bull';
 import { Process, Processor } from '@nestjs/bull';
 import { Logger } from '@nestjs/common';
 import { Logger } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { InjectRepository } from '@nestjs/typeorm';
@@ -8,16 +11,16 @@ import { Repository } from 'typeorm';
 import { AssetEntity } from '../../../../libs/database/src/entities/asset.entity';
 import { AssetEntity } from '../../../../libs/database/src/entities/asset.entity';
 import { APP_UPLOAD_LOCATION } from '../../../immich/src/constants/upload_location.constant';
 import { APP_UPLOAD_LOCATION } from '../../../immich/src/constants/upload_location.constant';
 
 
-@Processor('video-conversion-queue')
+@Processor(videoConversionQueueName)
 export class VideoTranscodeProcessor {
 export class VideoTranscodeProcessor {
   constructor(
   constructor(
     @InjectRepository(AssetEntity)
     @InjectRepository(AssetEntity)
     private assetRepository: Repository<AssetEntity>,
     private assetRepository: Repository<AssetEntity>,
   ) {}
   ) {}
 
 
-  @Process({ name: 'mp4-conversion', concurrency: 1 })
-  async mp4Conversion(job: Job) {
-    const { asset }: { asset: AssetEntity } = job.data;
+  @Process({ name: mp4ConversionProcessorName, concurrency: 1 })
+  async mp4Conversion(job: Job<IMp4ConversionProcessor>) {
+    const { asset } = job.data;
 
 
     if (asset.mimeType != 'video/mp4') {
     if (asset.mimeType != 'video/mp4') {
       const basePath = APP_UPLOAD_LOCATION;
       const basePath = APP_UPLOAD_LOCATION;

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

@@ -0,0 +1,23 @@
+/**
+ * Asset Uploaded Queue Jobs
+ */
+export const assetUploadedProcessorName = 'asset-uploaded';
+
+/**
+ *  Video Conversion Queue Jobs
+ **/
+export const mp4ConversionProcessorName = 'mp4-conversion';
+
+/**
+ * Thumbnail Generator Queue Jobs
+ */
+export const generateJPEGThumbnailProcessorName = 'generate-jpeg-thumbnail';
+export const generateWEBPThumbnailProcessorName = 'generate-webp-thumbnail';
+
+/**
+ * Metadata Extraction Queue Jobs
+ */
+export const exifExtractionProcessorName = 'exif-extraction';
+export const videoLengthExtractionProcessorName = 'extract-video-length';
+export const objectDetectionProcessorName = 'detect-object';
+export const imageTaggingProcessorName = 'tag-image';

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

@@ -0,0 +1,4 @@
+export const thumbnailGeneratorQueueName = 'thumbnail-generator-queue';
+export const assetUploadedQueueName = 'asset-uploaded-queue';
+export const metadataExtractionQueueName = 'metadata-extraction-queue';
+export const videoConversionQueueName = 'video-conversion-queue';

+ 7 - 0
server/libs/job/src/index.ts

@@ -0,0 +1,7 @@
+export * from './interfaces/asset-uploaded.interface';
+export * from './interfaces/metadata-extraction.interface';
+export * from './interfaces/video-transcode.interface';
+export * from './interfaces/thumbnail-generation.interface';
+
+export * from './constants/job-name.constant';
+export * from './constants/queue-name.constant';

+ 18 - 0
server/libs/job/src/interfaces/asset-uploaded.interface.ts

@@ -0,0 +1,18 @@
+import { AssetEntity } from '@app/database/entities/asset.entity';
+
+export interface IAssetUploadedJob {
+  /**
+   * The Asset entity that was saved in the database
+   */
+  asset: AssetEntity;
+
+  /**
+   * Original file name
+   */
+  fileName: string;
+
+  /**
+   * File size in byte
+   */
+  fileSize: number;
+}

+ 27 - 0
server/libs/job/src/interfaces/metadata-extraction.interface.ts

@@ -0,0 +1,27 @@
+import { AssetEntity } from '@app/database/entities/asset.entity';
+
+export interface IExifExtractionProcessor {
+  /**
+   * The Asset entity that was saved in the database
+   */
+  asset: AssetEntity;
+
+  /**
+   * Original file name
+   */
+  fileName: string;
+
+  /**
+   * File size in byte
+   */
+  fileSize: number;
+}
+
+export interface IVideoLengthExtractionProcessor {
+  /**
+   * The Asset entity that was saved in the database
+   */
+  asset: AssetEntity;
+}
+
+export type IMetadataExtractionJob = IExifExtractionProcessor | IVideoLengthExtractionProcessor;

+ 17 - 0
server/libs/job/src/interfaces/thumbnail-generation.interface.ts

@@ -0,0 +1,17 @@
+import { AssetEntity } from '@app/database/entities/asset.entity';
+
+export interface JpegGeneratorProcessor {
+  /**
+   * The Asset entity that was saved in the database
+   */
+  asset: AssetEntity;
+}
+
+export interface WebpGeneratorProcessor {
+  /**
+   * The Asset entity that was saved in the database
+   */
+  asset: AssetEntity;
+}
+
+export type IThumbnailGenerationJob = JpegGeneratorProcessor | WebpGeneratorProcessor;

+ 10 - 0
server/libs/job/src/interfaces/video-transcode.interface.ts

@@ -0,0 +1,10 @@
+import { AssetEntity } from '@app/database/entities/asset.entity';
+
+export interface IMp4ConversionProcessor {
+  /**
+   * The Asset entity that was saved in the database
+   */
+  asset: AssetEntity;
+}
+
+export type IVideoTranscodeJob = IMp4ConversionProcessor;

+ 9 - 0
server/libs/job/tsconfig.lib.json

@@ -0,0 +1,9 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "declaration": true,
+    "outDir": "../../dist/libs/job"
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
+}

+ 10 - 1
server/nest-cli.json

@@ -34,6 +34,15 @@
       "compilerOptions": {
       "compilerOptions": {
         "tsConfigPath": "libs/database/tsconfig.lib.json"
         "tsConfigPath": "libs/database/tsconfig.lib.json"
       }
       }
+    },
+    "job": {
+      "type": "library",
+      "root": "libs/job",
+      "entryFile": "index",
+      "sourceRoot": "libs/job/src",
+      "compilerOptions": {
+        "tsConfigPath": "libs/job/tsconfig.lib.json"
+      }
     }
     }
   }
   }
-}
+}

+ 3 - 2
server/package.json

@@ -120,7 +120,8 @@
     "moduleNameMapper": {
     "moduleNameMapper": {
       "^@app/database(|/.*)$": "<rootDir>/libs/database/src/$1",
       "^@app/database(|/.*)$": "<rootDir>/libs/database/src/$1",
       "@app/database/config/(.*)": "<rootDir>/libs/database/src/config/$1",
       "@app/database/config/(.*)": "<rootDir>/libs/database/src/config/$1",
-      "@app/database/config": "<rootDir>/libs/database/src/config"
+      "@app/database/config": "<rootDir>/libs/database/src/config",
+      "^@app/job(|/.*)$": "<rootDir>/libs/job/src/$1"
     }
     }
   }
   }
-}
+}

+ 6 - 0
server/tsconfig.json

@@ -21,6 +21,12 @@
       ],
       ],
       "@app/database/*": [
       "@app/database/*": [
         "libs/database/src/*"
         "libs/database/src/*"
+      ],
+      "@app/job": [
+        "libs/job/src"
+      ],
+      "@app/job/*": [
+        "libs/job/src/*"
       ]
       ]
     }
     }
   },
   },