mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05:00 
			
		
		
		
	refactor(server): move asset upload job to domain (#1434)
* refactor: move to domain * refactor: rename method * Update comments --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									5aee5c0fb8
								
							
						
					
					
						commit
						42a3149fe3
					
				@ -1,50 +1,13 @@
 | 
				
			|||||||
import { AssetType } from '@app/infra';
 | 
					import { IAssetUploadedJob, JobName, JobService, QueueName } from '@app/domain';
 | 
				
			||||||
import {
 | 
					import { Process, Processor } from '@nestjs/bull';
 | 
				
			||||||
  IAssetUploadedJob,
 | 
					import { Job } from 'bull';
 | 
				
			||||||
  IMetadataExtractionJob,
 | 
					 | 
				
			||||||
  IThumbnailGenerationJob,
 | 
					 | 
				
			||||||
  IVideoTranscodeJob,
 | 
					 | 
				
			||||||
  QueueName,
 | 
					 | 
				
			||||||
  JobName,
 | 
					 | 
				
			||||||
} from '@app/domain';
 | 
					 | 
				
			||||||
import { InjectQueue, Process, Processor } from '@nestjs/bull';
 | 
					 | 
				
			||||||
import { Job, Queue } from 'bull';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Processor(QueueName.ASSET_UPLOADED)
 | 
					@Processor(QueueName.ASSET_UPLOADED)
 | 
				
			||||||
export class AssetUploadedProcessor {
 | 
					export class AssetUploadedProcessor {
 | 
				
			||||||
  constructor(
 | 
					  constructor(private jobService: JobService) {}
 | 
				
			||||||
    @InjectQueue(QueueName.THUMBNAIL_GENERATION)
 | 
					 | 
				
			||||||
    private thumbnailGeneratorQueue: Queue<IThumbnailGenerationJob>,
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @InjectQueue(QueueName.METADATA_EXTRACTION)
 | 
					 | 
				
			||||||
    private metadataExtractionQueue: Queue<IMetadataExtractionJob>,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @InjectQueue(QueueName.VIDEO_CONVERSION)
 | 
					 | 
				
			||||||
    private videoConversionQueue: Queue<IVideoTranscodeJob>,
 | 
					 | 
				
			||||||
  ) {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Post processing uploaded asset to perform the following function if missing
 | 
					 | 
				
			||||||
   * 1. Generate JPEG Thumbnail
 | 
					 | 
				
			||||||
   * 2. Generate Webp Thumbnail
 | 
					 | 
				
			||||||
   * 3. EXIF extractor
 | 
					 | 
				
			||||||
   * 4. Reverse Geocoding
 | 
					 | 
				
			||||||
   *
 | 
					 | 
				
			||||||
   * @param job asset-uploaded
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  @Process(JobName.ASSET_UPLOADED)
 | 
					  @Process(JobName.ASSET_UPLOADED)
 | 
				
			||||||
  async processUploadedVideo(job: Job<IAssetUploadedJob>) {
 | 
					  async processUploadedVideo(job: Job<IAssetUploadedJob>) {
 | 
				
			||||||
    const { asset, fileName } = job.data;
 | 
					    await this.jobService.handleUploadedAsset(job);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    await this.thumbnailGeneratorQueue.add(JobName.GENERATE_JPEG_THUMBNAIL, { asset });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Video Conversion
 | 
					 | 
				
			||||||
    if (asset.type == AssetType.VIDEO) {
 | 
					 | 
				
			||||||
      await this.videoConversionQueue.add(JobName.VIDEO_CONVERSION, { asset });
 | 
					 | 
				
			||||||
      await this.metadataExtractionQueue.add(JobName.EXTRACT_VIDEO_METADATA, { asset, fileName });
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      // Extract Metadata/Exif for Images - Currently the EXIF library on the web cannot extract EXIF for video yet
 | 
					 | 
				
			||||||
      await this.metadataExtractionQueue.add(JobName.EXIF_EXTRACTION, { asset, fileName });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,16 @@
 | 
				
			|||||||
import { DynamicModule, Global, Module, ModuleMetadata, Provider } from '@nestjs/common';
 | 
					import { DynamicModule, Global, Module, ModuleMetadata, Provider } from '@nestjs/common';
 | 
				
			||||||
import { APIKeyService } from './api-key';
 | 
					import { APIKeyService } from './api-key';
 | 
				
			||||||
import { ShareService } from './share';
 | 
					 | 
				
			||||||
import { AuthService } from './auth';
 | 
					import { AuthService } from './auth';
 | 
				
			||||||
 | 
					import { JobService } from './job';
 | 
				
			||||||
import { OAuthService } from './oauth';
 | 
					import { OAuthService } from './oauth';
 | 
				
			||||||
 | 
					import { ShareService } from './share';
 | 
				
			||||||
import { INITIAL_SYSTEM_CONFIG, SystemConfigService } from './system-config';
 | 
					import { INITIAL_SYSTEM_CONFIG, SystemConfigService } from './system-config';
 | 
				
			||||||
import { UserService } from './user';
 | 
					import { UserService } from './user';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const providers: Provider[] = [
 | 
					const providers: Provider[] = [
 | 
				
			||||||
  APIKeyService,
 | 
					  APIKeyService,
 | 
				
			||||||
  AuthService,
 | 
					  AuthService,
 | 
				
			||||||
 | 
					  JobService,
 | 
				
			||||||
  OAuthService,
 | 
					  OAuthService,
 | 
				
			||||||
  SystemConfigService,
 | 
					  SystemConfigService,
 | 
				
			||||||
  UserService,
 | 
					  UserService,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
export * from './interfaces';
 | 
					export * from './interfaces';
 | 
				
			||||||
export * from './job.constants';
 | 
					export * from './job.constants';
 | 
				
			||||||
export * from './job.repository';
 | 
					export * from './job.repository';
 | 
				
			||||||
 | 
					export * from './job.service';
 | 
				
			||||||
 | 
				
			|||||||
@ -20,6 +20,10 @@ export interface JobCounts {
 | 
				
			|||||||
  waiting: number;
 | 
					  waiting: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface Job<T> {
 | 
				
			||||||
 | 
					  data: T;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type JobItem =
 | 
					export type JobItem =
 | 
				
			||||||
  | { name: JobName.ASSET_UPLOADED; data: IAssetUploadedJob }
 | 
					  | { name: JobName.ASSET_UPLOADED; data: IAssetUploadedJob }
 | 
				
			||||||
  | { name: JobName.VIDEO_CONVERSION; data: IVideoConversionProcessor }
 | 
					  | { name: JobName.VIDEO_CONVERSION; data: IVideoConversionProcessor }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										54
									
								
								server/libs/domain/src/job/job.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								server/libs/domain/src/job/job.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					import { AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
				
			||||||
 | 
					import { newJobRepositoryMock } from '../../test';
 | 
				
			||||||
 | 
					import { IAssetUploadedJob } from './interfaces';
 | 
				
			||||||
 | 
					import { JobName } from './job.constants';
 | 
				
			||||||
 | 
					import { IJobRepository, Job } from './job.repository';
 | 
				
			||||||
 | 
					import { JobService } from './job.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const jobStub = {
 | 
				
			||||||
 | 
					  upload: {
 | 
				
			||||||
 | 
					    video: Object.freeze<Job<IAssetUploadedJob>>({
 | 
				
			||||||
 | 
					      data: { asset: { type: AssetType.VIDEO } as AssetEntity, fileName: 'video.mp4' },
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    image: Object.freeze<Job<IAssetUploadedJob>>({
 | 
				
			||||||
 | 
					      data: { asset: { type: AssetType.IMAGE } as AssetEntity, fileName: 'image.jpg' },
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe(JobService.name, () => {
 | 
				
			||||||
 | 
					  let sut: JobService;
 | 
				
			||||||
 | 
					  let jobMock: jest.Mocked<IJobRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should work', () => {
 | 
				
			||||||
 | 
					    expect(sut).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
 | 
					    sut = new JobService(jobMock);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('handleUploadedAsset', () => {
 | 
				
			||||||
 | 
					    it('should process a video', async () => {
 | 
				
			||||||
 | 
					      await expect(sut.handleUploadedAsset(jobStub.upload.video)).resolves.toBeUndefined();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(jobMock.add).toHaveBeenCalledTimes(3);
 | 
				
			||||||
 | 
					      expect(jobMock.add.mock.calls).toEqual([
 | 
				
			||||||
 | 
					        [{ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { asset: { type: AssetType.VIDEO } } }],
 | 
				
			||||||
 | 
					        [{ name: JobName.VIDEO_CONVERSION, data: { asset: { type: AssetType.VIDEO } } }],
 | 
				
			||||||
 | 
					        [{ name: JobName.EXTRACT_VIDEO_METADATA, data: { asset: { type: AssetType.VIDEO }, fileName: 'video.mp4' } }],
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should process an image', async () => {
 | 
				
			||||||
 | 
					      await sut.handleUploadedAsset(jobStub.upload.image);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(jobMock.add).toHaveBeenCalledTimes(2);
 | 
				
			||||||
 | 
					      expect(jobMock.add.mock.calls).toEqual([
 | 
				
			||||||
 | 
					        [{ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { asset: { type: AssetType.IMAGE } } }],
 | 
				
			||||||
 | 
					        [{ name: JobName.EXIF_EXTRACTION, data: { asset: { type: AssetType.IMAGE }, fileName: 'image.jpg' } }],
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										17
									
								
								server/libs/domain/src/job/job.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								server/libs/domain/src/job/job.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { IAssetUploadedJob } from './interfaces';
 | 
				
			||||||
 | 
					import { JobUploadCore } from './job.upload.core';
 | 
				
			||||||
 | 
					import { IJobRepository, Job } from './job.repository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class JobService {
 | 
				
			||||||
 | 
					  private uploadCore: JobUploadCore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(@Inject(IJobRepository) repository: IJobRepository) {
 | 
				
			||||||
 | 
					    this.uploadCore = new JobUploadCore(repository);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handleUploadedAsset(job: Job<IAssetUploadedJob>) {
 | 
				
			||||||
 | 
					    await this.uploadCore.handleAsset(job);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								server/libs/domain/src/job/job.upload.core.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								server/libs/domain/src/job/job.upload.core.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import { AssetType } from '@app/infra/db/entities';
 | 
				
			||||||
 | 
					import { IAssetUploadedJob } from './interfaces';
 | 
				
			||||||
 | 
					import { JobName } from './job.constants';
 | 
				
			||||||
 | 
					import { IJobRepository, Job } from './job.repository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class JobUploadCore {
 | 
				
			||||||
 | 
					  constructor(private repository: IJobRepository) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Post processing uploaded asset to perform the following function
 | 
				
			||||||
 | 
					   * 1. Generate JPEG Thumbnail
 | 
				
			||||||
 | 
					   * 2. Generate Webp Thumbnail
 | 
				
			||||||
 | 
					   * 3. EXIF extractor
 | 
				
			||||||
 | 
					   * 4. Reverse Geocoding
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param job asset-uploaded
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  async handleAsset(job: Job<IAssetUploadedJob>) {
 | 
				
			||||||
 | 
					    const { asset, fileName } = job.data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.repository.add({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { asset } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Video Conversion
 | 
				
			||||||
 | 
					    if (asset.type == AssetType.VIDEO) {
 | 
				
			||||||
 | 
					      await this.repository.add({ name: JobName.VIDEO_CONVERSION, data: { asset } });
 | 
				
			||||||
 | 
					      await this.repository.add({ name: JobName.EXTRACT_VIDEO_METADATA, data: { asset, fileName } });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Extract Metadata/Exif for Images - Currently the EXIF library on the web cannot extract EXIF for video yet
 | 
				
			||||||
 | 
					      await this.repository.add({ name: JobName.EXIF_EXTRACTION, data: { asset, fileName } });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user