mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04: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