thumbnail.processor.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import { APP_UPLOAD_LOCATION } from '@app/common';
  2. import { ImmichLogLevel } from '@app/common/constants/log-level.constant';
  3. import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
  4. import {
  5. WebpGeneratorProcessor,
  6. generateJPEGThumbnailProcessorName,
  7. generateWEBPThumbnailProcessorName,
  8. JpegGeneratorProcessor,
  9. QueueNameEnum,
  10. MachineLearningJobNameEnum,
  11. } from '@app/job';
  12. import { InjectQueue, Process, Processor } from '@nestjs/bull';
  13. import { Logger } from '@nestjs/common';
  14. import { ConfigService } from '@nestjs/config';
  15. import { InjectRepository } from '@nestjs/typeorm';
  16. import { mapAsset } from 'apps/immich/src/api-v1/asset/response-dto/asset-response.dto';
  17. import { Job, Queue } from 'bull';
  18. import ffmpeg from 'fluent-ffmpeg';
  19. import { randomUUID } from 'node:crypto';
  20. import { existsSync, mkdirSync } from 'node:fs';
  21. import sanitize from 'sanitize-filename';
  22. import sharp from 'sharp';
  23. import { Repository } from 'typeorm/repository/Repository';
  24. import { join } from 'path';
  25. import { CommunicationGateway } from 'apps/immich/src/api-v1/communication/communication.gateway';
  26. import { IMachineLearningJob } from '@app/job/interfaces/machine-learning.interface';
  27. @Processor(QueueNameEnum.THUMBNAIL_GENERATION)
  28. export class ThumbnailGeneratorProcessor {
  29. private logLevel: ImmichLogLevel;
  30. constructor(
  31. @InjectRepository(AssetEntity)
  32. private assetRepository: Repository<AssetEntity>,
  33. @InjectQueue(QueueNameEnum.THUMBNAIL_GENERATION)
  34. private thumbnailGeneratorQueue: Queue,
  35. private wsCommunicationGateway: CommunicationGateway,
  36. @InjectQueue(QueueNameEnum.MACHINE_LEARNING)
  37. private machineLearningQueue: Queue<IMachineLearningJob>,
  38. private configService: ConfigService,
  39. ) {
  40. this.logLevel = this.configService.get('LOG_LEVEL') || ImmichLogLevel.SIMPLE;
  41. }
  42. @Process({ name: generateJPEGThumbnailProcessorName, concurrency: 3 })
  43. async generateJPEGThumbnail(job: Job<JpegGeneratorProcessor>) {
  44. const basePath = APP_UPLOAD_LOCATION;
  45. const { asset } = job.data;
  46. const sanitizedDeviceId = sanitize(String(asset.deviceId));
  47. const resizePath = join(basePath, asset.userId, 'thumb', sanitizedDeviceId);
  48. if (!existsSync(resizePath)) {
  49. mkdirSync(resizePath, { recursive: true });
  50. }
  51. const temp = asset.originalPath.split('/');
  52. const originalFilename = temp[temp.length - 1].split('.')[0];
  53. const jpegThumbnailPath = join(resizePath, `${originalFilename}.jpeg`);
  54. if (asset.type == AssetType.IMAGE) {
  55. try {
  56. await sharp(asset.originalPath, { failOnError: false })
  57. .resize(1440, 2560, { fit: 'inside' })
  58. .jpeg()
  59. .rotate()
  60. .toFile(jpegThumbnailPath);
  61. await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
  62. } catch (error) {
  63. Logger.error('Failed to generate jpeg thumbnail for asset: ' + asset.id);
  64. if (this.logLevel == ImmichLogLevel.VERBOSE) {
  65. console.trace('Failed to generate jpeg thumbnail for asset', error);
  66. }
  67. }
  68. // Update resize path to send to generate webp queue
  69. asset.resizePath = jpegThumbnailPath;
  70. await this.thumbnailGeneratorQueue.add(generateWEBPThumbnailProcessorName, { asset }, { jobId: randomUUID() });
  71. await this.machineLearningQueue.add(MachineLearningJobNameEnum.IMAGE_TAGGING, { asset }, { jobId: randomUUID() });
  72. await this.machineLearningQueue.add(
  73. MachineLearningJobNameEnum.OBJECT_DETECTION,
  74. { asset },
  75. { jobId: randomUUID() },
  76. );
  77. this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
  78. }
  79. if (asset.type == AssetType.VIDEO) {
  80. await new Promise((resolve, reject) => {
  81. ffmpeg(asset.originalPath)
  82. .outputOptions(['-ss 00:00:00.000', '-frames:v 1'])
  83. .output(jpegThumbnailPath)
  84. .on('start', () => {
  85. Logger.log('Start Generating Video Thumbnail', 'generateJPEGThumbnail');
  86. })
  87. .on('error', (error) => {
  88. Logger.error(`Cannot Generate Video Thumbnail ${error}`, 'generateJPEGThumbnail');
  89. reject(error);
  90. })
  91. .on('end', async () => {
  92. Logger.log(`Generating Video Thumbnail Success ${asset.id}`, 'generateJPEGThumbnail');
  93. resolve(asset);
  94. })
  95. .run();
  96. });
  97. await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
  98. // Update resize path to send to generate webp queue
  99. asset.resizePath = jpegThumbnailPath;
  100. await this.thumbnailGeneratorQueue.add(generateWEBPThumbnailProcessorName, { asset }, { jobId: randomUUID() });
  101. await this.machineLearningQueue.add(MachineLearningJobNameEnum.IMAGE_TAGGING, { asset }, { jobId: randomUUID() });
  102. await this.machineLearningQueue.add(
  103. MachineLearningJobNameEnum.OBJECT_DETECTION,
  104. { asset },
  105. { jobId: randomUUID() },
  106. );
  107. this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
  108. }
  109. }
  110. @Process({ name: generateWEBPThumbnailProcessorName, concurrency: 3 })
  111. async generateWepbThumbnail(job: Job<WebpGeneratorProcessor>) {
  112. const { asset } = job.data;
  113. if (!asset.resizePath) {
  114. return;
  115. }
  116. const webpPath = asset.resizePath.replace('jpeg', 'webp');
  117. try {
  118. await sharp(asset.resizePath, { failOnError: false }).resize(250).webp().rotate().toFile(webpPath);
  119. await this.assetRepository.update({ id: asset.id }, { webpPath: webpPath });
  120. } catch (error) {
  121. Logger.error('Failed to generate webp thumbnail for asset: ' + asset.id);
  122. if (this.logLevel == ImmichLogLevel.VERBOSE) {
  123. console.trace('Failed to generate webp thumbnail for asset', error);
  124. }
  125. }
  126. }
  127. }