refactor(server): jobs and processors (#1787)

* refactor: jobs and processors

* refactor: storage migration processor

* fix: tests

* fix: code warning

* chore: ignore coverage from infra

* fix: sync move asset logic between job core and asset core

* refactor: move error handling inside of catch

* refactor(server): job core into dedicated service calls

* refactor: smart info

* fix: tests

* chore: smart info tests

* refactor: use asset repository

* refactor: thumbnail processor

* chore: coverage reqs
This commit is contained in:
Jason Rasmussen
2023-02-25 09:12:03 -05:00
committed by GitHub
parent 71d8567f18
commit 6c7679714b
108 changed files with 1645 additions and 1072 deletions

View File

@@ -1,56 +1,30 @@
import { immichAppConfig } from '@app/common/config';
import {
AssetEntity,
ExifEntity,
SmartInfoEntity,
UserEntity,
APIKeyEntity,
InfraModule,
UserTokenEntity,
AlbumEntity,
} from '@app/infra';
import { StorageModule } from '@app/storage';
import { DomainModule } from '@app/domain';
import { ExifEntity, InfraModule } from '@app/infra';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module';
import { AssetUploadedProcessor } from './processors/asset-uploaded.processor';
import { MachineLearningProcessor } from './processors/machine-learning.processor';
import {
BackgroundTaskProcessor,
MachineLearningProcessor,
StorageTemplateMigrationProcessor,
ThumbnailGeneratorProcessor,
} from './processors';
import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor';
import { StorageMigrationProcessor } from './processors/storage-migration.processor';
import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor';
import { UserDeletionProcessor } from './processors/user-deletion.processor';
import { VideoTranscodeProcessor } from './processors/video-transcode.processor';
import { BackgroundTaskProcessor } from './processors/background-task.processor';
import { DomainModule } from '@app/domain';
@Module({
imports: [
ConfigModule.forRoot(immichAppConfig),
DomainModule.register({
imports: [InfraModule],
}),
TypeOrmModule.forFeature([
UserEntity,
ExifEntity,
AssetEntity,
SmartInfoEntity,
APIKeyEntity,
UserTokenEntity,
AlbumEntity,
]),
StorageModule,
CommunicationModule,
DomainModule.register({ imports: [InfraModule] }),
TypeOrmModule.forFeature([ExifEntity]),
],
controllers: [],
providers: [
AssetUploadedProcessor,
ThumbnailGeneratorProcessor,
MetadataExtractionProcessor,
VideoTranscodeProcessor,
MachineLearningProcessor,
UserDeletionProcessor,
StorageMigrationProcessor,
StorageTemplateMigrationProcessor,
BackgroundTaskProcessor,
],
})

View File

@@ -0,0 +1,87 @@
import {
AssetService,
IAssetJob,
IAssetUploadedJob,
IDeleteFilesJob,
IUserDeletionJob,
JobName,
MediaService,
QueueName,
SmartInfoService,
StorageService,
StorageTemplateService,
SystemConfigService,
UserService,
} from '@app/domain';
import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
@Processor(QueueName.BACKGROUND_TASK)
export class BackgroundTaskProcessor {
constructor(
private assetService: AssetService,
private storageService: StorageService,
private systemConfigService: SystemConfigService,
private userService: UserService,
) {}
@Process(JobName.ASSET_UPLOADED)
async onAssetUpload(job: Job<IAssetUploadedJob>) {
await this.assetService.handleAssetUpload(job.data);
}
@Process(JobName.DELETE_FILES)
async onDeleteFile(job: Job<IDeleteFilesJob>) {
await this.storageService.handleDeleteFiles(job.data);
}
@Process(JobName.SYSTEM_CONFIG_CHANGE)
async onSystemConfigChange() {
await this.systemConfigService.refreshConfig();
}
@Process(JobName.USER_DELETION)
async onUserDelete(job: Job<IUserDeletionJob>) {
await this.userService.handleUserDelete(job.data);
}
}
@Processor(QueueName.MACHINE_LEARNING)
export class MachineLearningProcessor {
constructor(private smartInfoService: SmartInfoService) {}
@Process({ name: JobName.IMAGE_TAGGING, concurrency: 2 })
async onTagImage(job: Job<IAssetJob>) {
await this.smartInfoService.handleTagImage(job.data);
}
@Process({ name: JobName.OBJECT_DETECTION, concurrency: 2 })
async onDetectObject(job: Job<IAssetJob>) {
await this.smartInfoService.handleDetectObjects(job.data);
}
}
@Processor(QueueName.STORAGE_TEMPLATE_MIGRATION)
export class StorageTemplateMigrationProcessor {
constructor(private storageTemplateService: StorageTemplateService) {}
@Process({ name: JobName.STORAGE_TEMPLATE_MIGRATION })
async onTemplateMigration() {
await this.storageTemplateService.handleTemplateMigration();
}
}
@Processor(QueueName.THUMBNAIL_GENERATION)
export class ThumbnailGeneratorProcessor {
constructor(private mediaService: MediaService) {}
@Process({ name: JobName.GENERATE_JPEG_THUMBNAIL, concurrency: 3 })
async handleGenerateJpegThumbnail(job: Job<IAssetJob>) {
await this.mediaService.handleGenerateJpegThumbnail(job.data);
}
@Process({ name: JobName.GENERATE_WEBP_THUMBNAIL, concurrency: 3 })
async handleGenerateWepbThumbnail(job: Job<IAssetJob>) {
await this.mediaService.handleGenerateWepbThumbnail(job.data);
}
}

View File

@@ -1,13 +0,0 @@
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(private jobService: JobService) {}
@Process(JobName.ASSET_UPLOADED)
async processUploadedVideo(job: Job<IAssetUploadedJob>) {
await this.jobService.handleUploadedAsset(job);
}
}

View File

@@ -1,17 +0,0 @@
import { assetUtils } from '@app/common/utils';
import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import { JobName, QueueName } from '@app/domain';
import { AssetEntity } from '@app/infra/db/entities';
@Processor(QueueName.BACKGROUND_TASK)
export class BackgroundTaskProcessor {
@Process(JobName.DELETE_FILE_ON_DISK)
async deleteFileOnDisk(job: Job<{ assets: AssetEntity[] }>) {
const { assets } = job.data;
for (const asset of assets) {
assetUtils.deleteFiles(asset);
}
}
}

View File

@@ -1,68 +0,0 @@
import { AssetEntity } from '@app/infra';
import { SmartInfoEntity } from '@app/infra';
import { QueueName, JobName } from '@app/domain';
import { IMachineLearningJob } from '@app/domain';
import { Process, Processor } from '@nestjs/bull';
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import axios from 'axios';
import { Job } from 'bull';
import { Repository } from 'typeorm';
import { MACHINE_LEARNING_ENABLED, MACHINE_LEARNING_URL } from '@app/common';
@Processor(QueueName.MACHINE_LEARNING)
export class MachineLearningProcessor {
constructor(
@InjectRepository(SmartInfoEntity)
private smartInfoRepository: Repository<SmartInfoEntity>,
) {}
@Process({ name: JobName.IMAGE_TAGGING, concurrency: 2 })
async tagImage(job: Job<IMachineLearningJob>) {
if (!MACHINE_LEARNING_ENABLED) {
return;
}
const { asset } = job.data;
const res = await axios.post(MACHINE_LEARNING_URL + '/image-classifier/tag-image', {
thumbnailPath: asset.resizePath,
});
if (res.status == 201 && res.data.length > 0) {
const smartInfo = new SmartInfoEntity();
smartInfo.assetId = asset.id;
smartInfo.tags = [...res.data];
await this.smartInfoRepository.upsert(smartInfo, {
conflictPaths: ['assetId'],
});
}
}
@Process({ name: JobName.OBJECT_DETECTION, concurrency: 2 })
async detectObject(job: Job<IMachineLearningJob>) {
if (!MACHINE_LEARNING_ENABLED) {
return;
}
try {
const { asset }: { asset: AssetEntity } = job.data;
const res = await axios.post(MACHINE_LEARNING_URL + '/object-detection/detect-object', {
thumbnailPath: asset.resizePath,
});
if (res.status == 201 && res.data.length > 0) {
const smartInfo = new SmartInfoEntity();
smartInfo.assetId = asset.id;
smartInfo.objects = [...res.data];
await this.smartInfoRepository.upsert(smartInfo, {
conflictPaths: ['assetId'],
});
}
} catch (error) {
Logger.error(`Failed to trigger object detection pipe line ${String(error)}`);
}
}
}

View File

@@ -1,13 +1,7 @@
import { AssetEntity, AssetType, ExifEntity } from '@app/infra';
import {
IExifExtractionProcessor,
IReverseGeocodingProcessor,
IVideoLengthExtractionProcessor,
QueueName,
JobName,
} from '@app/domain';
import { IReverseGeocodingJob, IAssetUploadedJob, QueueName, JobName, IAssetRepository } from '@app/domain';
import { Process, Processor } from '@nestjs/bull';
import { Logger } from '@nestjs/common';
import { Inject, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Job } from 'bull';
@@ -19,7 +13,6 @@ import geocoder, { InitOptions } from 'local-reverse-geocoder';
import { getName } from 'i18n-iso-countries';
import fs from 'node:fs';
import { ExifDateTime, exiftool, Tags } from 'exiftool-vendored';
import { IsNull, Not } from 'typeorm';
interface ImmichTags extends Tags {
ContentIdentifier?: string;
@@ -79,9 +72,7 @@ export class MetadataExtractionProcessor {
private logger = new Logger(MetadataExtractionProcessor.name);
private isGeocodeInitialized = false;
constructor(
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@InjectRepository(ExifEntity)
private exifRepository: Repository<ExifEntity>,
@@ -141,7 +132,7 @@ export class MetadataExtractionProcessor {
}
@Process(JobName.EXIF_EXTRACTION)
async extractExifInfo(job: Job<IExifExtractionProcessor>) {
async extractExifInfo(job: Job<IAssetUploadedJob>) {
try {
const { asset, fileName }: { asset: AssetEntity; fileName: string } = job.data;
const exifData = await exiftool.read<ImmichTags>(asset.originalPath).catch((e) => {
@@ -190,22 +181,14 @@ export class MetadataExtractionProcessor {
});
if (newExif.livePhotoCID && !asset.livePhotoVideoId) {
const motionAsset = await this.assetRepository.findOne({
where: {
id: Not(asset.id),
type: AssetType.VIDEO,
exifInfo: {
livePhotoCID: newExif.livePhotoCID,
},
},
relations: {
exifInfo: true,
},
});
const motionAsset = await this.assetRepository.findLivePhotoMatch(
newExif.livePhotoCID,
AssetType.VIDEO,
asset.id,
);
if (motionAsset) {
await this.assetRepository.update(asset.id, { livePhotoVideoId: motionAsset.id });
await this.assetRepository.update(motionAsset.id, { isVisible: false });
await this.assetRepository.save({ id: asset.id, livePhotoVideoId: motionAsset.id });
await this.assetRepository.save({ id: motionAsset.id, isVisible: false });
}
}
@@ -249,7 +232,7 @@ export class MetadataExtractionProcessor {
}
@Process({ name: JobName.REVERSE_GEOCODING })
async reverseGeocoding(job: Job<IReverseGeocodingProcessor>) {
async reverseGeocoding(job: Job<IReverseGeocodingJob>) {
if (this.isGeocodeInitialized) {
const { latitude, longitude } = job.data;
const { country, state, city } = await this.reverseGeocodeExif(latitude, longitude);
@@ -258,7 +241,7 @@ export class MetadataExtractionProcessor {
}
@Process({ name: JobName.EXTRACT_VIDEO_METADATA, concurrency: 2 })
async extractVideoMetadata(job: Job<IVideoLengthExtractionProcessor>) {
async extractVideoMetadata(job: Job<IAssetUploadedJob>) {
const { asset, fileName } = job.data;
if (!asset.isVisible) {
@@ -309,20 +292,14 @@ export class MetadataExtractionProcessor {
newExif.livePhotoCID = exifData?.ContentIdentifier || null;
if (newExif.livePhotoCID) {
const photoAsset = await this.assetRepository.findOne({
where: {
id: Not(asset.id),
type: AssetType.IMAGE,
livePhotoVideoId: IsNull(),
exifInfo: {
livePhotoCID: newExif.livePhotoCID,
},
},
});
const photoAsset = await this.assetRepository.findLivePhotoMatch(
newExif.livePhotoCID,
AssetType.IMAGE,
asset.id,
);
if (photoAsset) {
await this.assetRepository.update(photoAsset.id, { livePhotoVideoId: asset.id });
await this.assetRepository.update(asset.id, { isVisible: false });
await this.assetRepository.save({ id: photoAsset.id, livePhotoVideoId: asset.id });
await this.assetRepository.save({ id: asset.id, isVisible: false });
}
}
@@ -378,7 +355,7 @@ export class MetadataExtractionProcessor {
}
await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] });
await this.assetRepository.update({ id: asset.id }, { duration: durationString, fileCreatedAt });
await this.assetRepository.save({ id: asset.id, duration: durationString, fileCreatedAt });
} catch (err) {
``;
// do nothing

View File

@@ -1,61 +0,0 @@
import { APP_UPLOAD_LOCATION } from '@app/common';
import { AssetEntity } from '@app/infra';
import { SystemConfigService } from '@app/domain';
import { QueueName, JobName } from '@app/domain';
import { StorageService } from '@app/storage';
import { Process, Processor } from '@nestjs/bull';
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Processor(QueueName.CONFIG)
export class StorageMigrationProcessor {
readonly logger: Logger = new Logger(StorageMigrationProcessor.name);
constructor(
private storageService: StorageService,
private systemConfigService: SystemConfigService,
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
) {}
/**
* Migration process when a new user set a new storage template.
* @param job
*/
@Process({ name: JobName.TEMPLATE_MIGRATION, concurrency: 100 })
async templateMigration() {
console.time('migrating-time');
const assets = await this.assetRepository.find({
relations: ['exifInfo'],
});
const livePhotoMap: Record<string, AssetEntity> = {};
for (const asset of assets) {
if (asset.livePhotoVideoId) {
livePhotoMap[asset.livePhotoVideoId] = asset;
}
}
for (const asset of assets) {
const livePhotoParentAsset = livePhotoMap[asset.id];
const filename = asset.exifInfo?.imageName || livePhotoParentAsset?.exifInfo?.imageName || asset.id;
await this.storageService.moveAsset(asset, filename);
}
await this.storageService.removeEmptyDirectories(APP_UPLOAD_LOCATION);
console.timeEnd('migrating-time');
}
/**
* Update config when a new storage template is set.
* This is to ensure the synchronization between processes.
* @param job
*/
@Process({ name: JobName.CONFIG_CHANGE, concurrency: 1 })
async updateTemplate() {
await this.systemConfigService.refreshConfig();
}
}

View File

@@ -1,130 +0,0 @@
import { APP_UPLOAD_LOCATION } from '@app/common';
import { AssetEntity, AssetType } from '@app/infra';
import { WebpGeneratorProcessor, JpegGeneratorProcessor, QueueName, JobName } from '@app/domain';
import { InjectQueue, Process, Processor } from '@nestjs/bull';
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { mapAsset } from '@app/domain';
import { Job, Queue } from 'bull';
import ffmpeg from 'fluent-ffmpeg';
import { existsSync, mkdirSync } from 'node:fs';
import sanitize from 'sanitize-filename';
import sharp from 'sharp';
import { Repository } from 'typeorm/repository/Repository';
import { join } from 'path';
import { CommunicationGateway } from 'apps/immich/src/api-v1/communication/communication.gateway';
import { IMachineLearningJob } from '@app/domain';
import { exiftool } from 'exiftool-vendored';
@Processor(QueueName.THUMBNAIL_GENERATION)
export class ThumbnailGeneratorProcessor {
readonly logger: Logger = new Logger(ThumbnailGeneratorProcessor.name);
constructor(
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
@InjectQueue(QueueName.THUMBNAIL_GENERATION)
private thumbnailGeneratorQueue: Queue,
private wsCommunicationGateway: CommunicationGateway,
@InjectQueue(QueueName.MACHINE_LEARNING)
private machineLearningQueue: Queue<IMachineLearningJob>,
) {}
@Process({ name: JobName.GENERATE_JPEG_THUMBNAIL, concurrency: 3 })
async generateJPEGThumbnail(job: Job<JpegGeneratorProcessor>) {
const basePath = APP_UPLOAD_LOCATION;
const { asset } = job.data;
const sanitizedDeviceId = sanitize(String(asset.deviceId));
const resizePath = join(basePath, asset.ownerId, 'thumb', sanitizedDeviceId);
if (!existsSync(resizePath)) {
mkdirSync(resizePath, { recursive: true });
}
const jpegThumbnailPath = join(resizePath, `${asset.id}.jpeg`);
if (asset.type == AssetType.IMAGE) {
try {
await sharp(asset.originalPath, { failOnError: false })
.resize(1440, 1440, { fit: 'outside', withoutEnlargement: true })
.jpeg()
.rotate()
.toFile(jpegThumbnailPath)
.catch(() => {
this.logger.warn(
'Failed to generate jpeg thumbnail for asset: ' +
asset.id +
' using sharp, failing over to exiftool-vendored',
);
return exiftool.extractThumbnail(asset.originalPath, jpegThumbnailPath);
});
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
} catch (error: any) {
this.logger.error('Failed to generate jpeg thumbnail for asset: ' + asset.id, error.stack);
}
// Update resize path to send to generate webp queue
asset.resizePath = jpegThumbnailPath;
await this.thumbnailGeneratorQueue.add(JobName.GENERATE_WEBP_THUMBNAIL, { asset });
await this.machineLearningQueue.add(JobName.IMAGE_TAGGING, { asset });
await this.machineLearningQueue.add(JobName.OBJECT_DETECTION, { asset });
this.wsCommunicationGateway.server.to(asset.ownerId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
}
if (asset.type == AssetType.VIDEO) {
await new Promise((resolve, reject) => {
ffmpeg(asset.originalPath)
.outputOptions(['-ss 00:00:00.000', '-frames:v 1'])
.output(jpegThumbnailPath)
.on('start', () => {
Logger.log('Start Generating Video Thumbnail', 'generateJPEGThumbnail');
})
.on('error', (error) => {
Logger.error(`Cannot Generate Video Thumbnail ${error}`, 'generateJPEGThumbnail');
reject(error);
})
.on('end', async () => {
Logger.log(`Generating Video Thumbnail Success ${asset.id}`, 'generateJPEGThumbnail');
resolve(asset);
})
.run();
});
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
// Update resize path to send to generate webp queue
asset.resizePath = jpegThumbnailPath;
await this.thumbnailGeneratorQueue.add(JobName.GENERATE_WEBP_THUMBNAIL, { asset });
await this.machineLearningQueue.add(JobName.IMAGE_TAGGING, { asset });
await this.machineLearningQueue.add(JobName.OBJECT_DETECTION, { asset });
this.wsCommunicationGateway.server.to(asset.ownerId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
}
}
@Process({ name: JobName.GENERATE_WEBP_THUMBNAIL, concurrency: 3 })
async generateWepbThumbnail(job: Job<WebpGeneratorProcessor>) {
const { asset } = job.data;
if (!asset.resizePath) {
return;
}
const webpPath = asset.resizePath.replace('jpeg', 'webp');
try {
await sharp(asset.resizePath, { failOnError: false }).resize(250).webp().rotate().toFile(webpPath);
await this.assetRepository.update({ id: asset.id }, { webpPath: webpPath });
} catch (error: any) {
this.logger.error('Failed to generate webp thumbnail for asset: ' + asset.id, error.stack);
}
}
}

View File

@@ -1,72 +0,0 @@
import { APP_UPLOAD_LOCATION, userUtils } from '@app/common';
import { AlbumEntity, APIKeyEntity, AssetEntity, UserEntity, UserTokenEntity } from '@app/infra';
import { QueueName, JobName } from '@app/domain';
import { IUserDeletionJob } from '@app/domain';
import { Process, Processor } from '@nestjs/bull';
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Job } from 'bull';
import { join } from 'path';
import fs from 'fs';
import { Repository } from 'typeorm';
@Processor(QueueName.USER_DELETION)
export class UserDeletionProcessor {
private logger = new Logger(UserDeletionProcessor.name);
constructor(
@InjectRepository(UserEntity)
private userRepository: Repository<UserEntity>,
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
@InjectRepository(APIKeyEntity)
private apiKeyRepository: Repository<APIKeyEntity>,
@InjectRepository(UserTokenEntity)
private userTokenRepository: Repository<UserTokenEntity>,
@InjectRepository(AlbumEntity)
private albumRepository: Repository<AlbumEntity>,
) {}
@Process(JobName.USER_DELETION)
async processUserDeletion(job: Job<IUserDeletionJob>) {
const { user } = job.data;
// just for extra protection here
if (!userUtils.isReadyForDeletion(user)) {
this.logger.warn(`Skipped user that was not ready for deletion: id=${user.id}`);
return;
}
this.logger.log(`Deleting user: ${user.id}`);
try {
const basePath = APP_UPLOAD_LOCATION;
const userAssetDir = join(basePath, user.id);
this.logger.warn(`Removing user from filesystem: ${userAssetDir}`);
fs.rmSync(userAssetDir, { recursive: true, force: true });
this.logger.warn(`Removing user from database: ${user.id}`);
const userTokens = await this.userTokenRepository.find({
where: { user: { id: user.id } },
relations: { user: true },
withDeleted: true,
});
await this.userTokenRepository.remove(userTokens);
const albums = await this.albumRepository.find({ where: { ownerId: user.id } });
await this.albumRepository.remove(albums);
await this.apiKeyRepository.delete({ userId: user.id });
await this.assetRepository.delete({ ownerId: user.id });
await this.userRepository.remove(user);
} catch (error: any) {
this.logger.error(`Failed to remove user`);
this.logger.error(error, error?.stack);
throw error;
}
}
}

View File

@@ -1,25 +1,22 @@
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
import { AssetEntity } from '@app/infra';
import { IVideoConversionProcessor, JobName, QueueName, SystemConfigService } from '@app/domain';
import { IAssetJob, IAssetRepository, JobName, QueueName, SystemConfigService } from '@app/domain';
import { Process, Processor } from '@nestjs/bull';
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Inject, Logger } from '@nestjs/common';
import { Job } from 'bull';
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
import { existsSync, mkdirSync } from 'fs';
import { Repository } from 'typeorm';
@Processor(QueueName.VIDEO_CONVERSION)
export class VideoTranscodeProcessor {
readonly logger = new Logger(VideoTranscodeProcessor.name);
constructor(
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
private systemConfigService: SystemConfigService,
) {}
@Process({ name: JobName.VIDEO_CONVERSION, concurrency: 2 })
async videoConversion(job: Job<IVideoConversionProcessor>) {
async videoConversion(job: Job<IAssetJob>) {
const { asset } = job.data;
const basePath = APP_UPLOAD_LOCATION;
const encodedVideoPath = `${basePath}/${asset.ownerId}/encoded-video`;
@@ -93,7 +90,7 @@ export class VideoTranscodeProcessor {
})
.on('end', async () => {
this.logger.log(`Converting Success ${asset.id}`);
await this.assetRepository.update({ id: asset.id }, { encodedVideoPath: savedEncodedPath });
await this.assetRepository.save({ id: asset.id, encodedVideoPath: savedEncodedPath });
resolve();
})
.run();