diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 62b938b3a9..56a94fb339 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -53,7 +53,6 @@ doc/JobCommand.md doc/JobCommandDto.md doc/JobCounts.md doc/JobId.md -doc/JobStatusResponseDto.md doc/LoginCredentialDto.md doc/LoginResponseDto.md doc/LogoutResponseDto.md @@ -162,7 +161,6 @@ lib/model/job_command.dart lib/model/job_command_dto.dart lib/model/job_counts.dart lib/model/job_id.dart -lib/model/job_status_response_dto.dart lib/model/login_credential_dto.dart lib/model/login_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_counts_test.dart test/job_id_test.dart -test/job_status_response_dto_test.dart test/login_credential_dto_test.dart test/login_response_dto_test.dart test/logout_response_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index d2a1a0c83b..30bcbc8b3f 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/AllJobStatusResponseDto.md b/mobile/openapi/doc/AllJobStatusResponseDto.md index 606375b8a6..66695ac2e1 100644 Binary files a/mobile/openapi/doc/AllJobStatusResponseDto.md and b/mobile/openapi/doc/AllJobStatusResponseDto.md differ diff --git a/mobile/openapi/doc/JobApi.md b/mobile/openapi/doc/JobApi.md index b2f2b4f7ae..11a6544e07 100644 Binary files a/mobile/openapi/doc/JobApi.md and b/mobile/openapi/doc/JobApi.md differ diff --git a/mobile/openapi/doc/JobStatusResponseDto.md b/mobile/openapi/doc/JobStatusResponseDto.md deleted file mode 100644 index 13325a5152..0000000000 Binary files a/mobile/openapi/doc/JobStatusResponseDto.md and /dev/null differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 70ff81ddf7..7948c769ea 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api/job_api.dart b/mobile/openapi/lib/api/job_api.dart index 2fafd44d31..5d53dfb919 100644 Binary files a/mobile/openapi/lib/api/job_api.dart and b/mobile/openapi/lib/api/job_api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 38bf12fb8f..417c8ecc2f 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/model/all_job_status_response_dto.dart b/mobile/openapi/lib/model/all_job_status_response_dto.dart index 9bf95e3702..60f788a302 100644 Binary files a/mobile/openapi/lib/model/all_job_status_response_dto.dart and b/mobile/openapi/lib/model/all_job_status_response_dto.dart differ diff --git a/mobile/openapi/lib/model/job_status_response_dto.dart b/mobile/openapi/lib/model/job_status_response_dto.dart deleted file mode 100644 index 2e6edb2938..0000000000 Binary files a/mobile/openapi/lib/model/job_status_response_dto.dart and /dev/null differ diff --git a/mobile/openapi/test/all_job_status_response_dto_test.dart b/mobile/openapi/test/all_job_status_response_dto_test.dart index fdea6f26e7..9405fd1f3d 100644 Binary files a/mobile/openapi/test/all_job_status_response_dto_test.dart and b/mobile/openapi/test/all_job_status_response_dto_test.dart differ diff --git a/mobile/openapi/test/job_api_test.dart b/mobile/openapi/test/job_api_test.dart index 5ff6fd7745..cfdfa6479c 100644 Binary files a/mobile/openapi/test/job_api_test.dart and b/mobile/openapi/test/job_api_test.dart differ diff --git a/mobile/openapi/test/job_status_response_dto_test.dart b/mobile/openapi/test/job_status_response_dto_test.dart deleted file mode 100644 index 09ea08df58..0000000000 Binary files a/mobile/openapi/test/job_status_response_dto_test.dart and /dev/null differ diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index 4f2d24a7c8..223fac9e78 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -295,7 +295,7 @@ export class AssetController { 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; } diff --git a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts index a07873d815..7a46efb6eb 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts @@ -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 { DownloadService } from '../../modules/download/download.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 { StorageService } from '@app/storage'; import { ISharedLinkRepository } from '../share/shared-link.repository'; +import { IJobRepository } from '@app/domain'; +import { newJobRepositoryMock } from '@app/domain/../test'; describe('AssetService', () => { let sui: AssetService; @@ -22,10 +22,9 @@ describe('AssetService', () => { let albumRepositoryMock: jest.Mocked; let downloadServiceMock: jest.Mocked>; let backgroundTaskServiceMock: jest.Mocked; - let assetUploadedQueueMock: jest.Mocked>; - let videoConversionQueueMock: jest.Mocked>; let storageSeriveMock: jest.Mocked; let sharedLinkRepositoryMock: jest.Mocked; + let jobMock: jest.Mocked; const authUser: AuthUserDto = Object.freeze({ id: 'user_id_1', email: 'auth@test.com', @@ -148,16 +147,17 @@ describe('AssetService', () => { getByIdAndUserId: jest.fn(), }; + jobMock = newJobRepositoryMock(); + sui = new AssetService( assetRepositoryMock, albumRepositoryMock, a, backgroundTaskServiceMock, - assetUploadedQueueMock, - videoConversionQueueMock, downloadServiceMock as DownloadService, storageSeriveMock, sharedLinkRepositoryMock, + jobMock, ); }); diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index c4977cd5bf..0f6438f0b0 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -43,9 +43,7 @@ import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-as import { UpdateAssetDto } from './dto/update-asset.dto'; import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto'; import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; -import { IAssetUploadedJob, IVideoTranscodeJob, JobName, QueueName } from '@app/domain'; -import { InjectQueue } from '@nestjs/bull'; -import { Queue } from 'bull'; +import { IJobRepository, JobName } from '@app/domain'; import { DownloadService } from '../../modules/download/download.service'; import { DownloadDto } from './dto/download-library.dto'; import { IAlbumRepository } from '../album/album-repository'; @@ -66,24 +64,14 @@ export class AssetService { constructor( @Inject(IAssetRepository) private _assetRepository: IAssetRepository, - @Inject(IAlbumRepository) private _albumRepository: IAlbumRepository, - @InjectRepository(AssetEntity) private assetRepository: Repository, - private backgroundTaskService: BackgroundTaskService, - - @InjectQueue(QueueName.ASSET_UPLOADED) - private assetUploadedQueue: Queue, - - @InjectQueue(QueueName.VIDEO_CONVERSION) - private videoConversionQueue: Queue, - private downloadService: DownloadService, - private storageService: StorageService, @Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository, + @Inject(IJobRepository) private jobRepository: IJobRepository, ) { this.shareCore = new ShareCore(sharedLinkRepository); } @@ -122,7 +110,7 @@ export class AssetService { 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( @@ -146,11 +134,10 @@ export class AssetService { const movedAsset = await this.storageService.moveAsset(assetEntity, originalAssetData.originalname); - await this.assetUploadedQueue.add( - JobName.ASSET_UPLOADED, - { asset: movedAsset, fileName: originalAssetData.originalname }, - { jobId: movedAsset.id }, - ); + await this.jobRepository.add({ + name: JobName.ASSET_UPLOADED, + data: { asset: movedAsset, fileName: originalAssetData.originalname }, + }); return new AssetFileUploadResponseDto(movedAsset.id); } catch (err) { diff --git a/server/apps/immich/src/api-v1/job/job.controller.ts b/server/apps/immich/src/api-v1/job/job.controller.ts index 5766ada3cf..5dcb3e7a0c 100644 --- a/server/apps/immich/src/api-v1/job/job.controller.ts +++ b/server/apps/immich/src/api-v1/job/job.controller.ts @@ -1,11 +1,9 @@ -import { Controller, Get, Body, ValidationPipe, Put, Param } from '@nestjs/common'; -import { JobService } from './job.service'; +import { Body, Controller, Get, Param, Put, ValidationPipe } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { Authenticated } from '../../decorators/authenticated.decorator'; import { AllJobStatusResponseDto } from './response-dto/all-job-status-response.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'; @Authenticated({ admin: true }) @@ -20,21 +18,16 @@ export class JobController { return this.jobService.getAllJobsStatus(); } - @Get('/:jobId') - getJobStatus(@Param(ValidationPipe) params: GetJobDto): Promise { - return this.jobService.getJobStatus(params); - } - @Put('/:jobId') async sendJobCommand( @Param(ValidationPipe) params: GetJobDto, @Body(ValidationPipe) body: JobCommandDto, ): Promise { if (body.command === 'start') { - return await this.jobService.startJob(params); + return await this.jobService.start(params.jobId); } if (body.command === 'stop') { - return await this.jobService.stopJob(params); + return await this.jobService.stop(params.jobId); } return 0; } diff --git a/server/apps/immich/src/api-v1/job/job.service.ts b/server/apps/immich/src/api-v1/job/job.service.ts index 4bba764bc4..ea45f7aca8 100644 --- a/server/apps/immich/src/api-v1/job/job.service.ts +++ b/server/apps/immich/src/api-v1/job/job.service.ts @@ -1,217 +1,118 @@ -import { - IMachineLearningJob, - IMetadataExtractionJob, - IThumbnailGenerationJob, - IVideoTranscodeJob, - JobName, - QueueName, -} from '@app/domain'; -import { InjectQueue } from '@nestjs/bull'; -import { Queue } from 'bull'; +import { JobName, IJobRepository, QueueName } from '@app/domain'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { AllJobStatusResponseDto } from './response-dto/all-job-status-response.dto'; import { IAssetRepository } from '../asset/asset-repository'; import { AssetType } from '@app/infra'; -import { GetJobDto, JobId } from './dto/get-job.dto'; -import { JobStatusResponseDto } from './response-dto/job-status-response.dto'; -import { StorageService } from '@app/storage'; +import { JobId } from './dto/get-job.dto'; import { MACHINE_LEARNING_ENABLED } from '@app/common'; +const jobIds = Object.values(JobId) as JobId[]; + @Injectable() export class JobService { constructor( - @InjectQueue(QueueName.THUMBNAIL_GENERATION) - private thumbnailGeneratorQueue: Queue, - - @InjectQueue(QueueName.METADATA_EXTRACTION) - private metadataExtractionQueue: Queue, - - @InjectQueue(QueueName.VIDEO_CONVERSION) - private videoConversionQueue: Queue, - - @InjectQueue(QueueName.MACHINE_LEARNING) - private machineLearningQueue: Queue, - - @InjectQueue(QueueName.CONFIG) - private configQueue: Queue, - - @Inject(IAssetRepository) - private _assetRepository: IAssetRepository, - - private storageService: StorageService, + @Inject(IAssetRepository) private _assetRepository: IAssetRepository, + @Inject(IJobRepository) private jobRepository: IJobRepository, ) { - this.thumbnailGeneratorQueue.empty(); - this.metadataExtractionQueue.empty(); - this.videoConversionQueue.empty(); - this.configQueue.empty(); + for (const jobId of jobIds) { + this.jobRepository.empty(this.asQueueName(jobId)); + } } - async startJob(jobDto: GetJobDto): Promise { - switch (jobDto.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'); - } + start(jobId: JobId): Promise { + return this.run(this.asQueueName(jobId)); + } + + async stop(jobId: JobId): Promise { + await this.jobRepository.empty(this.asQueueName(jobId)); + return 0; } async getAllJobsStatus(): Promise { - 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(); - response.isThumbnailGenerationActive = Boolean(thumbnailGeneratorJobCount.waiting); - response.thumbnailGenerationQueueCount = thumbnailGeneratorJobCount; - 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; - + for (const jobId of jobIds) { + response[jobId] = await this.jobRepository.getJobCounts(this.asQueueName(jobId)); + } return response; } - async getJobStatus(query: GetJobDto): Promise { - const response = new JobStatusResponseDto(); - if (query.jobId === JobId.THUMBNAIL_GENERATION) { - response.isActive = Boolean((await this.thumbnailGeneratorQueue.getJobCounts()).waiting); - response.queueCount = await this.thumbnailGeneratorQueue.getJobCounts(); + private async run(name: QueueName): Promise { + const isActive = await this.jobRepository.isActive(name); + if (isActive) { + throw new BadRequestException(`Job is already running`); } - if (query.jobId === JobId.METADATA_EXTRACTION) { - response.isActive = Boolean((await this.metadataExtractionQueue.getJobCounts()).waiting); - response.queueCount = await this.metadataExtractionQueue.getJobCounts(); - } + switch (name) { + case QueueName.VIDEO_CONVERSION: { + 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) { - response.isActive = Boolean((await this.videoConversionQueue.getJobCounts()).waiting); - response.queueCount = await this.videoConversionQueue.getJobCounts(); - } - - if (query.jobId === JobId.STORAGE_TEMPLATE_MIGRATION) { - response.isActive = Boolean((await this.configQueue.getJobCounts()).waiting); - response.queueCount = await this.configQueue.getJobCounts(); - } - - return response; - } - - async stopJob(query: GetJobDto): Promise { - 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 { - 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 { - 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 assets.length; } + + case QueueName.CONFIG: + await this.jobRepository.add({ name: JobName.TEMPLATE_MIGRATION }); + return 1; + + case QueueName.MACHINE_LEARNING: { + if (!MACHINE_LEARNING_ENABLED) { + throw new BadRequestException('Machine learning is not enabled.'); + } + + const assets = await this._assetRepository.getAssetWithNoSmartInfo(); + for (const asset of assets) { + await this.jobRepository.add({ name: JobName.IMAGE_TAGGING, data: { asset } }); + await this.jobRepository.add({ name: JobName.OBJECT_DETECTION, data: { asset } }); + } + return assets.length; + } + + case QueueName.METADATA_EXTRACTION: { + const assets = await this._assetRepository.getAssetWithNoEXIF(); + for (const asset of assets) { + if (asset.type === AssetType.VIDEO) { + 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 assets.length; + } + + case QueueName.THUMBNAIL_GENERATION: { + const assets = await this._assetRepository.getAssetWithNoThumbnail(); + for (const asset of assets) { + await this.jobRepository.add({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { asset } }); + } + return assets.length; + } + + default: + return 0; } - return assetsWithNoExif.length; } - private async runMachineLearningPipeline(): Promise { - if (!MACHINE_LEARNING_ENABLED) { - throw new BadRequestException('Machine learning is not enabled.'); + private asQueueName(jobId: JobId) { + switch (jobId) { + case JobId.THUMBNAIL_GENERATION: + return QueueName.THUMBNAIL_GENERATION; + + case JobId.METADATA_EXTRACTION: + return QueueName.METADATA_EXTRACTION; + + case JobId.VIDEO_CONVERSION: + return QueueName.VIDEO_CONVERSION; + + case JobId.STORAGE_TEMPLATE_MIGRATION: + return QueueName.CONFIG; + + case JobId.MACHINE_LEARNING: + return QueueName.MACHINE_LEARNING; + + default: + throw new BadRequestException(`Invalid job id: ${jobId}`); } - - const jobCount = await this.machineLearningQueue.getJobCounts(); - - if (jobCount.waiting > 0) { - throw new BadRequestException('Metadata extraction job is already running'); - } - - const assetWithNoSmartInfo = await this._assetRepository.getAssetWithNoSmartInfo(); - - for (const asset of assetWithNoSmartInfo) { - await this.machineLearningQueue.add(JobName.IMAGE_TAGGING, { asset }); - await this.machineLearningQueue.add(JobName.OBJECT_DETECTION, { asset }); - } - - return assetWithNoSmartInfo.length; - } - - private async runVideoConversionJob(): Promise { - const jobCount = await this.videoConversionQueue.getJobCounts(); - - if (jobCount.waiting > 0) { - throw new BadRequestException('Video conversion job is already running'); - } - - const assetsWithNoConvertedVideo = await this._assetRepository.getAssetWithNoEncodedVideo(); - - for (const asset of assetsWithNoConvertedVideo) { - await this.videoConversionQueue.add(JobName.VIDEO_CONVERSION, { asset }); - } - - return assetsWithNoConvertedVideo.length; - } - - async runStorageMigration() { - const jobCount = await this.configQueue.getJobCounts(); - - if (jobCount.active > 0) { - throw new BadRequestException('Storage migration job is already running'); - } - - await this.configQueue.add(JobName.TEMPLATE_MIGRATION, {}); - - return 1; } } diff --git a/server/apps/immich/src/api-v1/job/response-dto/all-job-status-response.dto.ts b/server/apps/immich/src/api-v1/job/response-dto/all-job-status-response.dto.ts index 06cbbdf2a5..9b26936ce6 100644 --- a/server/apps/immich/src/api-v1/job/response-dto/all-job-status-response.dto.ts +++ b/server/apps/immich/src/api-v1/job/response-dto/all-job-status-response.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { JobId } from '../dto/get-job.dto'; export class JobCounts { @ApiProperty({ type: 'integer' }) @@ -12,35 +13,20 @@ export class JobCounts { @ApiProperty({ type: 'integer' }) waiting!: number; } + export class AllJobStatusResponseDto { - isThumbnailGenerationActive!: boolean; - isMetadataExtractionActive!: boolean; - isVideoConversionActive!: boolean; - isMachineLearningActive!: boolean; - isStorageMigrationActive!: boolean; + @ApiProperty({ type: JobCounts }) + [JobId.THUMBNAIL_GENERATION]!: JobCounts; - @ApiProperty({ - type: JobCounts, - }) - thumbnailGenerationQueueCount!: JobCounts; + @ApiProperty({ type: JobCounts }) + [JobId.METADATA_EXTRACTION]!: JobCounts; - @ApiProperty({ - type: JobCounts, - }) - metadataExtractionQueueCount!: JobCounts; + @ApiProperty({ type: JobCounts }) + [JobId.VIDEO_CONVERSION]!: JobCounts; - @ApiProperty({ - type: JobCounts, - }) - videoConversionQueueCount!: JobCounts; + @ApiProperty({ type: JobCounts }) + [JobId.MACHINE_LEARNING]!: JobCounts; - @ApiProperty({ - type: JobCounts, - }) - machineLearningQueueCount!: JobCounts; - - @ApiProperty({ - type: JobCounts, - }) - storageMigrationQueueCount!: JobCounts; + @ApiProperty({ type: JobCounts }) + [JobId.STORAGE_TEMPLATE_MIGRATION]!: JobCounts; } diff --git a/server/apps/immich/src/api-v1/job/response-dto/job-status-response.dto.ts b/server/apps/immich/src/api-v1/job/response-dto/job-status-response.dto.ts deleted file mode 100644 index fe411fa2ef..0000000000 --- a/server/apps/immich/src/api-v1/job/response-dto/job-status-response.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Bull from 'bull'; - -export class JobStatusResponseDto { - isActive!: boolean; - queueCount!: Bull.JobCounts; -} diff --git a/server/apps/immich/src/modules/background-task/background-task.module.ts b/server/apps/immich/src/modules/background-task/background-task.module.ts index fc199abc54..fa52b6b44d 100644 --- a/server/apps/immich/src/modules/background-task/background-task.module.ts +++ b/server/apps/immich/src/modules/background-task/background-task.module.ts @@ -1,12 +1,9 @@ -import { BullModule } from '@nestjs/bull'; import { Module } from '@nestjs/common'; -import { QueueName } from '@app/domain'; import { BackgroundTaskProcessor } from './background-task.processor'; import { BackgroundTaskService } from './background-task.service'; @Module({ - imports: [BullModule.registerQueue({ name: QueueName.BACKGROUND_TASK })], providers: [BackgroundTaskService, BackgroundTaskProcessor], - exports: [BackgroundTaskService, BullModule], + exports: [BackgroundTaskService], }) export class BackgroundTaskModule {} diff --git a/server/apps/immich/src/modules/background-task/background-task.processor.ts b/server/apps/immich/src/modules/background-task/background-task.processor.ts index 7e68185689..0df0d0ada6 100644 --- a/server/apps/immich/src/modules/background-task/background-task.processor.ts +++ b/server/apps/immich/src/modules/background-task/background-task.processor.ts @@ -2,12 +2,12 @@ import { assetUtils } from '@app/common/utils'; import { Process, Processor } from '@nestjs/bull'; import { Job } from 'bull'; 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) export class BackgroundTaskProcessor { @Process(JobName.DELETE_FILE_ON_DISK) - async deleteFileOnDisk(job: Job<{ assets: AssetResponseDto[] }>) { + async deleteFileOnDisk(job: Job<{ assets: AssetEntity[] }>) { const { assets } = job.data; for (const asset of assets) { diff --git a/server/apps/immich/src/modules/background-task/background-task.service.ts b/server/apps/immich/src/modules/background-task/background-task.service.ts index a7124c8807..b32a89b266 100644 --- a/server/apps/immich/src/modules/background-task/background-task.service.ts +++ b/server/apps/immich/src/modules/background-task/background-task.service.ts @@ -1,17 +1,12 @@ -import { InjectQueue } from '@nestjs/bull/dist/decorators'; -import { Injectable } from '@nestjs/common'; -import { Queue } from 'bull'; -import { JobName, QueueName } from '@app/domain'; -import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto'; +import { IJobRepository, JobName } from '@app/domain'; +import { AssetEntity } from '@app/infra'; +import { Inject, Injectable } from '@nestjs/common'; @Injectable() export class BackgroundTaskService { - constructor( - @InjectQueue(QueueName.BACKGROUND_TASK) - private backgroundTaskQueue: Queue, - ) {} + constructor(@Inject(IJobRepository) private jobRepository: IJobRepository) {} - async deleteFileOnDisk(assets: AssetResponseDto[]) { - await this.backgroundTaskQueue.add(JobName.DELETE_FILE_ON_DISK, { assets }); + async deleteFileOnDisk(assets: AssetEntity[]) { + await this.jobRepository.add({ name: JobName.DELETE_FILE_ON_DISK, data: { assets } }); } } diff --git a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts index e1e92dcd2b..e77163bf02 100644 --- a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts +++ b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts @@ -1,14 +1,11 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { InjectRepository } from '@nestjs/typeorm'; import { IsNull, Not, Repository } from 'typeorm'; 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 { IUserDeletionJob } from '@app/domain'; import { userUtils } from '@app/common'; +import { IJobRepository, JobName } from '@app/domain'; @Injectable() export class ScheduleTasksService { @@ -22,17 +19,7 @@ export class ScheduleTasksService { @InjectRepository(ExifEntity) private exifRepository: Repository, - @InjectQueue(QueueName.THUMBNAIL_GENERATION) - private thumbnailGeneratorQueue: Queue, - - @InjectQueue(QueueName.VIDEO_CONVERSION) - private videoConversionQueue: Queue, - - @InjectQueue(QueueName.METADATA_EXTRACTION) - private metadataExtractionQueue: Queue, - - @InjectQueue(QueueName.USER_DELETION) - private userDeletionQueue: Queue, + @Inject(IJobRepository) private jobRepository: IJobRepository, private configService: ConfigService, ) {} @@ -51,7 +38,7 @@ export class ScheduleTasksService { } 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) { - 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) { - await this.metadataExtractionQueue.add( - JobName.REVERSE_GEOCODING, + await this.jobRepository.add({ + name: JobName.REVERSE_GEOCODING, // 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) { 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 { - 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()) } }); for (const user of usersToDelete) { if (userUtils.isReadyForDeletion(user)) { - await this.userDeletionQueue.add(JobName.USER_DELETION, { user }); + await this.jobRepository.add({ name: JobName.USER_DELETION, data: { user } }); } } } diff --git a/server/apps/microservices/src/microservices.service.ts b/server/apps/microservices/src/microservices.service.ts index 329f2b5d4d..a52928e3ab 100644 --- a/server/apps/microservices/src/microservices.service.ts +++ b/server/apps/microservices/src/microservices.service.ts @@ -1,17 +1,16 @@ -import { QueueName } from '@app/domain'; -import { InjectQueue } from '@nestjs/bull'; -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { Queue } from 'bull'; +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { IJobRepository, JobName } from '@app/domain'; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(() => resolve(), ms)); @Injectable() export class MicroservicesService implements OnModuleInit { - constructor( - @InjectQueue(QueueName.CHECKSUM_GENERATION) - private generateChecksumQueue: Queue, - ) {} + constructor(@Inject(IJobRepository) private jobRepository: IJobRepository) {} async onModuleInit() { // wait for migration - await this.generateChecksumQueue.add({}, { delay: 10000 }); + await sleep(10_000); + + await this.jobRepository.add({ name: JobName.CHECKSUM_GENERATION }); } } diff --git a/server/apps/microservices/src/processors/generate-checksum.processor.ts b/server/apps/microservices/src/processors/generate-checksum.processor.ts index 32c2035151..fc9fafbfcf 100644 --- a/server/apps/microservices/src/processors/generate-checksum.processor.ts +++ b/server/apps/microservices/src/processors/generate-checksum.processor.ts @@ -1,5 +1,5 @@ import { AssetEntity } from '@app/infra'; -import { QueueName } from '@app/domain'; +import { JobName, QueueName } from '@app/domain'; import { Process, Processor } from '@nestjs/bull'; import { Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; @@ -15,7 +15,7 @@ export class GenerateChecksumProcessor { private assetRepository: Repository, ) {} - @Process() + @Process(JobName.CHECKSUM_GENERATION) async generateChecksum() { const pageSize = 200; let hasNext = true; diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index d9f5f23ece..a1b42eb5ec 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -2721,40 +2721,6 @@ } }, "/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": { "operationId": "sendJobCommand", "description": "", @@ -4569,48 +4535,28 @@ "AllJobStatusResponseDto": { "type": "object", "properties": { - "thumbnailGenerationQueueCount": { + "thumbnail-generation": { "$ref": "#/components/schemas/JobCounts" }, - "metadataExtractionQueueCount": { + "metadata-extraction": { "$ref": "#/components/schemas/JobCounts" }, - "videoConversionQueueCount": { + "video-conversion": { "$ref": "#/components/schemas/JobCounts" }, - "machineLearningQueueCount": { + "machine-learning": { "$ref": "#/components/schemas/JobCounts" }, - "storageMigrationQueueCount": { + "storage-template-migration": { "$ref": "#/components/schemas/JobCounts" - }, - "isThumbnailGenerationActive": { - "type": "boolean" - }, - "isMetadataExtractionActive": { - "type": "boolean" - }, - "isVideoConversionActive": { - "type": "boolean" - }, - "isMachineLearningActive": { - "type": "boolean" - }, - "isStorageMigrationActive": { - "type": "boolean" } }, "required": [ - "thumbnailGenerationQueueCount", - "metadataExtractionQueueCount", - "videoConversionQueueCount", - "machineLearningQueueCount", - "storageMigrationQueueCount", - "isThumbnailGenerationActive", - "isMetadataExtractionActive", - "isVideoConversionActive", - "isMachineLearningActive", - "isStorageMigrationActive" + "thumbnail-generation", + "metadata-extraction", + "video-conversion", + "machine-learning", + "storage-template-migration" ] }, "JobId": { @@ -4623,21 +4569,6 @@ "storage-template-migration" ] }, - "JobStatusResponseDto": { - "type": "object", - "properties": { - "isActive": { - "type": "boolean" - }, - "queueCount": { - "type": "object" - } - }, - "required": [ - "isActive", - "queueCount" - ] - }, "JobCommand": { "type": "string", "enum": [ diff --git a/server/libs/domain/src/job/job.constants.ts b/server/libs/domain/src/job/job.constants.ts index dd5d060c44..992faf19f0 100644 --- a/server/libs/domain/src/job/job.constants.ts +++ b/server/libs/domain/src/job/job.constants.ts @@ -24,4 +24,5 @@ export enum JobName { OBJECT_DETECTION = 'detect-object', IMAGE_TAGGING = 'tag-image', DELETE_FILE_ON_DISK = 'delete-file-on-disk', + CHECKSUM_GENERATION = 'checksum-generation', } diff --git a/server/libs/domain/src/job/job.repository.ts b/server/libs/domain/src/job/job.repository.ts index fe129b7748..3e5bc7c18a 100644 --- a/server/libs/domain/src/job/job.repository.ts +++ b/server/libs/domain/src/job/job.repository.ts @@ -6,10 +6,19 @@ import { IVideoConversionProcessor, IReverseGeocodingProcessor, IUserDeletionJob, + IVideoLengthExtractionProcessor, JpegGeneratorProcessor, WebpGeneratorProcessor, } 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 = | { name: JobName.ASSET_UPLOADED; data: IAssetUploadedJob } @@ -21,6 +30,8 @@ export type JobItem = | { name: JobName.USER_DELETION; data: IUserDeletionJob } | { name: JobName.TEMPLATE_MIGRATION } | { name: JobName.CONFIG_CHANGE } + | { name: JobName.CHECKSUM_GENERATION } + | { name: JobName.EXTRACT_VIDEO_METADATA; data: IVideoLengthExtractionProcessor } | { name: JobName.OBJECT_DETECTION; data: IMachineLearningJob } | { name: JobName.IMAGE_TAGGING; data: IMachineLearningJob } | { name: JobName.DELETE_FILE_ON_DISK; data: IDeleteFileOnDiskJob }; @@ -28,5 +39,8 @@ export type JobItem = export const IJobRepository = 'IJobRepository'; export interface IJobRepository { + empty(name: QueueName): Promise; add(item: JobItem): Promise; + isActive(name: QueueName): Promise; + getJobCounts(name: QueueName): Promise; } diff --git a/server/libs/domain/test/job.repository.mock.ts b/server/libs/domain/test/job.repository.mock.ts index d75beed223..623f10fbf1 100644 --- a/server/libs/domain/test/job.repository.mock.ts +++ b/server/libs/domain/test/job.repository.mock.ts @@ -2,6 +2,9 @@ import { IJobRepository } from '../src'; export const newJobRepositoryMock = (): jest.Mocked => { return { + empty: jest.fn(), add: jest.fn().mockImplementation(() => Promise.resolve()), + isActive: jest.fn(), + getJobCounts: jest.fn(), }; }; diff --git a/server/libs/infra/src/job/job.repository.ts b/server/libs/infra/src/job/job.repository.ts index cdac756c54..dea3438de9 100644 --- a/server/libs/infra/src/job/job.repository.ts +++ b/server/libs/infra/src/job/job.repository.ts @@ -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 { Logger } from '@nestjs/common'; +import { BadRequestException, Logger } from '@nestjs/common'; import { Queue } from 'bull'; export class JobRepository implements IJobRepository { private logger = new Logger(JobRepository.name); - constructor(@InjectQueue(QueueName.CONFIG) private configQueue: Queue) {} + constructor( + @InjectQueue(QueueName.ASSET_UPLOADED) private assetUploaded: Queue, + @InjectQueue(QueueName.BACKGROUND_TASK) private backgroundTask: Queue, + @InjectQueue(QueueName.CHECKSUM_GENERATION) private generateChecksum: Queue, + @InjectQueue(QueueName.MACHINE_LEARNING) private machineLearning: Queue, + @InjectQueue(QueueName.METADATA_EXTRACTION) private metadataExtraction: Queue, + @InjectQueue(QueueName.CONFIG) private storageMigration: Queue, + @InjectQueue(QueueName.THUMBNAIL_GENERATION) private thumbnail: Queue, + @InjectQueue(QueueName.USER_DELETION) private userDeletion: Queue, + @InjectQueue(QueueName.VIDEO_CONVERSION) private videoTranscode: Queue, + ) {} + + async isActive(name: QueueName): Promise { + const counts = await this.getJobCounts(name); + return !!counts.active; + } + + empty(name: QueueName) { + return this.getQueue(name).empty(); + } + + getJobCounts(name: QueueName): Promise { + return this.getQueue(name).getJobCounts(); + } async add(item: JobItem): Promise { switch (item.name) { - case JobName.CONFIG_CHANGE: - await this.configQueue.add(JobName.CONFIG_CHANGE, {}); + case JobName.ASSET_UPLOADED: + await this.assetUploaded.add(item.name, item.data, { jobId: item.data.asset.id }); 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: // TODO inject remaining queues and map job to queue 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'); + } + } } diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index ba8b0e9f9c..a2e80b807e 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -13,24 +13,13 @@ */ -import {Configuration} from './configuration'; -import globalAxios, {AxiosInstance, AxiosPromise, AxiosRequestConfig} from 'axios'; +import { Configuration } from './configuration'; +import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; // Some imports not used depending on template conditions // @ts-ignore -import { - assertParamExists, - createRequestFunction, - DUMMY_BASE_URL, - serializeDataIfNeeded, - setApiKeyToObject, - setBasicAuthToObject, - setBearerAuthToObject, - setOAuthToObject, - setSearchParams, - toPathString -} from './common'; +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common'; // @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} * @memberof AllJobStatusResponseDto */ - 'thumbnailGenerationQueueCount': JobCounts; + 'thumbnail-generation': JobCounts; /** * * @type {JobCounts} * @memberof AllJobStatusResponseDto */ - 'metadataExtractionQueueCount': JobCounts; + 'metadata-extraction': JobCounts; /** * * @type {JobCounts} * @memberof AllJobStatusResponseDto */ - 'videoConversionQueueCount': JobCounts; + 'video-conversion': JobCounts; /** * * @type {JobCounts} * @memberof AllJobStatusResponseDto */ - 'machineLearningQueueCount': JobCounts; + 'machine-learning': JobCounts; /** * * @type {JobCounts} * @memberof AllJobStatusResponseDto */ - 'storageMigrationQueueCount': 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; + 'storage-template-migration': JobCounts; } /** * @@ -1269,25 +1228,6 @@ export const 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 @@ -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 => { - // 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); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -5880,16 +5783,6 @@ export const JobApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getAllJobsStatus(options); 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> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getJobStatus(jobId, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * * @param {JobId} jobId @@ -5919,15 +5812,6 @@ export const JobApiFactory = function (configuration?: Configuration, basePath?: getAllJobsStatus(options?: any): AxiosPromise { 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 { - return localVarFp.getJobStatus(jobId, options).then((request) => request(axios, basePath)); - }, /** * * @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)); } - /** - * - * @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 diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte index e23e33a525..134ad611fe 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -1,13 +1,12 @@ @@ -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" > - {jobStatus ? 'Active' : 'Idle'} - - {#if activeJobCount !== undefined} - {activeJobCount} + + {#if jobCounts} + {jobCounts.active > 0 || jobCounts.waiting > 0 ? 'Active' : 'Idle'} {:else} {/if} - {#if waitingJobCount !== undefined} - {waitingJobCount} + {#if jobCounts.active !== undefined} + {jobCounts.active} + {:else} + + {/if} + + + {#if jobCounts.waiting !== undefined} + {jobCounts.waiting} {:else} {/if} @@ -59,9 +64,9 @@