From 76bf1c0379088899d59a843511d1840665d75fba Mon Sep 17 00:00:00 2001 From: Alex 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, ) {} @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, - @InjectQueue('thumbnail-generator-queue') + @InjectQueue(thumbnailGeneratorQueueName) private thumbnailGeneratorQueue: Queue, - @InjectQueue('video-conversion-queue') - private videoConversionQueue: Queue, + @InjectQueue(videoConversionQueueName) + private videoConversionQueue: Queue, ) {} @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, - @InjectQueue('metadata-extraction-queue') - private metadataExtractionQueue: Queue, + @InjectQueue(metadataExtractionQueueName) + private metadataExtractionQueue: Queue, - @InjectQueue('video-conversion-queue') - private videoConversionQueue: Queue, - - @InjectRepository(AssetEntity) - private assetRepository: Repository, + @InjectQueue(videoConversionQueueName) + private videoConversionQueue: Queue, ) {} /** * 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) { + 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) { 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) { + 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, - @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) { + 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) { 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, ) {} - @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) { + 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(|/.*)$": "/libs/database/src/$1", "@app/database/config/(.*)": "/libs/database/src/config/$1", - "@app/database/config": "/libs/database/src/config" + "@app/database/config": "/libs/database/src/config", + "^@app/job(|/.*)$": "/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/*" ] } },