mirror of
https://github.com/immich-app/immich.git
synced 2024-12-26 10:50:29 +02:00
refactor(server): job repository (#1382)
* refactor(server): job repository * refactor: job repository * chore: generate open-api * fix: job panel * Remove incorrect subtitle Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
f4c90426a5
commit
4cfac47674
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
@ -53,7 +53,6 @@ doc/JobCommand.md
|
|||||||
doc/JobCommandDto.md
|
doc/JobCommandDto.md
|
||||||
doc/JobCounts.md
|
doc/JobCounts.md
|
||||||
doc/JobId.md
|
doc/JobId.md
|
||||||
doc/JobStatusResponseDto.md
|
|
||||||
doc/LoginCredentialDto.md
|
doc/LoginCredentialDto.md
|
||||||
doc/LoginResponseDto.md
|
doc/LoginResponseDto.md
|
||||||
doc/LogoutResponseDto.md
|
doc/LogoutResponseDto.md
|
||||||
@ -162,7 +161,6 @@ lib/model/job_command.dart
|
|||||||
lib/model/job_command_dto.dart
|
lib/model/job_command_dto.dart
|
||||||
lib/model/job_counts.dart
|
lib/model/job_counts.dart
|
||||||
lib/model/job_id.dart
|
lib/model/job_id.dart
|
||||||
lib/model/job_status_response_dto.dart
|
|
||||||
lib/model/login_credential_dto.dart
|
lib/model/login_credential_dto.dart
|
||||||
lib/model/login_response_dto.dart
|
lib/model/login_response_dto.dart
|
||||||
lib/model/logout_response_dto.dart
|
lib/model/logout_response_dto.dart
|
||||||
@ -250,7 +248,6 @@ test/job_command_dto_test.dart
|
|||||||
test/job_command_test.dart
|
test/job_command_test.dart
|
||||||
test/job_counts_test.dart
|
test/job_counts_test.dart
|
||||||
test/job_id_test.dart
|
test/job_id_test.dart
|
||||||
test/job_status_response_dto_test.dart
|
|
||||||
test/login_credential_dto_test.dart
|
test/login_credential_dto_test.dart
|
||||||
test/login_response_dto_test.dart
|
test/login_response_dto_test.dart
|
||||||
test/logout_response_dto_test.dart
|
test/logout_response_dto_test.dart
|
||||||
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AllJobStatusResponseDto.md
generated
BIN
mobile/openapi/doc/AllJobStatusResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/JobApi.md
generated
BIN
mobile/openapi/doc/JobApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/JobStatusResponseDto.md
generated
BIN
mobile/openapi/doc/JobStatusResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/job_api.dart
generated
BIN
mobile/openapi/lib/api/job_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/job_status_response_dto.dart
generated
BIN
mobile/openapi/lib/model/job_status_response_dto.dart
generated
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/test/job_api_test.dart
generated
BIN
mobile/openapi/test/job_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/job_status_response_dto_test.dart
generated
BIN
mobile/openapi/test/job_status_response_dto_test.dart
generated
Binary file not shown.
@ -295,7 +295,7 @@ export class AssetController {
|
|||||||
deleteAssetList.filter((a) => a.id == res.id && res.status == DeleteAssetStatusEnum.SUCCESS);
|
deleteAssetList.filter((a) => a.id == res.id && res.status == DeleteAssetStatusEnum.SUCCESS);
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.backgroundTaskService.deleteFileOnDisk(deleteAssetList);
|
await this.backgroundTaskService.deleteFileOnDisk(deleteAssetList as any[]);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,11 @@ import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
|||||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||||
import { DownloadService } from '../../modules/download/download.service';
|
import { DownloadService } from '../../modules/download/download.service';
|
||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||||
import { IAssetUploadedJob, IVideoTranscodeJob } from '@app/domain';
|
|
||||||
import { Queue } from 'bull';
|
|
||||||
import { IAlbumRepository } from '../album/album-repository';
|
import { IAlbumRepository } from '../album/album-repository';
|
||||||
import { StorageService } from '@app/storage';
|
import { StorageService } from '@app/storage';
|
||||||
import { ISharedLinkRepository } from '../share/shared-link.repository';
|
import { ISharedLinkRepository } from '../share/shared-link.repository';
|
||||||
|
import { IJobRepository } from '@app/domain';
|
||||||
|
import { newJobRepositoryMock } from '@app/domain/../test';
|
||||||
|
|
||||||
describe('AssetService', () => {
|
describe('AssetService', () => {
|
||||||
let sui: AssetService;
|
let sui: AssetService;
|
||||||
@ -22,10 +22,9 @@ describe('AssetService', () => {
|
|||||||
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
||||||
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
|
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
|
||||||
let backgroundTaskServiceMock: jest.Mocked<BackgroundTaskService>;
|
let backgroundTaskServiceMock: jest.Mocked<BackgroundTaskService>;
|
||||||
let assetUploadedQueueMock: jest.Mocked<Queue<IAssetUploadedJob>>;
|
|
||||||
let videoConversionQueueMock: jest.Mocked<Queue<IVideoTranscodeJob>>;
|
|
||||||
let storageSeriveMock: jest.Mocked<StorageService>;
|
let storageSeriveMock: jest.Mocked<StorageService>;
|
||||||
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
||||||
|
let jobMock: jest.Mocked<IJobRepository>;
|
||||||
const authUser: AuthUserDto = Object.freeze({
|
const authUser: AuthUserDto = Object.freeze({
|
||||||
id: 'user_id_1',
|
id: 'user_id_1',
|
||||||
email: 'auth@test.com',
|
email: 'auth@test.com',
|
||||||
@ -148,16 +147,17 @@ describe('AssetService', () => {
|
|||||||
getByIdAndUserId: jest.fn(),
|
getByIdAndUserId: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
jobMock = newJobRepositoryMock();
|
||||||
|
|
||||||
sui = new AssetService(
|
sui = new AssetService(
|
||||||
assetRepositoryMock,
|
assetRepositoryMock,
|
||||||
albumRepositoryMock,
|
albumRepositoryMock,
|
||||||
a,
|
a,
|
||||||
backgroundTaskServiceMock,
|
backgroundTaskServiceMock,
|
||||||
assetUploadedQueueMock,
|
|
||||||
videoConversionQueueMock,
|
|
||||||
downloadServiceMock as DownloadService,
|
downloadServiceMock as DownloadService,
|
||||||
storageSeriveMock,
|
storageSeriveMock,
|
||||||
sharedLinkRepositoryMock,
|
sharedLinkRepositoryMock,
|
||||||
|
jobMock,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -43,9 +43,7 @@ import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-as
|
|||||||
import { UpdateAssetDto } from './dto/update-asset.dto';
|
import { UpdateAssetDto } from './dto/update-asset.dto';
|
||||||
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
|
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
|
||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||||
import { IAssetUploadedJob, IVideoTranscodeJob, JobName, QueueName } from '@app/domain';
|
import { IJobRepository, JobName } from '@app/domain';
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
|
||||||
import { Queue } from 'bull';
|
|
||||||
import { DownloadService } from '../../modules/download/download.service';
|
import { DownloadService } from '../../modules/download/download.service';
|
||||||
import { DownloadDto } from './dto/download-library.dto';
|
import { DownloadDto } from './dto/download-library.dto';
|
||||||
import { IAlbumRepository } from '../album/album-repository';
|
import { IAlbumRepository } from '../album/album-repository';
|
||||||
@ -66,24 +64,14 @@ export class AssetService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAssetRepository) private _assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private _assetRepository: IAssetRepository,
|
||||||
|
|
||||||
@Inject(IAlbumRepository) private _albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private _albumRepository: IAlbumRepository,
|
||||||
|
|
||||||
@InjectRepository(AssetEntity)
|
@InjectRepository(AssetEntity)
|
||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
|
|
||||||
private backgroundTaskService: BackgroundTaskService,
|
private backgroundTaskService: BackgroundTaskService,
|
||||||
|
|
||||||
@InjectQueue(QueueName.ASSET_UPLOADED)
|
|
||||||
private assetUploadedQueue: Queue<IAssetUploadedJob>,
|
|
||||||
|
|
||||||
@InjectQueue(QueueName.VIDEO_CONVERSION)
|
|
||||||
private videoConversionQueue: Queue<IVideoTranscodeJob>,
|
|
||||||
|
|
||||||
private downloadService: DownloadService,
|
private downloadService: DownloadService,
|
||||||
|
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
||||||
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
) {
|
) {
|
||||||
this.shareCore = new ShareCore(sharedLinkRepository);
|
this.shareCore = new ShareCore(sharedLinkRepository);
|
||||||
}
|
}
|
||||||
@ -122,7 +110,7 @@ export class AssetService {
|
|||||||
|
|
||||||
await this.storageService.moveAsset(livePhotoAssetEntity, originalAssetData.originalname);
|
await this.storageService.moveAsset(livePhotoAssetEntity, originalAssetData.originalname);
|
||||||
|
|
||||||
await this.videoConversionQueue.add(JobName.VIDEO_CONVERSION, { asset: livePhotoAssetEntity });
|
await this.jobRepository.add({ name: JobName.VIDEO_CONVERSION, data: { asset: livePhotoAssetEntity } });
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetEntity = await this.createUserAsset(
|
const assetEntity = await this.createUserAsset(
|
||||||
@ -146,11 +134,10 @@ export class AssetService {
|
|||||||
|
|
||||||
const movedAsset = await this.storageService.moveAsset(assetEntity, originalAssetData.originalname);
|
const movedAsset = await this.storageService.moveAsset(assetEntity, originalAssetData.originalname);
|
||||||
|
|
||||||
await this.assetUploadedQueue.add(
|
await this.jobRepository.add({
|
||||||
JobName.ASSET_UPLOADED,
|
name: JobName.ASSET_UPLOADED,
|
||||||
{ asset: movedAsset, fileName: originalAssetData.originalname },
|
data: { asset: movedAsset, fileName: originalAssetData.originalname },
|
||||||
{ jobId: movedAsset.id },
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return new AssetFileUploadResponseDto(movedAsset.id);
|
return new AssetFileUploadResponseDto(movedAsset.id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { Controller, Get, Body, ValidationPipe, Put, Param } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Put, ValidationPipe } from '@nestjs/common';
|
||||||
import { JobService } from './job.service';
|
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
import { Authenticated } from '../../decorators/authenticated.decorator';
|
||||||
import { AllJobStatusResponseDto } from './response-dto/all-job-status-response.dto';
|
import { AllJobStatusResponseDto } from './response-dto/all-job-status-response.dto';
|
||||||
import { GetJobDto } from './dto/get-job.dto';
|
import { GetJobDto } from './dto/get-job.dto';
|
||||||
import { JobStatusResponseDto } from './response-dto/job-status-response.dto';
|
import { JobService } from './job.service';
|
||||||
|
|
||||||
import { JobCommandDto } from './dto/job-command.dto';
|
import { JobCommandDto } from './dto/job-command.dto';
|
||||||
|
|
||||||
@Authenticated({ admin: true })
|
@Authenticated({ admin: true })
|
||||||
@ -20,21 +18,16 @@ export class JobController {
|
|||||||
return this.jobService.getAllJobsStatus();
|
return this.jobService.getAllJobsStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/:jobId')
|
|
||||||
getJobStatus(@Param(ValidationPipe) params: GetJobDto): Promise<JobStatusResponseDto> {
|
|
||||||
return this.jobService.getJobStatus(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Put('/:jobId')
|
@Put('/:jobId')
|
||||||
async sendJobCommand(
|
async sendJobCommand(
|
||||||
@Param(ValidationPipe) params: GetJobDto,
|
@Param(ValidationPipe) params: GetJobDto,
|
||||||
@Body(ValidationPipe) body: JobCommandDto,
|
@Body(ValidationPipe) body: JobCommandDto,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
if (body.command === 'start') {
|
if (body.command === 'start') {
|
||||||
return await this.jobService.startJob(params);
|
return await this.jobService.start(params.jobId);
|
||||||
}
|
}
|
||||||
if (body.command === 'stop') {
|
if (body.command === 'stop') {
|
||||||
return await this.jobService.stopJob(params);
|
return await this.jobService.stop(params.jobId);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -1,217 +1,118 @@
|
|||||||
import {
|
import { JobName, IJobRepository, QueueName } from '@app/domain';
|
||||||
IMachineLearningJob,
|
|
||||||
IMetadataExtractionJob,
|
|
||||||
IThumbnailGenerationJob,
|
|
||||||
IVideoTranscodeJob,
|
|
||||||
JobName,
|
|
||||||
QueueName,
|
|
||||||
} from '@app/domain';
|
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
|
||||||
import { Queue } from 'bull';
|
|
||||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { AllJobStatusResponseDto } from './response-dto/all-job-status-response.dto';
|
import { AllJobStatusResponseDto } from './response-dto/all-job-status-response.dto';
|
||||||
import { IAssetRepository } from '../asset/asset-repository';
|
import { IAssetRepository } from '../asset/asset-repository';
|
||||||
import { AssetType } from '@app/infra';
|
import { AssetType } from '@app/infra';
|
||||||
import { GetJobDto, JobId } from './dto/get-job.dto';
|
import { JobId } from './dto/get-job.dto';
|
||||||
import { JobStatusResponseDto } from './response-dto/job-status-response.dto';
|
|
||||||
import { StorageService } from '@app/storage';
|
|
||||||
import { MACHINE_LEARNING_ENABLED } from '@app/common';
|
import { MACHINE_LEARNING_ENABLED } from '@app/common';
|
||||||
|
|
||||||
|
const jobIds = Object.values(JobId) as JobId[];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JobService {
|
export class JobService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectQueue(QueueName.THUMBNAIL_GENERATION)
|
@Inject(IAssetRepository) private _assetRepository: IAssetRepository,
|
||||||
private thumbnailGeneratorQueue: Queue<IThumbnailGenerationJob>,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
|
|
||||||
@InjectQueue(QueueName.METADATA_EXTRACTION)
|
|
||||||
private metadataExtractionQueue: Queue<IMetadataExtractionJob>,
|
|
||||||
|
|
||||||
@InjectQueue(QueueName.VIDEO_CONVERSION)
|
|
||||||
private videoConversionQueue: Queue<IVideoTranscodeJob>,
|
|
||||||
|
|
||||||
@InjectQueue(QueueName.MACHINE_LEARNING)
|
|
||||||
private machineLearningQueue: Queue<IMachineLearningJob>,
|
|
||||||
|
|
||||||
@InjectQueue(QueueName.CONFIG)
|
|
||||||
private configQueue: Queue,
|
|
||||||
|
|
||||||
@Inject(IAssetRepository)
|
|
||||||
private _assetRepository: IAssetRepository,
|
|
||||||
|
|
||||||
private storageService: StorageService,
|
|
||||||
) {
|
) {
|
||||||
this.thumbnailGeneratorQueue.empty();
|
for (const jobId of jobIds) {
|
||||||
this.metadataExtractionQueue.empty();
|
this.jobRepository.empty(this.asQueueName(jobId));
|
||||||
this.videoConversionQueue.empty();
|
}
|
||||||
this.configQueue.empty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async startJob(jobDto: GetJobDto): Promise<number> {
|
start(jobId: JobId): Promise<number> {
|
||||||
switch (jobDto.jobId) {
|
return this.run(this.asQueueName(jobId));
|
||||||
case JobId.THUMBNAIL_GENERATION:
|
|
||||||
return this.runThumbnailGenerationJob();
|
|
||||||
case JobId.METADATA_EXTRACTION:
|
|
||||||
return this.runMetadataExtractionJob();
|
|
||||||
case JobId.VIDEO_CONVERSION:
|
|
||||||
return this.runVideoConversionJob();
|
|
||||||
case JobId.MACHINE_LEARNING:
|
|
||||||
return this.runMachineLearningPipeline();
|
|
||||||
case JobId.STORAGE_TEMPLATE_MIGRATION:
|
|
||||||
return this.runStorageMigration();
|
|
||||||
default:
|
|
||||||
throw new BadRequestException('Invalid job id');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async stop(jobId: JobId): Promise<number> {
|
||||||
|
await this.jobRepository.empty(this.asQueueName(jobId));
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllJobsStatus(): Promise<AllJobStatusResponseDto> {
|
async getAllJobsStatus(): Promise<AllJobStatusResponseDto> {
|
||||||
const thumbnailGeneratorJobCount = await this.thumbnailGeneratorQueue.getJobCounts();
|
|
||||||
const metadataExtractionJobCount = await this.metadataExtractionQueue.getJobCounts();
|
|
||||||
const videoConversionJobCount = await this.videoConversionQueue.getJobCounts();
|
|
||||||
const machineLearningJobCount = await this.machineLearningQueue.getJobCounts();
|
|
||||||
const storageMigrationJobCount = await this.configQueue.getJobCounts();
|
|
||||||
|
|
||||||
const response = new AllJobStatusResponseDto();
|
const response = new AllJobStatusResponseDto();
|
||||||
response.isThumbnailGenerationActive = Boolean(thumbnailGeneratorJobCount.waiting);
|
for (const jobId of jobIds) {
|
||||||
response.thumbnailGenerationQueueCount = thumbnailGeneratorJobCount;
|
response[jobId] = await this.jobRepository.getJobCounts(this.asQueueName(jobId));
|
||||||
response.isMetadataExtractionActive = Boolean(metadataExtractionJobCount.waiting);
|
}
|
||||||
response.metadataExtractionQueueCount = metadataExtractionJobCount;
|
|
||||||
response.isVideoConversionActive = Boolean(videoConversionJobCount.waiting);
|
|
||||||
response.videoConversionQueueCount = videoConversionJobCount;
|
|
||||||
response.isMachineLearningActive = Boolean(machineLearningJobCount.waiting);
|
|
||||||
response.machineLearningQueueCount = machineLearningJobCount;
|
|
||||||
response.isStorageMigrationActive = Boolean(storageMigrationJobCount.active);
|
|
||||||
response.storageMigrationQueueCount = storageMigrationJobCount;
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getJobStatus(query: GetJobDto): Promise<JobStatusResponseDto> {
|
private async run(name: QueueName): Promise<number> {
|
||||||
const response = new JobStatusResponseDto();
|
const isActive = await this.jobRepository.isActive(name);
|
||||||
if (query.jobId === JobId.THUMBNAIL_GENERATION) {
|
if (isActive) {
|
||||||
response.isActive = Boolean((await this.thumbnailGeneratorQueue.getJobCounts()).waiting);
|
throw new BadRequestException(`Job is already running`);
|
||||||
response.queueCount = await this.thumbnailGeneratorQueue.getJobCounts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.jobId === JobId.METADATA_EXTRACTION) {
|
switch (name) {
|
||||||
response.isActive = Boolean((await this.metadataExtractionQueue.getJobCounts()).waiting);
|
case QueueName.VIDEO_CONVERSION: {
|
||||||
response.queueCount = await this.metadataExtractionQueue.getJobCounts();
|
const assets = await this._assetRepository.getAssetWithNoEncodedVideo();
|
||||||
|
for (const asset of assets) {
|
||||||
|
await this.jobRepository.add({ name: JobName.VIDEO_CONVERSION, data: { asset } });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.jobId === JobId.VIDEO_CONVERSION) {
|
return assets.length;
|
||||||
response.isActive = Boolean((await this.videoConversionQueue.getJobCounts()).waiting);
|
|
||||||
response.queueCount = await this.videoConversionQueue.getJobCounts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.jobId === JobId.STORAGE_TEMPLATE_MIGRATION) {
|
case QueueName.CONFIG:
|
||||||
response.isActive = Boolean((await this.configQueue.getJobCounts()).waiting);
|
await this.jobRepository.add({ name: JobName.TEMPLATE_MIGRATION });
|
||||||
response.queueCount = await this.configQueue.getJobCounts();
|
return 1;
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
case QueueName.MACHINE_LEARNING: {
|
||||||
}
|
|
||||||
|
|
||||||
async stopJob(query: GetJobDto): Promise<number> {
|
|
||||||
switch (query.jobId) {
|
|
||||||
case JobId.THUMBNAIL_GENERATION:
|
|
||||||
this.thumbnailGeneratorQueue.empty();
|
|
||||||
return 0;
|
|
||||||
case JobId.METADATA_EXTRACTION:
|
|
||||||
this.metadataExtractionQueue.empty();
|
|
||||||
return 0;
|
|
||||||
case JobId.VIDEO_CONVERSION:
|
|
||||||
this.videoConversionQueue.empty();
|
|
||||||
return 0;
|
|
||||||
case JobId.MACHINE_LEARNING:
|
|
||||||
this.machineLearningQueue.empty();
|
|
||||||
return 0;
|
|
||||||
case JobId.STORAGE_TEMPLATE_MIGRATION:
|
|
||||||
this.configQueue.empty();
|
|
||||||
return 0;
|
|
||||||
default:
|
|
||||||
throw new BadRequestException('Invalid job id');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runThumbnailGenerationJob(): Promise<number> {
|
|
||||||
const jobCount = await this.thumbnailGeneratorQueue.getJobCounts();
|
|
||||||
|
|
||||||
if (jobCount.waiting > 0) {
|
|
||||||
throw new BadRequestException('Thumbnail generation job is already running');
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetsWithNoThumbnail = await this._assetRepository.getAssetWithNoThumbnail();
|
|
||||||
|
|
||||||
for (const asset of assetsWithNoThumbnail) {
|
|
||||||
await this.thumbnailGeneratorQueue.add(JobName.GENERATE_JPEG_THUMBNAIL, { asset });
|
|
||||||
}
|
|
||||||
|
|
||||||
return assetsWithNoThumbnail.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runMetadataExtractionJob(): Promise<number> {
|
|
||||||
const jobCount = await this.metadataExtractionQueue.getJobCounts();
|
|
||||||
|
|
||||||
if (jobCount.waiting > 0) {
|
|
||||||
throw new BadRequestException('Metadata extraction job is already running');
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetsWithNoExif = await this._assetRepository.getAssetWithNoEXIF();
|
|
||||||
for (const asset of assetsWithNoExif) {
|
|
||||||
if (asset.type === AssetType.VIDEO) {
|
|
||||||
await this.metadataExtractionQueue.add(JobName.EXTRACT_VIDEO_METADATA, { asset, fileName: asset.id });
|
|
||||||
} else {
|
|
||||||
await this.metadataExtractionQueue.add(JobName.EXIF_EXTRACTION, { asset, fileName: asset.id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return assetsWithNoExif.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runMachineLearningPipeline(): Promise<number> {
|
|
||||||
if (!MACHINE_LEARNING_ENABLED) {
|
if (!MACHINE_LEARNING_ENABLED) {
|
||||||
throw new BadRequestException('Machine learning is not enabled.');
|
throw new BadRequestException('Machine learning is not enabled.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobCount = await this.machineLearningQueue.getJobCounts();
|
const assets = await this._assetRepository.getAssetWithNoSmartInfo();
|
||||||
|
for (const asset of assets) {
|
||||||
if (jobCount.waiting > 0) {
|
await this.jobRepository.add({ name: JobName.IMAGE_TAGGING, data: { asset } });
|
||||||
throw new BadRequestException('Metadata extraction job is already running');
|
await this.jobRepository.add({ name: JobName.OBJECT_DETECTION, data: { asset } });
|
||||||
|
}
|
||||||
|
return assets.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetWithNoSmartInfo = await this._assetRepository.getAssetWithNoSmartInfo();
|
case QueueName.METADATA_EXTRACTION: {
|
||||||
|
const assets = await this._assetRepository.getAssetWithNoEXIF();
|
||||||
for (const asset of assetWithNoSmartInfo) {
|
for (const asset of assets) {
|
||||||
await this.machineLearningQueue.add(JobName.IMAGE_TAGGING, { asset });
|
if (asset.type === AssetType.VIDEO) {
|
||||||
await this.machineLearningQueue.add(JobName.OBJECT_DETECTION, { asset });
|
await this.jobRepository.add({ name: JobName.EXTRACT_VIDEO_METADATA, data: { asset, fileName: asset.id } });
|
||||||
}
|
} else {
|
||||||
|
await this.jobRepository.add({ name: JobName.EXIF_EXTRACTION, data: { asset, fileName: asset.id } });
|
||||||
return assetWithNoSmartInfo.length;
|
}
|
||||||
}
|
}
|
||||||
|
return assets.length;
|
||||||
private async runVideoConversionJob(): Promise<number> {
|
}
|
||||||
const jobCount = await this.videoConversionQueue.getJobCounts();
|
|
||||||
|
case QueueName.THUMBNAIL_GENERATION: {
|
||||||
if (jobCount.waiting > 0) {
|
const assets = await this._assetRepository.getAssetWithNoThumbnail();
|
||||||
throw new BadRequestException('Video conversion job is already running');
|
for (const asset of assets) {
|
||||||
}
|
await this.jobRepository.add({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { asset } });
|
||||||
|
}
|
||||||
const assetsWithNoConvertedVideo = await this._assetRepository.getAssetWithNoEncodedVideo();
|
return assets.length;
|
||||||
|
}
|
||||||
for (const asset of assetsWithNoConvertedVideo) {
|
|
||||||
await this.videoConversionQueue.add(JobName.VIDEO_CONVERSION, { asset });
|
default:
|
||||||
}
|
return 0;
|
||||||
|
}
|
||||||
return assetsWithNoConvertedVideo.length;
|
}
|
||||||
}
|
|
||||||
|
private asQueueName(jobId: JobId) {
|
||||||
async runStorageMigration() {
|
switch (jobId) {
|
||||||
const jobCount = await this.configQueue.getJobCounts();
|
case JobId.THUMBNAIL_GENERATION:
|
||||||
|
return QueueName.THUMBNAIL_GENERATION;
|
||||||
if (jobCount.active > 0) {
|
|
||||||
throw new BadRequestException('Storage migration job is already running');
|
case JobId.METADATA_EXTRACTION:
|
||||||
}
|
return QueueName.METADATA_EXTRACTION;
|
||||||
|
|
||||||
await this.configQueue.add(JobName.TEMPLATE_MIGRATION, {});
|
case JobId.VIDEO_CONVERSION:
|
||||||
|
return QueueName.VIDEO_CONVERSION;
|
||||||
return 1;
|
|
||||||
|
case JobId.STORAGE_TEMPLATE_MIGRATION:
|
||||||
|
return QueueName.CONFIG;
|
||||||
|
|
||||||
|
case JobId.MACHINE_LEARNING:
|
||||||
|
return QueueName.MACHINE_LEARNING;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new BadRequestException(`Invalid job id: ${jobId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { JobId } from '../dto/get-job.dto';
|
||||||
|
|
||||||
export class JobCounts {
|
export class JobCounts {
|
||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
||||||
@ -12,35 +13,20 @@ export class JobCounts {
|
|||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
||||||
waiting!: number;
|
waiting!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AllJobStatusResponseDto {
|
export class AllJobStatusResponseDto {
|
||||||
isThumbnailGenerationActive!: boolean;
|
@ApiProperty({ type: JobCounts })
|
||||||
isMetadataExtractionActive!: boolean;
|
[JobId.THUMBNAIL_GENERATION]!: JobCounts;
|
||||||
isVideoConversionActive!: boolean;
|
|
||||||
isMachineLearningActive!: boolean;
|
|
||||||
isStorageMigrationActive!: boolean;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({ type: JobCounts })
|
||||||
type: JobCounts,
|
[JobId.METADATA_EXTRACTION]!: JobCounts;
|
||||||
})
|
|
||||||
thumbnailGenerationQueueCount!: JobCounts;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({ type: JobCounts })
|
||||||
type: JobCounts,
|
[JobId.VIDEO_CONVERSION]!: JobCounts;
|
||||||
})
|
|
||||||
metadataExtractionQueueCount!: JobCounts;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({ type: JobCounts })
|
||||||
type: JobCounts,
|
[JobId.MACHINE_LEARNING]!: JobCounts;
|
||||||
})
|
|
||||||
videoConversionQueueCount!: JobCounts;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({ type: JobCounts })
|
||||||
type: JobCounts,
|
[JobId.STORAGE_TEMPLATE_MIGRATION]!: JobCounts;
|
||||||
})
|
|
||||||
machineLearningQueueCount!: JobCounts;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
type: JobCounts,
|
|
||||||
})
|
|
||||||
storageMigrationQueueCount!: JobCounts;
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import Bull from 'bull';
|
|
||||||
|
|
||||||
export class JobStatusResponseDto {
|
|
||||||
isActive!: boolean;
|
|
||||||
queueCount!: Bull.JobCounts;
|
|
||||||
}
|
|
@ -1,12 +1,9 @@
|
|||||||
import { BullModule } from '@nestjs/bull';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { QueueName } from '@app/domain';
|
|
||||||
import { BackgroundTaskProcessor } from './background-task.processor';
|
import { BackgroundTaskProcessor } from './background-task.processor';
|
||||||
import { BackgroundTaskService } from './background-task.service';
|
import { BackgroundTaskService } from './background-task.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BullModule.registerQueue({ name: QueueName.BACKGROUND_TASK })],
|
|
||||||
providers: [BackgroundTaskService, BackgroundTaskProcessor],
|
providers: [BackgroundTaskService, BackgroundTaskProcessor],
|
||||||
exports: [BackgroundTaskService, BullModule],
|
exports: [BackgroundTaskService],
|
||||||
})
|
})
|
||||||
export class BackgroundTaskModule {}
|
export class BackgroundTaskModule {}
|
||||||
|
@ -2,12 +2,12 @@ import { assetUtils } from '@app/common/utils';
|
|||||||
import { Process, Processor } from '@nestjs/bull';
|
import { Process, Processor } from '@nestjs/bull';
|
||||||
import { Job } from 'bull';
|
import { Job } from 'bull';
|
||||||
import { JobName, QueueName } from '@app/domain';
|
import { JobName, QueueName } from '@app/domain';
|
||||||
import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto';
|
import { AssetEntity } from '@app/infra';
|
||||||
|
|
||||||
@Processor(QueueName.BACKGROUND_TASK)
|
@Processor(QueueName.BACKGROUND_TASK)
|
||||||
export class BackgroundTaskProcessor {
|
export class BackgroundTaskProcessor {
|
||||||
@Process(JobName.DELETE_FILE_ON_DISK)
|
@Process(JobName.DELETE_FILE_ON_DISK)
|
||||||
async deleteFileOnDisk(job: Job<{ assets: AssetResponseDto[] }>) {
|
async deleteFileOnDisk(job: Job<{ assets: AssetEntity[] }>) {
|
||||||
const { assets } = job.data;
|
const { assets } = job.data;
|
||||||
|
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
import { InjectQueue } from '@nestjs/bull/dist/decorators';
|
import { IJobRepository, JobName } from '@app/domain';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { AssetEntity } from '@app/infra';
|
||||||
import { Queue } from 'bull';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { JobName, QueueName } from '@app/domain';
|
|
||||||
import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BackgroundTaskService {
|
export class BackgroundTaskService {
|
||||||
constructor(
|
constructor(@Inject(IJobRepository) private jobRepository: IJobRepository) {}
|
||||||
@InjectQueue(QueueName.BACKGROUND_TASK)
|
|
||||||
private backgroundTaskQueue: Queue,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async deleteFileOnDisk(assets: AssetResponseDto[]) {
|
async deleteFileOnDisk(assets: AssetEntity[]) {
|
||||||
await this.backgroundTaskQueue.add(JobName.DELETE_FILE_ON_DISK, { assets });
|
await this.jobRepository.add({ name: JobName.DELETE_FILE_ON_DISK, data: { assets } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { IsNull, Not, Repository } from 'typeorm';
|
import { IsNull, Not, Repository } from 'typeorm';
|
||||||
import { AssetEntity, AssetType, ExifEntity, UserEntity } from '@app/infra';
|
import { AssetEntity, AssetType, ExifEntity, UserEntity } from '@app/infra';
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
|
||||||
import { Queue } from 'bull';
|
|
||||||
import { IMetadataExtractionJob, IVideoTranscodeJob, QueueName, JobName } from '@app/domain';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { IUserDeletionJob } from '@app/domain';
|
|
||||||
import { userUtils } from '@app/common';
|
import { userUtils } from '@app/common';
|
||||||
|
import { IJobRepository, JobName } from '@app/domain';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ScheduleTasksService {
|
export class ScheduleTasksService {
|
||||||
@ -22,17 +19,7 @@ export class ScheduleTasksService {
|
|||||||
@InjectRepository(ExifEntity)
|
@InjectRepository(ExifEntity)
|
||||||
private exifRepository: Repository<ExifEntity>,
|
private exifRepository: Repository<ExifEntity>,
|
||||||
|
|
||||||
@InjectQueue(QueueName.THUMBNAIL_GENERATION)
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
private thumbnailGeneratorQueue: Queue,
|
|
||||||
|
|
||||||
@InjectQueue(QueueName.VIDEO_CONVERSION)
|
|
||||||
private videoConversionQueue: Queue<IVideoTranscodeJob>,
|
|
||||||
|
|
||||||
@InjectQueue(QueueName.METADATA_EXTRACTION)
|
|
||||||
private metadataExtractionQueue: Queue<IMetadataExtractionJob>,
|
|
||||||
|
|
||||||
@InjectQueue(QueueName.USER_DELETION)
|
|
||||||
private userDeletionQueue: Queue<IUserDeletionJob>,
|
|
||||||
|
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
@ -51,7 +38,7 @@ export class ScheduleTasksService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
await this.thumbnailGeneratorQueue.add(JobName.GENERATE_WEBP_THUMBNAIL, { asset: asset });
|
await this.jobRepository.add({ name: JobName.GENERATE_WEBP_THUMBNAIL, data: { asset } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +56,7 @@ export class ScheduleTasksService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
await this.videoConversionQueue.add(JobName.VIDEO_CONVERSION, { asset });
|
await this.jobRepository.add({ name: JobName.VIDEO_CONVERSION, data: { asset } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,11 +74,11 @@ export class ScheduleTasksService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const exif of exifInfo) {
|
for (const exif of exifInfo) {
|
||||||
await this.metadataExtractionQueue.add(
|
await this.jobRepository.add({
|
||||||
JobName.REVERSE_GEOCODING,
|
name: JobName.REVERSE_GEOCODING,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
{ exifId: exif.id, latitude: exif.latitude!, longitude: exif.longitude! },
|
data: { exifId: exif.id, latitude: exif.latitude!, longitude: exif.longitude! },
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,9 +93,9 @@ export class ScheduleTasksService {
|
|||||||
|
|
||||||
for (const asset of exifAssets) {
|
for (const asset of exifAssets) {
|
||||||
if (asset.type === AssetType.VIDEO) {
|
if (asset.type === AssetType.VIDEO) {
|
||||||
await this.metadataExtractionQueue.add(JobName.EXTRACT_VIDEO_METADATA, { asset, fileName: asset.id });
|
await this.jobRepository.add({ name: JobName.EXTRACT_VIDEO_METADATA, data: { asset, fileName: asset.id } });
|
||||||
} else {
|
} else {
|
||||||
await this.metadataExtractionQueue.add(JobName.EXIF_EXTRACTION, { asset, fileName: asset.id });
|
await this.jobRepository.add({ name: JobName.EXIF_EXTRACTION, data: { asset, fileName: asset.id } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,7 +105,7 @@ export class ScheduleTasksService {
|
|||||||
const usersToDelete = await this.userRepository.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) } });
|
const usersToDelete = await this.userRepository.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) } });
|
||||||
for (const user of usersToDelete) {
|
for (const user of usersToDelete) {
|
||||||
if (userUtils.isReadyForDeletion(user)) {
|
if (userUtils.isReadyForDeletion(user)) {
|
||||||
await this.userDeletionQueue.add(JobName.USER_DELETION, { user });
|
await this.jobRepository.add({ name: JobName.USER_DELETION, data: { user } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
import { QueueName } from '@app/domain';
|
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
import { IJobRepository, JobName } from '@app/domain';
|
||||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
|
||||||
import { Queue } from 'bull';
|
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(() => resolve(), ms));
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicroservicesService implements OnModuleInit {
|
export class MicroservicesService implements OnModuleInit {
|
||||||
constructor(
|
constructor(@Inject(IJobRepository) private jobRepository: IJobRepository) {}
|
||||||
@InjectQueue(QueueName.CHECKSUM_GENERATION)
|
|
||||||
private generateChecksumQueue: Queue,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
// wait for migration
|
// wait for migration
|
||||||
await this.generateChecksumQueue.add({}, { delay: 10000 });
|
await sleep(10_000);
|
||||||
|
|
||||||
|
await this.jobRepository.add({ name: JobName.CHECKSUM_GENERATION });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { AssetEntity } from '@app/infra';
|
import { AssetEntity } from '@app/infra';
|
||||||
import { QueueName } from '@app/domain';
|
import { JobName, QueueName } from '@app/domain';
|
||||||
import { Process, Processor } from '@nestjs/bull';
|
import { Process, Processor } from '@nestjs/bull';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
@ -15,7 +15,7 @@ export class GenerateChecksumProcessor {
|
|||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process()
|
@Process(JobName.CHECKSUM_GENERATION)
|
||||||
async generateChecksum() {
|
async generateChecksum() {
|
||||||
const pageSize = 200;
|
const pageSize = 200;
|
||||||
let hasNext = true;
|
let hasNext = true;
|
||||||
|
@ -2721,40 +2721,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/jobs/{jobId}": {
|
"/jobs/{jobId}": {
|
||||||
"get": {
|
|
||||||
"operationId": "getJobStatus",
|
|
||||||
"description": "",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "jobId",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/JobId"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/JobStatusResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"Job"
|
|
||||||
],
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"put": {
|
"put": {
|
||||||
"operationId": "sendJobCommand",
|
"operationId": "sendJobCommand",
|
||||||
"description": "",
|
"description": "",
|
||||||
@ -4569,48 +4535,28 @@
|
|||||||
"AllJobStatusResponseDto": {
|
"AllJobStatusResponseDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"thumbnailGenerationQueueCount": {
|
"thumbnail-generation": {
|
||||||
"$ref": "#/components/schemas/JobCounts"
|
"$ref": "#/components/schemas/JobCounts"
|
||||||
},
|
},
|
||||||
"metadataExtractionQueueCount": {
|
"metadata-extraction": {
|
||||||
"$ref": "#/components/schemas/JobCounts"
|
"$ref": "#/components/schemas/JobCounts"
|
||||||
},
|
},
|
||||||
"videoConversionQueueCount": {
|
"video-conversion": {
|
||||||
"$ref": "#/components/schemas/JobCounts"
|
"$ref": "#/components/schemas/JobCounts"
|
||||||
},
|
},
|
||||||
"machineLearningQueueCount": {
|
"machine-learning": {
|
||||||
"$ref": "#/components/schemas/JobCounts"
|
"$ref": "#/components/schemas/JobCounts"
|
||||||
},
|
},
|
||||||
"storageMigrationQueueCount": {
|
"storage-template-migration": {
|
||||||
"$ref": "#/components/schemas/JobCounts"
|
"$ref": "#/components/schemas/JobCounts"
|
||||||
},
|
|
||||||
"isThumbnailGenerationActive": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"isMetadataExtractionActive": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"isVideoConversionActive": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"isMachineLearningActive": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"isStorageMigrationActive": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"thumbnailGenerationQueueCount",
|
"thumbnail-generation",
|
||||||
"metadataExtractionQueueCount",
|
"metadata-extraction",
|
||||||
"videoConversionQueueCount",
|
"video-conversion",
|
||||||
"machineLearningQueueCount",
|
"machine-learning",
|
||||||
"storageMigrationQueueCount",
|
"storage-template-migration"
|
||||||
"isThumbnailGenerationActive",
|
|
||||||
"isMetadataExtractionActive",
|
|
||||||
"isVideoConversionActive",
|
|
||||||
"isMachineLearningActive",
|
|
||||||
"isStorageMigrationActive"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"JobId": {
|
"JobId": {
|
||||||
@ -4623,21 +4569,6 @@
|
|||||||
"storage-template-migration"
|
"storage-template-migration"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"JobStatusResponseDto": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"isActive": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"queueCount": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"isActive",
|
|
||||||
"queueCount"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"JobCommand": {
|
"JobCommand": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
@ -24,4 +24,5 @@ export enum JobName {
|
|||||||
OBJECT_DETECTION = 'detect-object',
|
OBJECT_DETECTION = 'detect-object',
|
||||||
IMAGE_TAGGING = 'tag-image',
|
IMAGE_TAGGING = 'tag-image',
|
||||||
DELETE_FILE_ON_DISK = 'delete-file-on-disk',
|
DELETE_FILE_ON_DISK = 'delete-file-on-disk',
|
||||||
|
CHECKSUM_GENERATION = 'checksum-generation',
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,19 @@ import {
|
|||||||
IVideoConversionProcessor,
|
IVideoConversionProcessor,
|
||||||
IReverseGeocodingProcessor,
|
IReverseGeocodingProcessor,
|
||||||
IUserDeletionJob,
|
IUserDeletionJob,
|
||||||
|
IVideoLengthExtractionProcessor,
|
||||||
JpegGeneratorProcessor,
|
JpegGeneratorProcessor,
|
||||||
WebpGeneratorProcessor,
|
WebpGeneratorProcessor,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
import { JobName } from './job.constants';
|
import { JobName, QueueName } from './job.constants';
|
||||||
|
|
||||||
|
export interface JobCounts {
|
||||||
|
active: number;
|
||||||
|
completed: number;
|
||||||
|
failed: number;
|
||||||
|
delayed: number;
|
||||||
|
waiting: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type JobItem =
|
export type JobItem =
|
||||||
| { name: JobName.ASSET_UPLOADED; data: IAssetUploadedJob }
|
| { name: JobName.ASSET_UPLOADED; data: IAssetUploadedJob }
|
||||||
@ -21,6 +30,8 @@ export type JobItem =
|
|||||||
| { name: JobName.USER_DELETION; data: IUserDeletionJob }
|
| { name: JobName.USER_DELETION; data: IUserDeletionJob }
|
||||||
| { name: JobName.TEMPLATE_MIGRATION }
|
| { name: JobName.TEMPLATE_MIGRATION }
|
||||||
| { name: JobName.CONFIG_CHANGE }
|
| { name: JobName.CONFIG_CHANGE }
|
||||||
|
| { name: JobName.CHECKSUM_GENERATION }
|
||||||
|
| { name: JobName.EXTRACT_VIDEO_METADATA; data: IVideoLengthExtractionProcessor }
|
||||||
| { name: JobName.OBJECT_DETECTION; data: IMachineLearningJob }
|
| { name: JobName.OBJECT_DETECTION; data: IMachineLearningJob }
|
||||||
| { name: JobName.IMAGE_TAGGING; data: IMachineLearningJob }
|
| { name: JobName.IMAGE_TAGGING; data: IMachineLearningJob }
|
||||||
| { name: JobName.DELETE_FILE_ON_DISK; data: IDeleteFileOnDiskJob };
|
| { name: JobName.DELETE_FILE_ON_DISK; data: IDeleteFileOnDiskJob };
|
||||||
@ -28,5 +39,8 @@ export type JobItem =
|
|||||||
export const IJobRepository = 'IJobRepository';
|
export const IJobRepository = 'IJobRepository';
|
||||||
|
|
||||||
export interface IJobRepository {
|
export interface IJobRepository {
|
||||||
|
empty(name: QueueName): Promise<void>;
|
||||||
add(item: JobItem): Promise<void>;
|
add(item: JobItem): Promise<void>;
|
||||||
|
isActive(name: QueueName): Promise<boolean>;
|
||||||
|
getJobCounts(name: QueueName): Promise<JobCounts>;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,9 @@ import { IJobRepository } from '../src';
|
|||||||
|
|
||||||
export const newJobRepositoryMock = (): jest.Mocked<IJobRepository> => {
|
export const newJobRepositoryMock = (): jest.Mocked<IJobRepository> => {
|
||||||
return {
|
return {
|
||||||
|
empty: jest.fn(),
|
||||||
add: jest.fn().mockImplementation(() => Promise.resolve()),
|
add: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
|
isActive: jest.fn(),
|
||||||
|
getJobCounts: jest.fn(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,21 +1,110 @@
|
|||||||
import { IJobRepository, JobItem, JobName, QueueName } from '@app/domain';
|
import {
|
||||||
|
IAssetUploadedJob,
|
||||||
|
IJobRepository,
|
||||||
|
IMachineLearningJob,
|
||||||
|
IMetadataExtractionJob,
|
||||||
|
IUserDeletionJob,
|
||||||
|
IVideoTranscodeJob,
|
||||||
|
JobCounts,
|
||||||
|
JobItem,
|
||||||
|
JobName,
|
||||||
|
QueueName,
|
||||||
|
} from '@app/domain';
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
import { Logger } from '@nestjs/common';
|
import { BadRequestException, Logger } from '@nestjs/common';
|
||||||
import { Queue } from 'bull';
|
import { Queue } from 'bull';
|
||||||
|
|
||||||
export class JobRepository implements IJobRepository {
|
export class JobRepository implements IJobRepository {
|
||||||
private logger = new Logger(JobRepository.name);
|
private logger = new Logger(JobRepository.name);
|
||||||
|
|
||||||
constructor(@InjectQueue(QueueName.CONFIG) private configQueue: Queue) {}
|
constructor(
|
||||||
|
@InjectQueue(QueueName.ASSET_UPLOADED) private assetUploaded: Queue<IAssetUploadedJob>,
|
||||||
|
@InjectQueue(QueueName.BACKGROUND_TASK) private backgroundTask: Queue,
|
||||||
|
@InjectQueue(QueueName.CHECKSUM_GENERATION) private generateChecksum: Queue,
|
||||||
|
@InjectQueue(QueueName.MACHINE_LEARNING) private machineLearning: Queue<IMachineLearningJob>,
|
||||||
|
@InjectQueue(QueueName.METADATA_EXTRACTION) private metadataExtraction: Queue<IMetadataExtractionJob>,
|
||||||
|
@InjectQueue(QueueName.CONFIG) private storageMigration: Queue,
|
||||||
|
@InjectQueue(QueueName.THUMBNAIL_GENERATION) private thumbnail: Queue,
|
||||||
|
@InjectQueue(QueueName.USER_DELETION) private userDeletion: Queue<IUserDeletionJob>,
|
||||||
|
@InjectQueue(QueueName.VIDEO_CONVERSION) private videoTranscode: Queue<IVideoTranscodeJob>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async isActive(name: QueueName): Promise<boolean> {
|
||||||
|
const counts = await this.getJobCounts(name);
|
||||||
|
return !!counts.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
empty(name: QueueName) {
|
||||||
|
return this.getQueue(name).empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
getJobCounts(name: QueueName): Promise<JobCounts> {
|
||||||
|
return this.getQueue(name).getJobCounts();
|
||||||
|
}
|
||||||
|
|
||||||
async add(item: JobItem): Promise<void> {
|
async add(item: JobItem): Promise<void> {
|
||||||
switch (item.name) {
|
switch (item.name) {
|
||||||
case JobName.CONFIG_CHANGE:
|
case JobName.ASSET_UPLOADED:
|
||||||
await this.configQueue.add(JobName.CONFIG_CHANGE, {});
|
await this.assetUploaded.add(item.name, item.data, { jobId: item.data.asset.id });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case JobName.DELETE_FILE_ON_DISK:
|
||||||
|
await this.backgroundTask.add(item.name, item.data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JobName.CHECKSUM_GENERATION:
|
||||||
|
await this.generateChecksum.add(item.name, {});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JobName.OBJECT_DETECTION:
|
||||||
|
case JobName.IMAGE_TAGGING:
|
||||||
|
await this.machineLearning.add(item.name, item.data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JobName.EXIF_EXTRACTION:
|
||||||
|
case JobName.EXTRACT_VIDEO_METADATA:
|
||||||
|
case JobName.REVERSE_GEOCODING:
|
||||||
|
await this.metadataExtraction.add(item.name, item.data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JobName.TEMPLATE_MIGRATION:
|
||||||
|
case JobName.CONFIG_CHANGE:
|
||||||
|
await this.storageMigration.add(item.name, {});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JobName.GENERATE_JPEG_THUMBNAIL:
|
||||||
|
case JobName.GENERATE_WEBP_THUMBNAIL:
|
||||||
|
await this.thumbnail.add(item.name, item.data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JobName.USER_DELETION:
|
||||||
|
await this.userDeletion.add(item.name, item.data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JobName.VIDEO_CONVERSION:
|
||||||
|
await this.videoTranscode.add(item.name, item.data);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// TODO inject remaining queues and map job to queue
|
// TODO inject remaining queues and map job to queue
|
||||||
this.logger.error('Invalid job', item);
|
this.logger.error('Invalid job', item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getQueue(name: QueueName) {
|
||||||
|
switch (name) {
|
||||||
|
case QueueName.THUMBNAIL_GENERATION:
|
||||||
|
return this.thumbnail;
|
||||||
|
case QueueName.METADATA_EXTRACTION:
|
||||||
|
return this.metadataExtraction;
|
||||||
|
case QueueName.VIDEO_CONVERSION:
|
||||||
|
return this.videoTranscode;
|
||||||
|
case QueueName.CONFIG:
|
||||||
|
return this.storageMigration;
|
||||||
|
case QueueName.MACHINE_LEARNING:
|
||||||
|
return this.machineLearning;
|
||||||
|
default:
|
||||||
|
throw new BadRequestException('Invalid job name');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
143
web/src/api/open-api/api.ts
generated
143
web/src/api/open-api/api.ts
generated
@ -14,23 +14,12 @@
|
|||||||
|
|
||||||
|
|
||||||
import { Configuration } from './configuration';
|
import { Configuration } from './configuration';
|
||||||
import globalAxios, {AxiosInstance, AxiosPromise, AxiosRequestConfig} from 'axios';
|
import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||||
// Some imports not used depending on template conditions
|
// Some imports not used depending on template conditions
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import {
|
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common';
|
||||||
assertParamExists,
|
|
||||||
createRequestFunction,
|
|
||||||
DUMMY_BASE_URL,
|
|
||||||
serializeDataIfNeeded,
|
|
||||||
setApiKeyToObject,
|
|
||||||
setBasicAuthToObject,
|
|
||||||
setBearerAuthToObject,
|
|
||||||
setOAuthToObject,
|
|
||||||
setSearchParams,
|
|
||||||
toPathString
|
|
||||||
} from './common';
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import {BASE_PATH, BaseAPI, COLLECTION_FORMATS, RequestArgs, RequiredError} from './base';
|
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from './base';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -293,61 +282,31 @@ export interface AllJobStatusResponseDto {
|
|||||||
* @type {JobCounts}
|
* @type {JobCounts}
|
||||||
* @memberof AllJobStatusResponseDto
|
* @memberof AllJobStatusResponseDto
|
||||||
*/
|
*/
|
||||||
'thumbnailGenerationQueueCount': JobCounts;
|
'thumbnail-generation': JobCounts;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {JobCounts}
|
* @type {JobCounts}
|
||||||
* @memberof AllJobStatusResponseDto
|
* @memberof AllJobStatusResponseDto
|
||||||
*/
|
*/
|
||||||
'metadataExtractionQueueCount': JobCounts;
|
'metadata-extraction': JobCounts;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {JobCounts}
|
* @type {JobCounts}
|
||||||
* @memberof AllJobStatusResponseDto
|
* @memberof AllJobStatusResponseDto
|
||||||
*/
|
*/
|
||||||
'videoConversionQueueCount': JobCounts;
|
'video-conversion': JobCounts;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {JobCounts}
|
* @type {JobCounts}
|
||||||
* @memberof AllJobStatusResponseDto
|
* @memberof AllJobStatusResponseDto
|
||||||
*/
|
*/
|
||||||
'machineLearningQueueCount': JobCounts;
|
'machine-learning': JobCounts;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {JobCounts}
|
* @type {JobCounts}
|
||||||
* @memberof AllJobStatusResponseDto
|
* @memberof AllJobStatusResponseDto
|
||||||
*/
|
*/
|
||||||
'storageMigrationQueueCount': JobCounts;
|
'storage-template-migration': JobCounts;
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
* @memberof AllJobStatusResponseDto
|
|
||||||
*/
|
|
||||||
'isThumbnailGenerationActive': boolean;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
* @memberof AllJobStatusResponseDto
|
|
||||||
*/
|
|
||||||
'isMetadataExtractionActive': boolean;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
* @memberof AllJobStatusResponseDto
|
|
||||||
*/
|
|
||||||
'isVideoConversionActive': boolean;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
* @memberof AllJobStatusResponseDto
|
|
||||||
*/
|
|
||||||
'isMachineLearningActive': boolean;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
* @memberof AllJobStatusResponseDto
|
|
||||||
*/
|
|
||||||
'isStorageMigrationActive': boolean;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -1269,25 +1228,6 @@ export const JobId = {
|
|||||||
export type JobId = typeof JobId[keyof typeof JobId];
|
export type JobId = typeof JobId[keyof typeof JobId];
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @interface JobStatusResponseDto
|
|
||||||
*/
|
|
||||||
export interface JobStatusResponseDto {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
* @memberof JobStatusResponseDto
|
|
||||||
*/
|
|
||||||
'isActive': boolean;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {object}
|
|
||||||
* @memberof JobStatusResponseDto
|
|
||||||
*/
|
|
||||||
'queueCount': object;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -5772,43 +5712,6 @@ export const JobApiAxiosParamCreator = function (configuration?: Configuration)
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: toPathString(localVarUrlObj),
|
|
||||||
options: localVarRequestOptions,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {JobId} jobId
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
getJobStatus: async (jobId: JobId, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
|
||||||
// verify required parameter 'jobId' is not null or undefined
|
|
||||||
assertParamExists('getJobStatus', 'jobId', jobId)
|
|
||||||
const localVarPath = `/jobs/{jobId}`
|
|
||||||
.replace(`{${"jobId"}}`, encodeURIComponent(String(jobId)));
|
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
|
||||||
let baseOptions;
|
|
||||||
if (configuration) {
|
|
||||||
baseOptions = configuration.baseOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
|
||||||
const localVarHeaderParameter = {} as any;
|
|
||||||
const localVarQueryParameter = {} as any;
|
|
||||||
|
|
||||||
// authentication bearer required
|
|
||||||
// http bearer authentication required
|
|
||||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
@ -5880,16 +5783,6 @@ export const JobApiFp = function(configuration?: Configuration) {
|
|||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllJobsStatus(options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllJobsStatus(options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {JobId} jobId
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
async getJobStatus(jobId: JobId, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<JobStatusResponseDto>> {
|
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getJobStatus(jobId, options);
|
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {JobId} jobId
|
* @param {JobId} jobId
|
||||||
@ -5919,15 +5812,6 @@ export const JobApiFactory = function (configuration?: Configuration, basePath?:
|
|||||||
getAllJobsStatus(options?: any): AxiosPromise<AllJobStatusResponseDto> {
|
getAllJobsStatus(options?: any): AxiosPromise<AllJobStatusResponseDto> {
|
||||||
return localVarFp.getAllJobsStatus(options).then((request) => request(axios, basePath));
|
return localVarFp.getAllJobsStatus(options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {JobId} jobId
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
getJobStatus(jobId: JobId, options?: any): AxiosPromise<JobStatusResponseDto> {
|
|
||||||
return localVarFp.getJobStatus(jobId, options).then((request) => request(axios, basePath));
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {JobId} jobId
|
* @param {JobId} jobId
|
||||||
@ -5958,17 +5842,6 @@ export class JobApi extends BaseAPI {
|
|||||||
return JobApiFp(this.configuration).getAllJobsStatus(options).then((request) => request(this.axios, this.basePath));
|
return JobApiFp(this.configuration).getAllJobsStatus(options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {JobId} jobId
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
* @memberof JobApi
|
|
||||||
*/
|
|
||||||
public getJobStatus(jobId: JobId, options?: AxiosRequestConfig) {
|
|
||||||
return JobApiFp(this.configuration).getJobStatus(jobId, options).then((request) => request(this.axios, this.basePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {JobId} jobId
|
* @param {JobId} jobId
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { JobCounts } from '@api';
|
||||||
|
|
||||||
export let title: string;
|
export let title: string;
|
||||||
export let subtitle: string;
|
export let subtitle: string;
|
||||||
export let buttonTitle = 'Run';
|
export let buttonTitle = 'Run';
|
||||||
export let jobStatus: boolean;
|
export let jobCounts: JobCounts;
|
||||||
export let waitingJobCount: number;
|
|
||||||
export let activeJobCount: number;
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
</script>
|
</script>
|
||||||
@ -36,17 +35,23 @@
|
|||||||
class="overflow-y-auto rounded-md w-full max-h-[320px] block border bg-white dark:border-immich-dark-gray dark:bg-immich-dark-gray/75 dark:text-immich-dark-fg"
|
class="overflow-y-auto rounded-md w-full max-h-[320px] block border bg-white dark:border-immich-dark-gray dark:bg-immich-dark-gray/75 dark:text-immich-dark-fg"
|
||||||
>
|
>
|
||||||
<tr class="text-center flex place-items-center w-full h-[60px]">
|
<tr class="text-center flex place-items-center w-full h-[60px]">
|
||||||
<td class="text-sm px-2 w-1/3 text-ellipsis">{jobStatus ? 'Active' : 'Idle'}</td>
|
<td class="text-sm px-2 w-1/3 text-ellipsis">
|
||||||
<td class="flex justify-center text-sm px-2 w-1/3 text-ellipsis">
|
{#if jobCounts}
|
||||||
{#if activeJobCount !== undefined}
|
<span>{jobCounts.active > 0 || jobCounts.waiting > 0 ? 'Active' : 'Idle'}</span>
|
||||||
{activeJobCount}
|
|
||||||
{:else}
|
{:else}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="flex justify-center text-sm px-2 w-1/3 text-ellipsis">
|
<td class="flex justify-center text-sm px-2 w-1/3 text-ellipsis">
|
||||||
{#if waitingJobCount !== undefined}
|
{#if jobCounts.active !== undefined}
|
||||||
{waitingJobCount}
|
{jobCounts.active}
|
||||||
|
{:else}
|
||||||
|
<LoadingSpinner />
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td class="flex justify-center text-sm px-2 w-1/3 text-ellipsis">
|
||||||
|
{#if jobCounts.waiting !== undefined}
|
||||||
|
{jobCounts.waiting}
|
||||||
{:else}
|
{:else}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
@ -59,9 +64,9 @@
|
|||||||
<button
|
<button
|
||||||
on:click={() => dispatch('click')}
|
on:click={() => dispatch('click')}
|
||||||
class="px-6 py-3 text-sm bg-immich-primary dark:bg-immich-dark-primary font-medium rounded-2xl hover:bg-immich-primary/50 transition-all hover:cursor-pointer disabled:cursor-not-allowed shadow-sm text-immich-bg dark:text-immich-dark-gray"
|
class="px-6 py-3 text-sm bg-immich-primary dark:bg-immich-dark-primary font-medium rounded-2xl hover:bg-immich-primary/50 transition-all hover:cursor-pointer disabled:cursor-not-allowed shadow-sm text-immich-bg dark:text-immich-dark-gray"
|
||||||
disabled={jobStatus}
|
disabled={jobCounts.active > 0 && jobCounts.waiting > 0}
|
||||||
>
|
>
|
||||||
{#if jobStatus}
|
{#if jobCounts.active > 0 || jobCounts.waiting > 0}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{:else}
|
{:else}
|
||||||
{buttonTitle}
|
{buttonTitle}
|
||||||
|
@ -8,175 +8,64 @@
|
|||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import JobTile from './job-tile.svelte';
|
import JobTile from './job-tile.svelte';
|
||||||
|
|
||||||
let allJobsStatus: AllJobStatusResponseDto;
|
let jobs: AllJobStatusResponseDto;
|
||||||
let setIntervalHandler: NodeJS.Timer;
|
let timer: NodeJS.Timer;
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
const { data } = await api.jobApi.getAllJobsStatus();
|
||||||
|
jobs = data;
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const { data } = await api.jobApi.getAllJobsStatus();
|
await load();
|
||||||
allJobsStatus = data;
|
timer = setInterval(async () => await load(), 5_000);
|
||||||
|
|
||||||
setIntervalHandler = setInterval(async () => {
|
|
||||||
const { data } = await api.jobApi.getAllJobsStatus();
|
|
||||||
allJobsStatus = data;
|
|
||||||
}, 1000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
clearInterval(setIntervalHandler);
|
clearInterval(timer);
|
||||||
});
|
});
|
||||||
|
|
||||||
const runThumbnailGeneration = async () => {
|
const run = async (jobId: JobId, jobName: string, emptyMessage: string) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await api.jobApi.sendJobCommand(JobId.ThumbnailGeneration, {
|
const { data } = await api.jobApi.sendJobCommand(jobId, { command: JobCommand.Start });
|
||||||
command: JobCommand.Start
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `Thumbnail generation job started for ${data} assets`,
|
message: `Started ${jobName}`,
|
||||||
type: NotificationType.Info
|
type: NotificationType.Info
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notificationController.show({
|
notificationController.show({ message: emptyMessage, type: NotificationType.Info });
|
||||||
message: `No missing thumbnails found`,
|
|
||||||
type: NotificationType.Info
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('[ERROR] runThumbnailGeneration', e);
|
|
||||||
|
|
||||||
notificationController.show({
|
|
||||||
message: `Error running thumbnail generation job, check console for more detail`,
|
|
||||||
type: NotificationType.Error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const runExtractEXIF = async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await api.jobApi.sendJobCommand(JobId.MetadataExtraction, {
|
|
||||||
command: JobCommand.Start
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
notificationController.show({
|
|
||||||
message: `Extract EXIF job started for ${data} assets`,
|
|
||||||
type: NotificationType.Info
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
notificationController.show({
|
|
||||||
message: `No missing EXIF found`,
|
|
||||||
type: NotificationType.Info
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('[ERROR] runExtractEXIF', e);
|
|
||||||
|
|
||||||
notificationController.show({
|
|
||||||
message: `Error running extract EXIF job, check console for more detail`,
|
|
||||||
type: NotificationType.Error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const runMachineLearning = async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await api.jobApi.sendJobCommand(JobId.MachineLearning, {
|
|
||||||
command: JobCommand.Start
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
notificationController.show({
|
|
||||||
message: `Object detection job started for ${data} assets`,
|
|
||||||
type: NotificationType.Info
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
notificationController.show({
|
|
||||||
message: `No missing object detection found`,
|
|
||||||
type: NotificationType.Info
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Error running machine learning job, check console for more detail`);
|
handleError(error, `Unable to start ${jobName}`);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const runVideoConversion = async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await api.jobApi.sendJobCommand(JobId.VideoConversion, {
|
|
||||||
command: JobCommand.Start
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
notificationController.show({
|
|
||||||
message: `Video conversion job started for ${data} assets`,
|
|
||||||
type: NotificationType.Info
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
notificationController.show({
|
|
||||||
message: `No videos without an encoded version found`,
|
|
||||||
type: NotificationType.Info
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, `Error running video conversion job, check console for more detail`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const runTemplateMigration = async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await api.jobApi.sendJobCommand(JobId.StorageTemplateMigration, {
|
|
||||||
command: JobCommand.Start
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
notificationController.show({
|
|
||||||
message: `Storage migration started`,
|
|
||||||
type: NotificationType.Info
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
notificationController.show({
|
|
||||||
message: `All files have been migrated to the new storage template`,
|
|
||||||
type: NotificationType.Info
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('[ERROR] runTemplateMigration', e);
|
|
||||||
|
|
||||||
notificationController.show({
|
|
||||||
message: `Error running template migration job, check console for more detail`,
|
|
||||||
type: NotificationType.Error
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-10">
|
<div class="flex flex-col gap-10">
|
||||||
|
{#if jobs}
|
||||||
<JobTile
|
<JobTile
|
||||||
title={'Generate thumbnails'}
|
title={'Generate thumbnails'}
|
||||||
subtitle={'Regenerate missing thumbnail (JPEG, WEBP)'}
|
subtitle={'Regenerate missing thumbnail (JPEG, WEBP)'}
|
||||||
on:click={runThumbnailGeneration}
|
on:click={() =>
|
||||||
jobStatus={allJobsStatus?.isThumbnailGenerationActive}
|
run(JobId.ThumbnailGeneration, 'thumbnail generation', 'No missing thumbnails found')}
|
||||||
waitingJobCount={allJobsStatus?.thumbnailGenerationQueueCount.waiting}
|
jobCounts={jobs[JobId.ThumbnailGeneration]}
|
||||||
activeJobCount={allJobsStatus?.thumbnailGenerationQueueCount.active}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<JobTile
|
<JobTile
|
||||||
title={'Extract EXIF'}
|
title={'Extract EXIF'}
|
||||||
subtitle={'Extract missing EXIF information'}
|
subtitle={'Extract missing EXIF information'}
|
||||||
on:click={runExtractEXIF}
|
on:click={() => run(JobId.MetadataExtraction, 'extract EXIF', 'No missing EXIF found')}
|
||||||
jobStatus={allJobsStatus?.isMetadataExtractionActive}
|
jobCounts={jobs[JobId.MetadataExtraction]}
|
||||||
waitingJobCount={allJobsStatus?.metadataExtractionQueueCount.waiting}
|
|
||||||
activeJobCount={allJobsStatus?.metadataExtractionQueueCount.active}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<JobTile
|
<JobTile
|
||||||
title={'Detect objects'}
|
title={'Detect objects'}
|
||||||
subtitle={'Run machine learning process to detect and classify objects'}
|
subtitle={'Run machine learning process to detect and classify objects'}
|
||||||
on:click={runMachineLearning}
|
on:click={() =>
|
||||||
jobStatus={allJobsStatus?.isMachineLearningActive}
|
run(JobId.MachineLearning, 'object detection', 'No missing object detection found')}
|
||||||
waitingJobCount={allJobsStatus?.machineLearningQueueCount.waiting}
|
jobCounts={jobs[JobId.MachineLearning]}
|
||||||
activeJobCount={allJobsStatus?.machineLearningQueueCount.active}
|
|
||||||
>
|
>
|
||||||
Note that some assets may not have any objects detected, this is normal.
|
Note that some assets may not have any objects detected, this is normal.
|
||||||
</JobTile>
|
</JobTile>
|
||||||
@ -184,21 +73,25 @@
|
|||||||
<JobTile
|
<JobTile
|
||||||
title={'Video transcoding'}
|
title={'Video transcoding'}
|
||||||
subtitle={'Run video transcoding process to transcode videos not in the desired format'}
|
subtitle={'Run video transcoding process to transcode videos not in the desired format'}
|
||||||
on:click={runVideoConversion}
|
on:click={() =>
|
||||||
jobStatus={allJobsStatus?.isVideoConversionActive}
|
run(
|
||||||
waitingJobCount={allJobsStatus?.videoConversionQueueCount.waiting}
|
JobId.VideoConversion,
|
||||||
activeJobCount={allJobsStatus?.videoConversionQueueCount.active}
|
'video conversion',
|
||||||
>
|
'No videos without an encoded version found'
|
||||||
Note that some videos won't require transcoding, this is normal.
|
)}
|
||||||
</JobTile>
|
jobCounts={jobs[JobId.MachineLearning]}
|
||||||
|
/>
|
||||||
|
|
||||||
<JobTile
|
<JobTile
|
||||||
title={'Storage migration'}
|
title={'Storage migration'}
|
||||||
subtitle={''}
|
subtitle={''}
|
||||||
on:click={runTemplateMigration}
|
on:click={() =>
|
||||||
jobStatus={allJobsStatus?.isStorageMigrationActive}
|
run(
|
||||||
waitingJobCount={allJobsStatus?.storageMigrationQueueCount.waiting}
|
JobId.StorageTemplateMigration,
|
||||||
activeJobCount={allJobsStatus?.storageMigrationQueueCount.active}
|
'storage template migration',
|
||||||
|
'All files have been migrated to the new storage template'
|
||||||
|
)}
|
||||||
|
jobCounts={jobs[JobId.StorageTemplateMigration]}
|
||||||
>
|
>
|
||||||
Apply the current
|
Apply the current
|
||||||
<a
|
<a
|
||||||
@ -207,4 +100,5 @@
|
|||||||
>
|
>
|
||||||
to previously uploaded assets
|
to previously uploaded assets
|
||||||
</JobTile>
|
</JobTile>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user