From 76bf1c0379088899d59a843511d1840665d75fba Mon Sep 17 00:00:00 2001
From: Alex <alex.tran1502@gmail.com>
Date: Sat, 2 Jul 2022 21:06:36 -0500
Subject: [PATCH] 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
---
 .../backup/services/backup.service.dart       | 18 -----
 .../src/api-v1/asset/asset.controller.ts      | 35 +++-------
 .../immich/src/api-v1/asset/asset.module.ts   |  3 +-
 .../immich/src/config/asset-upload.config.ts  | 31 +++------
 .../schedule-tasks/schedule-tasks.module.ts   |  5 +-
 .../schedule-tasks/schedule-tasks.service.ts  | 17 +++--
 .../microservices/src/microservices.module.ts | 14 ++--
 .../processors/asset-uploaded.processor.ts    | 67 +++++++++----------
 .../metadata-extraction.processor.ts          | 25 ++++---
 .../src/processors/thumbnail.processor.ts     | 48 +++++++++----
 .../processors/video-transcode.processor.ts   | 11 +--
 .../job/src/constants/job-name.constant.ts    | 23 +++++++
 .../job/src/constants/queue-name.constant.ts  |  4 ++
 server/libs/job/src/index.ts                  |  7 ++
 .../interfaces/asset-uploaded.interface.ts    | 18 +++++
 .../metadata-extraction.interface.ts          | 27 ++++++++
 .../thumbnail-generation.interface.ts         | 17 +++++
 .../interfaces/video-transcode.interface.ts   | 10 +++
 server/libs/job/tsconfig.lib.json             |  9 +++
 server/nest-cli.json                          | 11 ++-
 server/package.json                           |  5 +-
 server/tsconfig.json                          |  6 ++
 22 files changed, 270 insertions(+), 141 deletions(-)
 create mode 100644 server/libs/job/src/constants/job-name.constant.ts
 create mode 100644 server/libs/job/src/constants/queue-name.constant.ts
 create mode 100644 server/libs/job/src/index.ts
 create mode 100644 server/libs/job/src/interfaces/asset-uploaded.interface.ts
 create mode 100644 server/libs/job/src/interfaces/metadata-extraction.interface.ts
 create mode 100644 server/libs/job/src/interfaces/thumbnail-generation.interface.ts
 create mode 100644 server/libs/job/src/interfaces/video-transcode.interface.ts
 create mode 100644 server/libs/job/tsconfig.lib.json

diff --git a/mobile/lib/modules/backup/services/backup.service.dart b/mobile/lib/modules/backup/services/backup.service.dart
index 3a0e223bb5..61390a3948 100644
--- a/mobile/lib/modules/backup/services/backup.service.dart
+++ b/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 req = MultipartRequest(
@@ -101,9 +86,6 @@ class BackupService {
           req.fields['fileExtension'] = fileExtension;
           req.fields['duration'] = entity.videoDuration.toString();
 
-          if (thumbnailUploadData != null) {
-            req.files.add(thumbnailUploadData);
-          }
           req.files.add(assetRawUploadData);
 
           var res = await req.send(cancellationToken: cancelToken);
diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts
index b1e6e0a332..d7e73aa3b9 100644
--- a/server/apps/immich/src/api-v1/asset/asset.controller.ts
+++ b/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 { InjectQueue } from '@nestjs/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)
 @Controller('asset')
@@ -40,8 +43,8 @@ export class AssetController {
     private assetService: AssetService,
     private backgroundTaskService: BackgroundTaskService,
 
-    @InjectQueue('asset-uploaded-queue')
-    private assetUploadedQueue: Queue,
+    @InjectQueue(assetUploadedQueueName)
+    private assetUploadedQueue: Queue<IAssetUploadedJob>,
   ) {}
 
   @Post('upload')
@@ -56,7 +59,7 @@ export class AssetController {
   )
   async uploadFile(
     @GetAuthUser() authUser: AuthUserDto,
-    @UploadedFiles() uploadFiles: { assetData: Express.Multer.File[]; thumbnailData?: Express.Multer.File[] },
+    @UploadedFiles() uploadFiles: { assetData: Express.Multer.File[] },
     @Body(ValidationPipe) assetInfo: CreateAssetDto,
   ): Promise<'ok' | undefined> {
     for (const file of uploadFiles.assetData) {
@@ -66,28 +69,12 @@ export class AssetController {
         if (!savedAsset) {
           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) {
         Logger.error(`Error receiving upload file ${e}`);
       }
diff --git a/server/apps/immich/src/api-v1/asset/asset.module.ts b/server/apps/immich/src/api-v1/asset/asset.module.ts
index b8ef03b08d..a09831391f 100644
--- a/server/apps/immich/src/api-v1/asset/asset.module.ts
+++ b/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 { BackgroundTaskService } from '../../modules/background-task/background-task.service';
 import { CommunicationModule } from '../communication/communication.module';
+import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
 
 @Module({
   imports: [
@@ -14,7 +15,7 @@ import { CommunicationModule } from '../communication/communication.module';
     BackgroundTaskModule,
     TypeOrmModule.forFeature([AssetEntity]),
     BullModule.registerQueue({
-      name: 'asset-uploaded-queue',
+      name: assetUploadedQueueName,
       defaultJobOptions: {
         attempts: 3,
         removeOnComplete: true,
diff --git a/server/apps/immich/src/config/asset-upload.config.ts b/server/apps/immich/src/config/asset-upload.config.ts
index cbed1b3576..fcdba32459 100644
--- a/server/apps/immich/src/config/asset-upload.config.ts
+++ b/server/apps/immich/src/config/asset-upload.config.ts
@@ -6,7 +6,6 @@ import { extname } from 'path';
 import { Request } from 'express';
 import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant';
 import { randomUUID } from 'crypto';
-// import { CreateAssetDto } from '../api-v1/asset/dto/create-asset.dto';
 
 export const assetUploadOption: MulterOptions = {
   fileFilter: (req: Request, file: any, cb: any) => {
@@ -30,34 +29,20 @@ export const assetUploadOption: MulterOptions = {
         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) => {
       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()}`);
     },
   }),
 };
diff --git a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts
index bdf63482bd..2f69a741ef 100644
--- a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts
+++ b/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 { AssetEntity } from '@app/database/entities/asset.entity';
 import { ScheduleTasksService } from './schedule-tasks.service';
+import { thumbnailGeneratorQueueName, videoConversionQueueName } from '@app/job/constants/queue-name.constant';
 
 @Module({
   imports: [
     TypeOrmModule.forFeature([AssetEntity]),
     BullModule.registerQueue({
-      name: 'video-conversion-queue',
+      name: videoConversionQueueName,
       defaultJobOptions: {
         attempts: 3,
         removeOnComplete: true,
@@ -16,7 +17,7 @@ import { ScheduleTasksService } from './schedule-tasks.service';
       },
     }),
     BullModule.registerQueue({
-      name: 'thumbnail-generator-queue',
+      name: thumbnailGeneratorQueueName,
       defaultJobOptions: {
         attempts: 3,
         removeOnComplete: true,
diff --git a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts
index edf8bd5797..f09a3a72bd 100644
--- a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts
+++ b/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 { 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';
 
 @Injectable()
 export class ScheduleTasksService {
@@ -13,11 +16,11 @@ export class ScheduleTasksService {
     @InjectRepository(AssetEntity)
     private assetRepository: Repository<AssetEntity>,
 
-    @InjectQueue('thumbnail-generator-queue')
+    @InjectQueue(thumbnailGeneratorQueueName)
     private thumbnailGeneratorQueue: Queue,
 
-    @InjectQueue('video-conversion-queue')
-    private videoConversionQueue: Queue,
+    @InjectQueue(videoConversionQueueName)
+    private videoConversionQueue: Queue<IVideoTranscodeJob>,
   ) {}
 
   @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
@@ -36,7 +39,11 @@ export class ScheduleTasksService {
     }
 
     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) {
-      await this.videoConversionQueue.add('mp4-conversion', { asset }, { jobId: randomUUID() });
+      await this.videoConversionQueue.add(mp4ConversionProcessorName, { asset }, { jobId: randomUUID() });
     }
   }
 }
diff --git a/server/apps/microservices/src/microservices.module.ts b/server/apps/microservices/src/microservices.module.ts
index ffa0f6c03e..2eb76dd203 100644
--- a/server/apps/microservices/src/microservices.module.ts
+++ b/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 { VideoTranscodeProcessor } from './processors/video-transcode.processor';
 import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module';
+import {
+  assetUploadedQueueName,
+  metadataExtractionQueueName,
+  thumbnailGeneratorQueueName,
+  videoConversionQueueName,
+} from '@app/job/constants/queue-name.constant';
 
 @Module({
   imports: [
@@ -26,7 +32,7 @@ import { CommunicationModule } from '../../immich/src/api-v1/communication/commu
       }),
     }),
     BullModule.registerQueue({
-      name: 'thumbnail-generator-queue',
+      name: thumbnailGeneratorQueueName,
       defaultJobOptions: {
         attempts: 3,
         removeOnComplete: true,
@@ -34,7 +40,7 @@ import { CommunicationModule } from '../../immich/src/api-v1/communication/commu
       },
     }),
     BullModule.registerQueue({
-      name: 'asset-uploaded-queue',
+      name: assetUploadedQueueName,
       defaultJobOptions: {
         attempts: 3,
         removeOnComplete: true,
@@ -42,7 +48,7 @@ import { CommunicationModule } from '../../immich/src/api-v1/communication/commu
       },
     }),
     BullModule.registerQueue({
-      name: 'metadata-extraction-queue',
+      name: metadataExtractionQueueName,
       defaultJobOptions: {
         attempts: 3,
         removeOnComplete: true,
@@ -50,7 +56,7 @@ import { CommunicationModule } from '../../immich/src/api-v1/communication/commu
       },
     }),
     BullModule.registerQueue({
-      name: 'video-conversion-queue',
+      name: videoConversionQueueName,
       defaultJobOptions: {
         attempts: 3,
         removeOnComplete: true,
diff --git a/server/apps/microservices/src/processors/asset-uploaded.processor.ts b/server/apps/microservices/src/processors/asset-uploaded.processor.ts
index 28ee924abb..7172c43e4d 100644
--- a/server/apps/microservices/src/processors/asset-uploaded.processor.ts
+++ b/server/apps/microservices/src/processors/asset-uploaded.processor.ts
@@ -1,61 +1,58 @@
 import { InjectQueue, Process, Processor } from '@nestjs/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 {
+  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 {
   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
    * 1. Generate JPEG Thumbnail
-   * 2. Generate Webp Thumbnail <-> if JPEG thumbnail exist
+   * 2. Generate Webp Thumbnail
    * 3. EXIF extractor
    * 4. Reverse Geocoding
    *
    * @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
     if (asset.type == AssetType.VIDEO) {
-      await this.videoConversionQueue.add('mp4-conversion', { asset }, { jobId: randomUUID() });
+      await this.videoConversionQueue.add(mp4ConversionProcessorName, { asset }, { jobId: randomUUID() });
     } 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(
-        'exif-extraction',
+        exifExtractionProcessorName,
         {
           asset,
           fileName,
@@ -67,7 +64,7 @@ export class AssetUploadedProcessor {
 
     // Extract video duration if uploaded from the web
     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() });
     }
   }
 }
diff --git a/server/apps/microservices/src/processors/metadata-extraction.processor.ts b/server/apps/microservices/src/processors/metadata-extraction.processor.ts
index 4d6a70186f..f03a8e1b0c 100644
--- a/server/apps/microservices/src/processors/metadata-extraction.processor.ts
+++ b/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 ffmpeg from 'fluent-ffmpeg';
 import path from 'path';
+import {
+  IExifExtractionProcessor,
+  IVideoLengthExtractionProcessor,
+  exifExtractionProcessorName,
+  imageTaggingProcessorName,
+  objectDetectionProcessorName,
+  videoLengthExtractionProcessorName,
+  metadataExtractionQueueName,
+} from '@app/job';
 
-@Processor('metadata-extraction-queue')
+@Processor(metadataExtractionQueueName)
 export class MetadataExtractionProcessor {
   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 {
       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) {
     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) {
     try {
       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) => {
       if (!err) {
diff --git a/server/apps/microservices/src/processors/thumbnail.processor.ts b/server/apps/microservices/src/processors/thumbnail.processor.ts
index 5dfc2cf9be..cf3aefa3c0 100644
--- a/server/apps/microservices/src/processors/thumbnail.processor.ts
+++ b/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 ffmpeg from 'fluent-ffmpeg';
 import { Logger } from '@nestjs/common';
+import {
+  WebpGeneratorProcessor,
+  generateJPEGThumbnailProcessorName,
+  generateWEBPThumbnailProcessorName,
+  imageTaggingProcessorName,
+  objectDetectionProcessorName,
+  metadataExtractionQueueName,
+  thumbnailGeneratorQueueName,
+  JpegGeneratorProcessor,
+} from '@app/job';
 
-@Processor('thumbnail-generator-queue')
+@Processor(thumbnailGeneratorQueueName)
 export class ThumbnailGeneratorProcessor {
   constructor(
     @InjectRepository(AssetEntity)
     private assetRepository: Repository<AssetEntity>,
 
-    @InjectQueue('thumbnail-generator-queue')
+    @InjectQueue(thumbnailGeneratorQueueName)
     private thumbnailGeneratorQueue: Queue,
 
     private wsCommunicateionGateway: CommunicationGateway,
 
-    @InjectQueue('metadata-extraction-queue')
+    @InjectQueue(metadataExtractionQueueName)
     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}/`;
 
@@ -43,6 +53,7 @@ export class ThumbnailGeneratorProcessor {
       sharp(asset.originalPath)
         .resize(1440, 2560, { fit: 'inside' })
         .jpeg()
+        .rotate()
         .toFile(jpegThumbnailPath, async (err) => {
           if (!err) {
             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
             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));
           }
         });
@@ -76,9 +91,13 @@ export class ThumbnailGeneratorProcessor {
           // Update resize path to send to generate webp queue
           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));
         })
@@ -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;
 
     if (!asset.resizePath) {
@@ -98,6 +117,7 @@ export class ThumbnailGeneratorProcessor {
     sharp(asset.resizePath)
       .resize(250)
       .webp()
+      .rotate()
       .toFile(webpPath, (err) => {
         if (!err) {
           this.assetRepository.update({ id: asset.id }, { webpPath: webpPath });
diff --git a/server/apps/microservices/src/processors/video-transcode.processor.ts b/server/apps/microservices/src/processors/video-transcode.processor.ts
index 984124a7db..d75628c1af 100644
--- a/server/apps/microservices/src/processors/video-transcode.processor.ts
+++ b/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 { Logger } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
@@ -8,16 +11,16 @@ import { Repository } from 'typeorm';
 import { AssetEntity } from '../../../../libs/database/src/entities/asset.entity';
 import { APP_UPLOAD_LOCATION } from '../../../immich/src/constants/upload_location.constant';
 
-@Processor('video-conversion-queue')
+@Processor(videoConversionQueueName)
 export class VideoTranscodeProcessor {
   constructor(
     @InjectRepository(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') {
       const basePath = APP_UPLOAD_LOCATION;
diff --git a/server/libs/job/src/constants/job-name.constant.ts b/server/libs/job/src/constants/job-name.constant.ts
new file mode 100644
index 0000000000..a58f97e5ef
--- /dev/null
+++ b/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';
diff --git a/server/libs/job/src/constants/queue-name.constant.ts b/server/libs/job/src/constants/queue-name.constant.ts
new file mode 100644
index 0000000000..504e9fca81
--- /dev/null
+++ b/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';
diff --git a/server/libs/job/src/index.ts b/server/libs/job/src/index.ts
new file mode 100644
index 0000000000..b3aa4e829a
--- /dev/null
+++ b/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';
diff --git a/server/libs/job/src/interfaces/asset-uploaded.interface.ts b/server/libs/job/src/interfaces/asset-uploaded.interface.ts
new file mode 100644
index 0000000000..ce54c0b433
--- /dev/null
+++ b/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;
+}
diff --git a/server/libs/job/src/interfaces/metadata-extraction.interface.ts b/server/libs/job/src/interfaces/metadata-extraction.interface.ts
new file mode 100644
index 0000000000..76209ca375
--- /dev/null
+++ b/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;
diff --git a/server/libs/job/src/interfaces/thumbnail-generation.interface.ts b/server/libs/job/src/interfaces/thumbnail-generation.interface.ts
new file mode 100644
index 0000000000..7ead7c5f18
--- /dev/null
+++ b/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;
diff --git a/server/libs/job/src/interfaces/video-transcode.interface.ts b/server/libs/job/src/interfaces/video-transcode.interface.ts
new file mode 100644
index 0000000000..0eee715dad
--- /dev/null
+++ b/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;
diff --git a/server/libs/job/tsconfig.lib.json b/server/libs/job/tsconfig.lib.json
new file mode 100644
index 0000000000..52dbebd526
--- /dev/null
+++ b/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"]
+}
diff --git a/server/nest-cli.json b/server/nest-cli.json
index d39f78a074..5af76aff68 100644
--- a/server/nest-cli.json
+++ b/server/nest-cli.json
@@ -34,6 +34,15 @@
       "compilerOptions": {
         "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"
+      }
     }
   }
-}
+}
\ No newline at end of file
diff --git a/server/package.json b/server/package.json
index c2dfde0103..9e96ce5f67 100644
--- a/server/package.json
+++ b/server/package.json
@@ -120,7 +120,8 @@
     "moduleNameMapper": {
       "^@app/database(|/.*)$": "<rootDir>/libs/database/src/$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"
     }
   }
-}
+}
\ No newline at end of file
diff --git a/server/tsconfig.json b/server/tsconfig.json
index 6f892ac09e..fae99fd5f1 100644
--- a/server/tsconfig.json
+++ b/server/tsconfig.json
@@ -21,6 +21,12 @@
       ],
       "@app/database/*": [
         "libs/database/src/*"
+      ],
+      "@app/job": [
+        "libs/job/src"
+      ],
+      "@app/job/*": [
+        "libs/job/src/*"
       ]
     }
   },