mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00: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:
		| @@ -1,50 +1,13 @@ | ||||
| import { AssetType } from '@app/infra'; | ||||
| import { | ||||
|   IAssetUploadedJob, | ||||
|   IMetadataExtractionJob, | ||||
|   IThumbnailGenerationJob, | ||||
|   IVideoTranscodeJob, | ||||
|   QueueName, | ||||
|   JobName, | ||||
| } from '@app/domain'; | ||||
| import { InjectQueue, Process, Processor } from '@nestjs/bull'; | ||||
| import { Job, Queue } from 'bull'; | ||||
| import { IAssetUploadedJob, JobName, JobService, QueueName } from '@app/domain'; | ||||
| import { Process, Processor } from '@nestjs/bull'; | ||||
| import { Job } from 'bull'; | ||||
|  | ||||
| @Processor(QueueName.ASSET_UPLOADED) | ||||
| export class AssetUploadedProcessor { | ||||
|   constructor( | ||||
|     @InjectQueue(QueueName.THUMBNAIL_GENERATION) | ||||
|     private thumbnailGeneratorQueue: Queue<IThumbnailGenerationJob>, | ||||
|   constructor(private jobService: JobService) {} | ||||
|  | ||||
|     @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) | ||||
|   async processUploadedVideo(job: Job<IAssetUploadedJob>) { | ||||
|     const { asset, fileName } = job.data; | ||||
|  | ||||
|     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 }); | ||||
|     } | ||||
|     await this.jobService.handleUploadedAsset(job); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,16 @@ | ||||
| import { DynamicModule, Global, Module, ModuleMetadata, Provider } from '@nestjs/common'; | ||||
| import { APIKeyService } from './api-key'; | ||||
| import { ShareService } from './share'; | ||||
| import { AuthService } from './auth'; | ||||
| import { JobService } from './job'; | ||||
| import { OAuthService } from './oauth'; | ||||
| import { ShareService } from './share'; | ||||
| import { INITIAL_SYSTEM_CONFIG, SystemConfigService } from './system-config'; | ||||
| import { UserService } from './user'; | ||||
|  | ||||
| const providers: Provider[] = [ | ||||
|   APIKeyService, | ||||
|   AuthService, | ||||
|   JobService, | ||||
|   OAuthService, | ||||
|   SystemConfigService, | ||||
|   UserService, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| export * from './interfaces'; | ||||
| export * from './job.constants'; | ||||
| export * from './job.repository'; | ||||
| export * from './job.service'; | ||||
|   | ||||
| @@ -20,6 +20,10 @@ export interface JobCounts { | ||||
|   waiting: number; | ||||
| } | ||||
|  | ||||
| export interface Job<T> { | ||||
|   data: T; | ||||
| } | ||||
|  | ||||
| export type JobItem = | ||||
|   | { name: JobName.ASSET_UPLOADED; data: IAssetUploadedJob } | ||||
|   | { 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 } }); | ||||
|     } | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user