diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index a5d73e1474..3a8edc0030 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -8,6 +8,7 @@ doc/AdminSignupResponseDto.md doc/AlbumApi.md doc/AlbumCountResponseDto.md doc/AlbumResponseDto.md +doc/AllJobStatusResponseDto.md doc/AssetApi.md doc/AssetCountByTimeBucket.md doc/AssetCountByTimeBucketResponseDto.md @@ -33,6 +34,12 @@ doc/DeviceTypeEnum.md doc/ExifResponseDto.md doc/GetAssetByTimeBucketDto.md doc/GetAssetCountByTimeBucketDto.md +doc/JobApi.md +doc/JobCommand.md +doc/JobCommandDto.md +doc/JobCounts.md +doc/JobId.md +doc/JobStatusResponseDto.md doc/LoginCredentialDto.md doc/LoginResponseDto.md doc/LogoutResponseDto.md @@ -59,6 +66,7 @@ lib/api/album_api.dart lib/api/asset_api.dart lib/api/authentication_api.dart lib/api/device_info_api.dart +lib/api/job_api.dart lib/api/server_info_api.dart lib/api/user_api.dart lib/api_client.dart @@ -74,6 +82,7 @@ lib/model/add_users_dto.dart lib/model/admin_signup_response_dto.dart lib/model/album_count_response_dto.dart lib/model/album_response_dto.dart +lib/model/all_job_status_response_dto.dart lib/model/asset_count_by_time_bucket.dart lib/model/asset_count_by_time_bucket_response_dto.dart lib/model/asset_count_by_user_id_response_dto.dart @@ -96,6 +105,11 @@ lib/model/device_type_enum.dart lib/model/exif_response_dto.dart lib/model/get_asset_by_time_bucket_dto.dart lib/model/get_asset_count_by_time_bucket_dto.dart +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 diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 0d037e60b3..3154654d42 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 new file mode 100644 index 0000000000..3fa53791df Binary files /dev/null and b/mobile/openapi/doc/AllJobStatusResponseDto.md differ diff --git a/mobile/openapi/doc/CreateJobDto.md b/mobile/openapi/doc/CreateJobDto.md new file mode 100644 index 0000000000..64cdbf0184 Binary files /dev/null and b/mobile/openapi/doc/CreateJobDto.md differ diff --git a/mobile/openapi/doc/ExifResponseDto.md b/mobile/openapi/doc/ExifResponseDto.md index 0e96bdcbe9..af4bb349ec 100644 Binary files a/mobile/openapi/doc/ExifResponseDto.md and b/mobile/openapi/doc/ExifResponseDto.md differ diff --git a/mobile/openapi/doc/JobApi.md b/mobile/openapi/doc/JobApi.md new file mode 100644 index 0000000000..124e3d2149 Binary files /dev/null and b/mobile/openapi/doc/JobApi.md differ diff --git a/mobile/openapi/doc/JobCommand.md b/mobile/openapi/doc/JobCommand.md new file mode 100644 index 0000000000..620e0439a5 Binary files /dev/null and b/mobile/openapi/doc/JobCommand.md differ diff --git a/mobile/openapi/doc/JobCommandDto.md b/mobile/openapi/doc/JobCommandDto.md new file mode 100644 index 0000000000..4e87fde8e8 Binary files /dev/null and b/mobile/openapi/doc/JobCommandDto.md differ diff --git a/mobile/openapi/doc/JobCounts.md b/mobile/openapi/doc/JobCounts.md new file mode 100644 index 0000000000..195ed86a65 Binary files /dev/null and b/mobile/openapi/doc/JobCounts.md differ diff --git a/mobile/openapi/doc/JobId.md b/mobile/openapi/doc/JobId.md new file mode 100644 index 0000000000..d2f68234d0 Binary files /dev/null and b/mobile/openapi/doc/JobId.md differ diff --git a/mobile/openapi/doc/JobStatusResponseDto.md b/mobile/openapi/doc/JobStatusResponseDto.md new file mode 100644 index 0000000000..13325a5152 Binary files /dev/null and b/mobile/openapi/doc/JobStatusResponseDto.md differ diff --git a/mobile/openapi/doc/JobType.md b/mobile/openapi/doc/JobType.md new file mode 100644 index 0000000000..6d7faab6b7 Binary files /dev/null and b/mobile/openapi/doc/JobType.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 3c87fc703b..150d878f63 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 new file mode 100644 index 0000000000..b64a67c35a Binary files /dev/null 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 13cc028967..827332e9c3 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/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 26c90fd0c7..7db37768c4 100644 Binary files a/mobile/openapi/lib/api_helper.dart and b/mobile/openapi/lib/api_helper.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 new file mode 100644 index 0000000000..7be7166a77 Binary files /dev/null and b/mobile/openapi/lib/model/all_job_status_response_dto.dart differ diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 21dcce7595..cd1e83c5f2 100644 Binary files a/mobile/openapi/lib/model/asset_response_dto.dart and b/mobile/openapi/lib/model/asset_response_dto.dart differ diff --git a/mobile/openapi/lib/model/create_job_dto.dart b/mobile/openapi/lib/model/create_job_dto.dart new file mode 100644 index 0000000000..1eaf678647 Binary files /dev/null and b/mobile/openapi/lib/model/create_job_dto.dart differ diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart index 199c955e93..b81f0e347b 100644 Binary files a/mobile/openapi/lib/model/exif_response_dto.dart and b/mobile/openapi/lib/model/exif_response_dto.dart differ diff --git a/mobile/openapi/lib/model/job_command.dart b/mobile/openapi/lib/model/job_command.dart new file mode 100644 index 0000000000..2734028076 Binary files /dev/null and b/mobile/openapi/lib/model/job_command.dart differ diff --git a/mobile/openapi/lib/model/job_command_dto.dart b/mobile/openapi/lib/model/job_command_dto.dart new file mode 100644 index 0000000000..808eb50d75 Binary files /dev/null and b/mobile/openapi/lib/model/job_command_dto.dart differ diff --git a/mobile/openapi/lib/model/job_counts.dart b/mobile/openapi/lib/model/job_counts.dart new file mode 100644 index 0000000000..5c4f110f7e Binary files /dev/null and b/mobile/openapi/lib/model/job_counts.dart differ diff --git a/mobile/openapi/lib/model/job_id.dart b/mobile/openapi/lib/model/job_id.dart new file mode 100644 index 0000000000..308d9c06c1 Binary files /dev/null and b/mobile/openapi/lib/model/job_id.dart differ diff --git a/mobile/openapi/lib/model/job_status_response_dto.dart b/mobile/openapi/lib/model/job_status_response_dto.dart new file mode 100644 index 0000000000..d3854b8f3a Binary files /dev/null and b/mobile/openapi/lib/model/job_status_response_dto.dart differ diff --git a/mobile/openapi/lib/model/job_type.dart b/mobile/openapi/lib/model/job_type.dart new file mode 100644 index 0000000000..2cf21674ed Binary files /dev/null and b/mobile/openapi/lib/model/job_type.dart 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 new file mode 100644 index 0000000000..0853da9d1b Binary files /dev/null and b/mobile/openapi/test/all_job_status_response_dto_test.dart differ diff --git a/mobile/openapi/test/create_job_dto_test.dart b/mobile/openapi/test/create_job_dto_test.dart new file mode 100644 index 0000000000..5ae779231e Binary files /dev/null and b/mobile/openapi/test/create_job_dto_test.dart differ diff --git a/mobile/openapi/test/job_api_test.dart b/mobile/openapi/test/job_api_test.dart new file mode 100644 index 0000000000..2b8d82393c Binary files /dev/null and b/mobile/openapi/test/job_api_test.dart differ diff --git a/mobile/openapi/test/job_command_dto_test.dart b/mobile/openapi/test/job_command_dto_test.dart new file mode 100644 index 0000000000..fc31170277 Binary files /dev/null and b/mobile/openapi/test/job_command_dto_test.dart differ diff --git a/mobile/openapi/test/job_command_test.dart b/mobile/openapi/test/job_command_test.dart new file mode 100644 index 0000000000..df6822c9d4 Binary files /dev/null and b/mobile/openapi/test/job_command_test.dart differ diff --git a/mobile/openapi/test/job_counts_test.dart b/mobile/openapi/test/job_counts_test.dart new file mode 100644 index 0000000000..09fb4fc62f Binary files /dev/null and b/mobile/openapi/test/job_counts_test.dart differ diff --git a/mobile/openapi/test/job_id_test.dart b/mobile/openapi/test/job_id_test.dart new file mode 100644 index 0000000000..66b6b7656c Binary files /dev/null and b/mobile/openapi/test/job_id_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 new file mode 100644 index 0000000000..09ea08df58 Binary files /dev/null and b/mobile/openapi/test/job_status_response_dto_test.dart differ diff --git a/mobile/openapi/test/job_type_test.dart b/mobile/openapi/test/job_type_test.dart new file mode 100644 index 0000000000..d611a65570 Binary files /dev/null and b/mobile/openapi/test/job_type_test.dart differ diff --git a/server/.dockerignore b/server/.dockerignore index 834ab88b61..a66e51e358 100644 --- a/server/.dockerignore +++ b/server/.dockerignore @@ -1,4 +1,4 @@ node_modules/ upload/ dist/ - +.reverse-geocoding-dump diff --git a/server/apps/immich/src/api-v1/album/album.service.spec.ts b/server/apps/immich/src/api-v1/album/album.service.spec.ts index 672d39f9af..2ef7e5530f 100644 --- a/server/apps/immich/src/api-v1/album/album.service.spec.ts +++ b/server/apps/immich/src/api-v1/album/album.service.spec.ts @@ -134,6 +134,9 @@ describe('Album service', () => { getAssetByTimeBucket: jest.fn(), getAssetByChecksum: jest.fn(), getAssetCountByUserId: jest.fn(), + getAssetWithNoEXIF: jest.fn(), + getAssetWithNoThumbnail: jest.fn(), + getAssetWithNoSmartInfo: jest.fn(), }; sut = new AlbumService(albumRepositoryMock, assetRepositoryMock); diff --git a/server/apps/immich/src/api-v1/asset/asset-repository.ts b/server/apps/immich/src/api-v1/asset/asset-repository.ts index 3c88633231..089819af37 100644 --- a/server/apps/immich/src/api-v1/asset/asset-repository.ts +++ b/server/apps/immich/src/api-v1/asset/asset-repository.ts @@ -29,6 +29,9 @@ export interface IAssetRepository { getAssetCountByUserId(userId: string): Promise; getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise; getAssetByChecksum(userId: string, checksum: Buffer): Promise; + getAssetWithNoThumbnail(): Promise; + getAssetWithNoEXIF(): Promise; + getAssetWithNoSmartInfo(): Promise; } export const ASSET_REPOSITORY = 'ASSET_REPOSITORY'; @@ -40,6 +43,33 @@ export class AssetRepository implements IAssetRepository { private assetRepository: Repository, ) {} + async getAssetWithNoSmartInfo(): Promise { + return await this.assetRepository + .createQueryBuilder('asset') + .leftJoinAndSelect('asset.smartInfo', 'si') + .where('asset.resizePath IS NOT NULL') + .andWhere('si.id IS NULL') + .getMany(); + } + + async getAssetWithNoThumbnail(): Promise { + return await this.assetRepository + .createQueryBuilder('asset') + .where('asset.resizePath IS NULL') + .orWhere('asset.resizePath = :resizePath', { resizePath: '' }) + .orWhere('asset.webpPath IS NULL') + .orWhere('asset.webpPath = :webpPath', { webpPath: '' }) + .getMany(); + } + + async getAssetWithNoEXIF(): Promise { + return await this.assetRepository + .createQueryBuilder('asset') + .leftJoinAndSelect('asset.exifInfo', 'ei') + .where('ei."assetId" IS NULL') + .getMany(); + } + async getAssetCountByUserId(userId: string): Promise { // Get asset count by AssetType const res = await this.assetRepository 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 387671df3a..a045bdce45 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -30,7 +30,7 @@ import { CommunicationGateway } from '../communication/communication.gateway'; import { InjectQueue } from '@nestjs/bull'; import { Queue } from 'bull'; import { IAssetUploadedJob } from '@app/job/index'; -import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant'; +import { QueueNameEnum } from '@app/job/constants/queue-name.constant'; import { assetUploadedProcessorName } from '@app/job/constants/job-name.constant'; import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; @@ -59,7 +59,7 @@ export class AssetController { private assetService: AssetService, private backgroundTaskService: BackgroundTaskService, - @InjectQueue(assetUploadedQueueName) + @InjectQueue(QueueNameEnum.ASSET_UPLOADED) private assetUploadedQueue: Queue, ) {} diff --git a/server/apps/immich/src/api-v1/asset/asset.module.ts b/server/apps/immich/src/api-v1/asset/asset.module.ts index 13df9997f9..adc0705078 100644 --- a/server/apps/immich/src/api-v1/asset/asset.module.ts +++ b/server/apps/immich/src/api-v1/asset/asset.module.ts @@ -7,7 +7,7 @@ import { BullModule } from '@nestjs/bull'; import { BackgroundTaskModule } from '../../modules/background-task/background-task.module'; import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; import { CommunicationModule } from '../communication/communication.module'; -import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant'; +import { QueueNameEnum } from '@app/job/constants/queue-name.constant'; import { AssetRepository, ASSET_REPOSITORY } from './asset-repository'; @Module({ @@ -16,7 +16,7 @@ import { AssetRepository, ASSET_REPOSITORY } from './asset-repository'; BackgroundTaskModule, TypeOrmModule.forFeature([AssetEntity]), BullModule.registerQueue({ - name: assetUploadedQueueName, + name: QueueNameEnum.ASSET_UPLOADED, defaultJobOptions: { attempts: 3, removeOnComplete: true, 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 3b07d4f74f..89305dcd4b 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 @@ -107,6 +107,9 @@ describe('AssetService', () => { getAssetByTimeBucket: jest.fn(), getAssetByChecksum: jest.fn(), getAssetCountByUserId: jest.fn(), + getAssetWithNoEXIF: jest.fn(), + getAssetWithNoThumbnail: jest.fn(), + getAssetWithNoSmartInfo: jest.fn(), }; sui = new AssetService(assetRepositoryMock, a); diff --git a/server/apps/immich/src/api-v1/asset/response-dto/exif-response.dto.ts b/server/apps/immich/src/api-v1/asset/response-dto/exif-response.dto.ts index c43c55b4eb..ff86716eca 100644 --- a/server/apps/immich/src/api-v1/asset/response-dto/exif-response.dto.ts +++ b/server/apps/immich/src/api-v1/asset/response-dto/exif-response.dto.ts @@ -1,12 +1,16 @@ import { ExifEntity } from '@app/database/entities/exif.entity'; +import { ApiProperty } from '@nestjs/swagger'; export class ExifResponseDto { - id?: string | null = null; + @ApiProperty({ type: 'integer', format: 'int64' }) + id?: number | null = null; make?: string | null = null; model?: string | null = null; imageName?: string | null = null; exifImageWidth?: number | null = null; exifImageHeight?: number | null = null; + + @ApiProperty({ type: 'integer', format: 'int64' }) fileSizeInByte?: number | null = null; orientation?: string | null = null; dateTimeOriginal?: Date | null = null; @@ -25,13 +29,13 @@ export class ExifResponseDto { export function mapExif(entity: ExifEntity): ExifResponseDto { return { - id: entity.id, + id: parseInt(entity.id), make: entity.make, model: entity.model, imageName: entity.imageName, exifImageWidth: entity.exifImageWidth, exifImageHeight: entity.exifImageHeight, - fileSizeInByte: entity.fileSizeInByte, + fileSizeInByte: entity.fileSizeInByte ? parseInt(entity.fileSizeInByte.toString()) : null, orientation: entity.orientation, dateTimeOriginal: entity.dateTimeOriginal, modifyDate: entity.modifyDate, diff --git a/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts b/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts new file mode 100644 index 0000000000..38289ed134 --- /dev/null +++ b/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty } from 'class-validator'; + +export enum JobId { + THUMBNAIL_GENERATION = 'thumbnail-generation', + METADATA_EXTRACTION = 'metadata-extraction', + VIDEO_CONVERSION = 'video-conversion', + MACHINE_LEARNING = 'machine-learning', +} + +export class GetJobDto { + @IsNotEmpty() + @IsEnum(JobId, { + message: `params must be one of ${Object.values(JobId).join()}`, + }) + @ApiProperty({ + enum: JobId, + enumName: 'JobId', + }) + jobId!: string; +} diff --git a/server/apps/immich/src/api-v1/job/dto/job-command.dto.ts b/server/apps/immich/src/api-v1/job/dto/job-command.dto.ts new file mode 100644 index 0000000000..f63f0fa517 --- /dev/null +++ b/server/apps/immich/src/api-v1/job/dto/job-command.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsIn, IsNotEmpty } from 'class-validator'; + +export class JobCommandDto { + @IsNotEmpty() + @IsIn(['start', 'stop']) + @ApiProperty({ + enum: ['start', 'stop'], + enumName: 'JobCommand', + }) + command!: string; +} diff --git a/server/apps/immich/src/api-v1/job/job.controller.ts b/server/apps/immich/src/api-v1/job/job.controller.ts new file mode 100644 index 0000000000..2fbccb7fd8 --- /dev/null +++ b/server/apps/immich/src/api-v1/job/job.controller.ts @@ -0,0 +1,43 @@ +import { Controller, Get, Body, UseGuards, ValidationPipe, Put, Param } from '@nestjs/common'; +import { JobService } from './job.service'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; +import { AdminRolesGuard } from '../../middlewares/admin-role-guard.middleware'; +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 { JobCommandDto } from './dto/job-command.dto'; + +@UseGuards(JwtAuthGuard) +@UseGuards(AdminRolesGuard) +@ApiTags('Job') +@ApiBearerAuth() +@Controller('jobs') +export class JobController { + constructor(private readonly jobService: JobService) {} + + @Get() + getAllJobsStatus(): Promise { + 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); + } + if (body.command === 'stop') { + return await this.jobService.stopJob(params); + } + return 0; + } +} diff --git a/server/apps/immich/src/api-v1/job/job.module.ts b/server/apps/immich/src/api-v1/job/job.module.ts new file mode 100644 index 0000000000..2cb5beb7bf --- /dev/null +++ b/server/apps/immich/src/api-v1/job/job.module.ts @@ -0,0 +1,82 @@ +import { Module } from '@nestjs/common'; +import { JobService } from './job.service'; +import { JobController } from './job.controller'; +import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; +import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module'; +import { JwtModule } from '@nestjs/jwt'; +import { jwtConfig } from '../../config/jwt.config'; +import { UserEntity } from '@app/database/entities/user.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { BullModule } from '@nestjs/bull'; +import { QueueNameEnum } from '@app/job'; +import { AssetEntity } from '@app/database/entities/asset.entity'; +import { ExifEntity } from '@app/database/entities/exif.entity'; +import { AssetRepository, ASSET_REPOSITORY } from '../asset/asset-repository'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([UserEntity, AssetEntity, ExifEntity]), + ImmichJwtModule, + JwtModule.register(jwtConfig), + BullModule.registerQueue( + { + name: QueueNameEnum.THUMBNAIL_GENERATION, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + { + name: QueueNameEnum.ASSET_UPLOADED, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + { + name: QueueNameEnum.METADATA_EXTRACTION, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + { + name: QueueNameEnum.VIDEO_CONVERSION, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + { + name: QueueNameEnum.CHECKSUM_GENERATION, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + { + name: QueueNameEnum.MACHINE_LEARNING, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + ), + ], + controllers: [JobController], + providers: [ + JobService, + ImmichJwtService, + { + provide: ASSET_REPOSITORY, + useClass: AssetRepository, + }, + ], +}) +export class JobModule {} diff --git a/server/apps/immich/src/api-v1/job/job.service.ts b/server/apps/immich/src/api-v1/job/job.service.ts new file mode 100644 index 0000000000..761a70906f --- /dev/null +++ b/server/apps/immich/src/api-v1/job/job.service.ts @@ -0,0 +1,180 @@ +import { + exifExtractionProcessorName, + generateJPEGThumbnailProcessorName, + IMetadataExtractionJob, + IThumbnailGenerationJob, + IVideoTranscodeJob, + MachineLearningJobNameEnum, + QueueNameEnum, + videoMetadataExtractionProcessorName, +} from '@app/job'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { AllJobStatusResponseDto } from './response-dto/all-job-status-response.dto'; +import { randomUUID } from 'crypto'; +import { ASSET_REPOSITORY, IAssetRepository } from '../asset/asset-repository'; +import { AssetType } from '@app/database/entities/asset.entity'; +import { GetJobDto, JobId } from './dto/get-job.dto'; +import { JobStatusResponseDto } from './response-dto/job-status-response.dto'; +import { IMachineLearningJob } from '@app/job/interfaces/machine-learning.interface'; + +@Injectable() +export class JobService { + constructor( + @InjectQueue(QueueNameEnum.THUMBNAIL_GENERATION) + private thumbnailGeneratorQueue: Queue, + + @InjectQueue(QueueNameEnum.METADATA_EXTRACTION) + private metadataExtractionQueue: Queue, + + @InjectQueue(QueueNameEnum.VIDEO_CONVERSION) + private videoConversionQueue: Queue, + + @InjectQueue(QueueNameEnum.MACHINE_LEARNING) + private machineLearningQueue: Queue, + + @Inject(ASSET_REPOSITORY) + private _assetRepository: IAssetRepository, + ) { + this.thumbnailGeneratorQueue.empty(); + this.metadataExtractionQueue.empty(); + this.videoConversionQueue.empty(); + } + + 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 0; + case JobId.MACHINE_LEARNING: + return this.runMachineLearningPipeline(); + default: + throw new BadRequestException('Invalid job id'); + } + } + + 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 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; + + 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(); + } + + if (query.jobId === JobId.METADATA_EXTRACTION) { + response.isActive = Boolean((await this.metadataExtractionQueue.getJobCounts()).waiting); + response.queueCount = await this.metadataExtractionQueue.getJobCounts(); + } + + if (query.jobId === JobId.VIDEO_CONVERSION) { + response.isActive = Boolean((await this.videoConversionQueue.getJobCounts()).waiting); + response.queueCount = await this.videoConversionQueue.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; + 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(generateJPEGThumbnailProcessorName, { asset }, { jobId: randomUUID() }); + } + + 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( + videoMetadataExtractionProcessorName, + { asset, fileName: asset.id }, + { jobId: randomUUID() }, + ); + } else { + await this.metadataExtractionQueue.add( + exifExtractionProcessorName, + { asset, fileName: asset.id }, + { jobId: randomUUID() }, + ); + } + } + return assetsWithNoExif.length; + } + + private async runMachineLearningPipeline(): Promise { + 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(MachineLearningJobNameEnum.IMAGE_TAGGING, { asset }, { jobId: randomUUID() }); + await this.machineLearningQueue.add( + MachineLearningJobNameEnum.OBJECT_DETECTION, + { asset }, + { jobId: randomUUID() }, + ); + } + + return assetWithNoSmartInfo.length; + } +} 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 new file mode 100644 index 0000000000..aeb558acd5 --- /dev/null +++ b/server/apps/immich/src/api-v1/job/response-dto/all-job-status-response.dto.ts @@ -0,0 +1,35 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class JobCounts { + active!: number; + completed!: number; + failed!: number; + delayed!: number; + waiting!: number; +} +export class AllJobStatusResponseDto { + isThumbnailGenerationActive!: boolean; + isMetadataExtractionActive!: boolean; + isVideoConversionActive!: boolean; + isMachineLearningActive!: boolean; + + @ApiProperty({ + type: JobCounts, + }) + thumbnailGenerationQueueCount!: JobCounts; + + @ApiProperty({ + type: JobCounts, + }) + metadataExtractionQueueCount!: JobCounts; + + @ApiProperty({ + type: JobCounts, + }) + videoConversionQueueCount!: JobCounts; + + @ApiProperty({ + type: JobCounts, + }) + machineLearningQueueCount!: 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 new file mode 100644 index 0000000000..fe411fa2ef --- /dev/null +++ b/server/apps/immich/src/api-v1/job/response-dto/job-status-response.dto.ts @@ -0,0 +1,6 @@ +import Bull from 'bull'; + +export class JobStatusResponseDto { + isActive!: boolean; + queueCount!: Bull.JobCounts; +} diff --git a/server/apps/immich/src/api-v1/server-info/response-dto/server-info-response.dto.ts b/server/apps/immich/src/api-v1/server-info/response-dto/server-info-response.dto.ts index 444292091b..e844da6899 100644 --- a/server/apps/immich/src/api-v1/server-info/response-dto/server-info-response.dto.ts +++ b/server/apps/immich/src/api-v1/server-info/response-dto/server-info-response.dto.ts @@ -5,13 +5,13 @@ export class ServerInfoResponseDto { diskUse!: string; diskAvailable!: string; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', format: 'int64' }) diskSizeRaw!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', format: 'int64' }) diskUseRaw!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', format: 'int64' }) diskAvailableRaw!: number; @ApiProperty({ type: 'number', format: 'float' }) diff --git a/server/apps/immich/src/app.module.ts b/server/apps/immich/src/app.module.ts index 16f644c030..3aef3d4b4d 100644 --- a/server/apps/immich/src/app.module.ts +++ b/server/apps/immich/src/app.module.ts @@ -15,6 +15,7 @@ import { AppController } from './app.controller'; import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.module'; import { DatabaseModule } from '@app/database'; +import { JobModule } from './api-v1/job/job.module'; @Module({ imports: [ @@ -55,6 +56,8 @@ import { DatabaseModule } from '@app/database'; ScheduleModule.forRoot(), ScheduleTasksModule, + + JobModule, ], controllers: [AppController], providers: [], diff --git a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts index 51f70fe8f5..e932a67f97 100644 --- a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts +++ b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts @@ -3,18 +3,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AssetEntity } from '@app/database/entities/asset.entity'; import { ScheduleTasksService } from './schedule-tasks.service'; -import { - metadataExtractionQueueName, - thumbnailGeneratorQueueName, - videoConversionQueueName, -} from '@app/job/constants/queue-name.constant'; +import { QueueNameEnum } from '@app/job/constants/queue-name.constant'; import { ExifEntity } from '@app/database/entities/exif.entity'; @Module({ imports: [ TypeOrmModule.forFeature([AssetEntity, ExifEntity]), BullModule.registerQueue({ - name: videoConversionQueueName, + name: QueueNameEnum.VIDEO_CONVERSION, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -22,7 +18,7 @@ import { ExifEntity } from '@app/database/entities/exif.entity'; }, }), BullModule.registerQueue({ - name: thumbnailGeneratorQueueName, + name: QueueNameEnum.THUMBNAIL_GENERATION, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -31,7 +27,7 @@ import { ExifEntity } from '@app/database/entities/exif.entity'; }), BullModule.registerQueue({ - name: metadataExtractionQueueName, + name: QueueNameEnum.METADATA_EXTRACTION, defaultJobOptions: { attempts: 3, removeOnComplete: true, 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 cbdf3d8b16..d814d8b307 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 @@ -12,11 +12,9 @@ import { generateWEBPThumbnailProcessorName, IMetadataExtractionJob, IVideoTranscodeJob, - metadataExtractionQueueName, mp4ConversionProcessorName, + QueueNameEnum, reverseGeocodingProcessorName, - thumbnailGeneratorQueueName, - videoConversionQueueName, videoMetadataExtractionProcessorName, } from '@app/job'; import { ConfigService } from '@nestjs/config'; @@ -30,13 +28,13 @@ export class ScheduleTasksService { @InjectRepository(ExifEntity) private exifRepository: Repository, - @InjectQueue(thumbnailGeneratorQueueName) + @InjectQueue(QueueNameEnum.THUMBNAIL_GENERATION) private thumbnailGeneratorQueue: Queue, - @InjectQueue(videoConversionQueueName) + @InjectQueue(QueueNameEnum.VIDEO_CONVERSION) private videoConversionQueue: Queue, - @InjectQueue(metadataExtractionQueueName) + @InjectQueue(QueueNameEnum.METADATA_EXTRACTION) private metadataExtractionQueue: Queue, private configService: ConfigService, @@ -108,11 +106,11 @@ export class ScheduleTasksService { @Cron(CronExpression.EVERY_DAY_AT_3AM) async extractExif() { - const exifAssets = await this.assetRepository.find({ - where: { - exifInfo: IsNull(), - }, - }); + const exifAssets = await this.assetRepository + .createQueryBuilder('asset') + .leftJoinAndSelect('asset.exifInfo', 'ei') + .where('ei."assetId" IS NULL') + .getMany(); for (const asset of exifAssets) { if (asset.type === AssetType.VIDEO) { diff --git a/server/apps/microservices/src/microservices.module.ts b/server/apps/microservices/src/microservices.module.ts index 46b3b6afe0..0353bb08a9 100644 --- a/server/apps/microservices/src/microservices.module.ts +++ b/server/apps/microservices/src/microservices.module.ts @@ -4,13 +4,7 @@ import { AssetEntity } from '@app/database/entities/asset.entity'; import { ExifEntity } from '@app/database/entities/exif.entity'; import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; import { UserEntity } from '@app/database/entities/user.entity'; -import { - assetUploadedQueueName, - generateChecksumQueueName, - metadataExtractionQueueName, - thumbnailGeneratorQueueName, - videoConversionQueueName, -} from '@app/job/constants/queue-name.constant'; +import { QueueNameEnum } from '@app/job/constants/queue-name.constant'; import { BullModule } from '@nestjs/bull'; import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; @@ -19,6 +13,7 @@ import { CommunicationModule } from '../../immich/src/api-v1/communication/commu import { MicroservicesService } from './microservices.service'; import { AssetUploadedProcessor } from './processors/asset-uploaded.processor'; import { GenerateChecksumProcessor } from './processors/generate-checksum.processor'; +import { MachineLearningProcessor } from './processors/machine-learning.processor'; import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor'; import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor'; import { VideoTranscodeProcessor } from './processors/video-transcode.processor'; @@ -42,7 +37,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor' }), BullModule.registerQueue( { - name: thumbnailGeneratorQueueName, + name: QueueNameEnum.THUMBNAIL_GENERATION, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -50,7 +45,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor' }, }, { - name: assetUploadedQueueName, + name: QueueNameEnum.ASSET_UPLOADED, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -58,7 +53,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor' }, }, { - name: metadataExtractionQueueName, + name: QueueNameEnum.METADATA_EXTRACTION, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -66,7 +61,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor' }, }, { - name: videoConversionQueueName, + name: QueueNameEnum.VIDEO_CONVERSION, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -74,7 +69,15 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor' }, }, { - name: generateChecksumQueueName, + name: QueueNameEnum.CHECKSUM_GENERATION, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + { + name: QueueNameEnum.MACHINE_LEARNING, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -92,6 +95,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor' MetadataExtractionProcessor, VideoTranscodeProcessor, GenerateChecksumProcessor, + MachineLearningProcessor, ConfigService, ], exports: [], diff --git a/server/apps/microservices/src/microservices.service.ts b/server/apps/microservices/src/microservices.service.ts index e10fe46b83..5a03220f97 100644 --- a/server/apps/microservices/src/microservices.service.ts +++ b/server/apps/microservices/src/microservices.service.ts @@ -1,4 +1,4 @@ -import { generateChecksumQueueName } from '@app/job'; +import { QueueNameEnum } from '@app/job'; import { InjectQueue } from '@nestjs/bull'; import { Injectable, OnModuleInit } from '@nestjs/common'; import { Queue } from 'bull'; @@ -6,14 +6,18 @@ import { randomUUID } from 'node:crypto'; @Injectable() export class MicroservicesService implements OnModuleInit { - constructor ( - @InjectQueue(generateChecksumQueueName) + constructor( + @InjectQueue(QueueNameEnum.CHECKSUM_GENERATION) private generateChecksumQueue: Queue, ) {} async onModuleInit() { - await this.generateChecksumQueue.add({}, { - jobId: randomUUID(), delay: 10000 // wait for migration - }); + await this.generateChecksumQueue.add( + {}, + { + jobId: randomUUID(), + delay: 10000, // wait for migration + }, + ); } } diff --git a/server/apps/microservices/src/processors/asset-uploaded.processor.ts b/server/apps/microservices/src/processors/asset-uploaded.processor.ts index 7b70c07482..340d22a06d 100644 --- a/server/apps/microservices/src/processors/asset-uploaded.processor.ts +++ b/server/apps/microservices/src/processors/asset-uploaded.processor.ts @@ -4,30 +4,27 @@ import { IMetadataExtractionJob, IThumbnailGenerationJob, IVideoTranscodeJob, - assetUploadedQueueName, - metadataExtractionQueueName, - thumbnailGeneratorQueueName, - videoConversionQueueName, assetUploadedProcessorName, exifExtractionProcessorName, generateJPEGThumbnailProcessorName, mp4ConversionProcessorName, videoMetadataExtractionProcessorName, + QueueNameEnum, } from '@app/job'; import { InjectQueue, Process, Processor } from '@nestjs/bull'; import { Job, Queue } from 'bull'; import { randomUUID } from 'crypto'; -@Processor(assetUploadedQueueName) +@Processor(QueueNameEnum.ASSET_UPLOADED) export class AssetUploadedProcessor { constructor( - @InjectQueue(thumbnailGeneratorQueueName) + @InjectQueue(QueueNameEnum.THUMBNAIL_GENERATION) private thumbnailGeneratorQueue: Queue, - @InjectQueue(metadataExtractionQueueName) + @InjectQueue(QueueNameEnum.METADATA_EXTRACTION) private metadataExtractionQueue: Queue, - @InjectQueue(videoConversionQueueName) + @InjectQueue(QueueNameEnum.VIDEO_CONVERSION) private videoConversionQueue: Queue, ) {} diff --git a/server/apps/microservices/src/processors/generate-checksum.processor.ts b/server/apps/microservices/src/processors/generate-checksum.processor.ts index 2dcd1c2bd4..bbf20cccd4 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/database/entities/asset.entity'; -import { generateChecksumQueueName } from '@app/job'; +import { QueueNameEnum } from '@app/job'; import { Process, Processor } from '@nestjs/bull'; import { Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; @@ -8,7 +8,7 @@ import fs from 'node:fs'; import { FindOptionsWhere, IsNull, MoreThan, QueryFailedError, Repository } from 'typeorm'; // TODO: just temporary task to generate previous uploaded assets. -@Processor(generateChecksumQueueName) +@Processor(QueueNameEnum.CHECKSUM_GENERATION) export class GenerateChecksumProcessor { constructor( @InjectRepository(AssetEntity) @@ -33,7 +33,7 @@ export class GenerateChecksumProcessor { const assets = await this.assetRepository.find({ where: whereStat, take: pageSize, - order: { id: 'ASC' } + order: { id: 'ASC' }, }); if (!assets?.length) { diff --git a/server/apps/microservices/src/processors/machine-learning.processor.ts b/server/apps/microservices/src/processors/machine-learning.processor.ts new file mode 100644 index 0000000000..39c92fd9f0 --- /dev/null +++ b/server/apps/microservices/src/processors/machine-learning.processor.ts @@ -0,0 +1,60 @@ +import { AssetEntity } from '@app/database/entities/asset.entity'; +import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; +import { MachineLearningJobNameEnum, QueueNameEnum } from '@app/job'; +import { IMachineLearningJob } from '@app/job/interfaces/machine-learning.interface'; +import { Process, Processor } from '@nestjs/bull'; +import { Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import axios from 'axios'; +import { Job } from 'bull'; +import { Repository } from 'typeorm'; + +@Processor(QueueNameEnum.MACHINE_LEARNING) +export class MachineLearningProcessor { + constructor( + @InjectRepository(SmartInfoEntity) + private smartInfoRepository: Repository, + ) {} + + @Process({ name: MachineLearningJobNameEnum.IMAGE_TAGGING, concurrency: 2 }) + async tagImage(job: Job) { + const { asset } = job.data; + + const res = await axios.post('http://immich-machine-learning:3003/image-classifier/tag-image', { + thumbnailPath: asset.resizePath, + }); + + if (res.status == 201 && res.data.length > 0) { + const smartInfo = new SmartInfoEntity(); + smartInfo.assetId = asset.id; + smartInfo.tags = [...res.data]; + + await this.smartInfoRepository.upsert(smartInfo, { + conflictPaths: ['assetId'], + }); + } + } + + @Process({ name: MachineLearningJobNameEnum.OBJECT_DETECTION, concurrency: 2 }) + async detectObject(job: Job) { + try { + const { asset }: { asset: AssetEntity } = job.data; + + const res = await axios.post('http://immich-machine-learning:3003/object-detection/detect-object', { + thumbnailPath: asset.resizePath, + }); + + if (res.status == 201 && res.data.length > 0) { + const smartInfo = new SmartInfoEntity(); + smartInfo.assetId = asset.id; + smartInfo.objects = [...res.data]; + + await this.smartInfoRepository.upsert(smartInfo, { + conflictPaths: ['assetId'], + }); + } + } catch (error) { + Logger.error(`Failed to trigger object detection pipe line ${String(error)}`); + } + } +} diff --git a/server/apps/microservices/src/processors/metadata-extraction.processor.ts b/server/apps/microservices/src/processors/metadata-extraction.processor.ts index a06972e042..27f2688d74 100644 --- a/server/apps/microservices/src/processors/metadata-extraction.processor.ts +++ b/server/apps/microservices/src/processors/metadata-extraction.processor.ts @@ -1,23 +1,19 @@ import { ImmichLogLevel } from '@app/common/constants/log-level.constant'; import { AssetEntity } from '@app/database/entities/asset.entity'; import { ExifEntity } from '@app/database/entities/exif.entity'; -import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; import { IExifExtractionProcessor, IVideoLengthExtractionProcessor, exifExtractionProcessorName, - imageTaggingProcessorName, - objectDetectionProcessorName, videoMetadataExtractionProcessorName, - metadataExtractionQueueName, reverseGeocodingProcessorName, IReverseGeocodingProcessor, + QueueNameEnum, } from '@app/job'; import { Process, Processor } from '@nestjs/bull'; import { Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; -import axios from 'axios'; import { Job } from 'bull'; import exifr from 'exifr'; import ffmpeg from 'fluent-ffmpeg'; @@ -79,7 +75,7 @@ export interface GeoData { distance: number; } -@Processor(metadataExtractionQueueName) +@Processor(QueueNameEnum.METADATA_EXTRACTION) export class MetadataExtractionProcessor { private isGeocodeInitialized = false; private logLevel: ImmichLogLevel; @@ -91,9 +87,6 @@ export class MetadataExtractionProcessor { @InjectRepository(ExifEntity) private exifRepository: Repository, - @InjectRepository(SmartInfoEntity) - private smartInfoRepository: Repository, - private configService: ConfigService, ) { if (!configService.get('DISABLE_REVERSE_GEOCODING')) { @@ -109,7 +102,8 @@ export class MetadataExtractionProcessor { alternateNames: false, }, countries: [], - dumpDirectory: configService.get('REVERSE_GEOCODING_DUMP_DIRECTORY') || (process.cwd() + '/.reverse-geocoding-dump/'), + dumpDirectory: + configService.get('REVERSE_GEOCODING_DUMP_DIRECTORY') || process.cwd() + '/.reverse-geocoding-dump/', }).then(() => { this.isGeocodeInitialized = true; Logger.log('Reverse Geocoding Initialised'); @@ -273,48 +267,6 @@ export class MetadataExtractionProcessor { } } - @Process({ name: imageTaggingProcessorName, concurrency: 2 }) - async tagImage(job: Job) { - const { asset }: { asset: AssetEntity } = job.data; - - const res = await axios.post('http://immich-machine-learning:3003/image-classifier/tag-image', { - thumbnailPath: asset.resizePath, - }); - - if (res.status == 201 && res.data.length > 0) { - const smartInfo = new SmartInfoEntity(); - smartInfo.assetId = asset.id; - smartInfo.tags = [...res.data]; - - await this.smartInfoRepository.upsert(smartInfo, { - conflictPaths: ['assetId'], - }); - } - } - - @Process({ name: objectDetectionProcessorName, concurrency: 2 }) - async detectObject(job: Job) { - try { - const { asset }: { asset: AssetEntity } = job.data; - - const res = await axios.post('http://immich-machine-learning:3003/object-detection/detect-object', { - thumbnailPath: asset.resizePath, - }); - - if (res.status == 201 && res.data.length > 0) { - const smartInfo = new SmartInfoEntity(); - smartInfo.assetId = asset.id; - smartInfo.objects = [...res.data]; - - await this.smartInfoRepository.upsert(smartInfo, { - conflictPaths: ['assetId'], - }); - } - } catch (error) { - Logger.error(`Failed to trigger object detection pipe line ${String(error)}`); - } - } - @Process({ name: videoMetadataExtractionProcessorName, concurrency: 2 }) async extractVideoMetadata(job: Job) { const { asset, fileName } = job.data; diff --git a/server/apps/microservices/src/processors/thumbnail.processor.ts b/server/apps/microservices/src/processors/thumbnail.processor.ts index 130aa6c1ad..211da0537c 100644 --- a/server/apps/microservices/src/processors/thumbnail.processor.ts +++ b/server/apps/microservices/src/processors/thumbnail.processor.ts @@ -5,11 +5,9 @@ import { WebpGeneratorProcessor, generateJPEGThumbnailProcessorName, generateWEBPThumbnailProcessorName, - imageTaggingProcessorName, - objectDetectionProcessorName, - metadataExtractionQueueName, - thumbnailGeneratorQueueName, JpegGeneratorProcessor, + QueueNameEnum, + MachineLearningJobNameEnum, } from '@app/job'; import { InjectQueue, Process, Processor } from '@nestjs/bull'; import { Logger } from '@nestjs/common'; @@ -25,8 +23,9 @@ import sharp from 'sharp'; import { Repository } from 'typeorm/repository/Repository'; import { join } from 'path'; import { CommunicationGateway } from 'apps/immich/src/api-v1/communication/communication.gateway'; +import { IMachineLearningJob } from '@app/job/interfaces/machine-learning.interface'; -@Processor(thumbnailGeneratorQueueName) +@Processor(QueueNameEnum.THUMBNAIL_GENERATION) export class ThumbnailGeneratorProcessor { private logLevel: ImmichLogLevel; @@ -34,13 +33,13 @@ export class ThumbnailGeneratorProcessor { @InjectRepository(AssetEntity) private assetRepository: Repository, - @InjectQueue(thumbnailGeneratorQueueName) + @InjectQueue(QueueNameEnum.THUMBNAIL_GENERATION) private thumbnailGeneratorQueue: Queue, private wsCommunicationGateway: CommunicationGateway, - @InjectQueue(metadataExtractionQueueName) - private metadataExtractionQueue: Queue, + @InjectQueue(QueueNameEnum.MACHINE_LEARNING) + private machineLearningQueue: Queue, private configService: ConfigService, ) { @@ -80,8 +79,12 @@ export class ThumbnailGeneratorProcessor { asset.resizePath = jpegThumbnailPath; await this.thumbnailGeneratorQueue.add(generateWEBPThumbnailProcessorName, { asset }, { jobId: randomUUID() }); - await this.metadataExtractionQueue.add(imageTaggingProcessorName, { asset }, { jobId: randomUUID() }); - await this.metadataExtractionQueue.add(objectDetectionProcessorName, { asset }, { jobId: randomUUID() }); + await this.machineLearningQueue.add(MachineLearningJobNameEnum.IMAGE_TAGGING, { asset }, { jobId: randomUUID() }); + await this.machineLearningQueue.add( + MachineLearningJobNameEnum.OBJECT_DETECTION, + { asset }, + { jobId: randomUUID() }, + ); this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset))); } @@ -110,8 +113,12 @@ export class ThumbnailGeneratorProcessor { asset.resizePath = jpegThumbnailPath; await this.thumbnailGeneratorQueue.add(generateWEBPThumbnailProcessorName, { asset }, { jobId: randomUUID() }); - await this.metadataExtractionQueue.add(imageTaggingProcessorName, { asset }, { jobId: randomUUID() }); - await this.metadataExtractionQueue.add(objectDetectionProcessorName, { asset }, { jobId: randomUUID() }); + await this.machineLearningQueue.add(MachineLearningJobNameEnum.IMAGE_TAGGING, { asset }, { jobId: randomUUID() }); + await this.machineLearningQueue.add( + MachineLearningJobNameEnum.OBJECT_DETECTION, + { asset }, + { jobId: randomUUID() }, + ); this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset))); } diff --git a/server/apps/microservices/src/processors/video-transcode.processor.ts b/server/apps/microservices/src/processors/video-transcode.processor.ts index 45ea17ab09..2e04a75e0a 100644 --- a/server/apps/microservices/src/processors/video-transcode.processor.ts +++ b/server/apps/microservices/src/processors/video-transcode.processor.ts @@ -1,7 +1,7 @@ import { APP_UPLOAD_LOCATION } from '@app/common/constants'; import { AssetEntity } from '@app/database/entities/asset.entity'; +import { QueueNameEnum } from '@app/job'; import { mp4ConversionProcessorName } from '@app/job/constants/job-name.constant'; -import { videoConversionQueueName } from '@app/job/constants/queue-name.constant'; import { IMp4ConversionProcessor } from '@app/job/interfaces/video-transcode.interface'; import { Process, Processor } from '@nestjs/bull'; import { Logger } from '@nestjs/common'; @@ -11,7 +11,7 @@ import ffmpeg from 'fluent-ffmpeg'; import { existsSync, mkdirSync } from 'fs'; import { Repository } from 'typeorm'; -@Processor(videoConversionQueueName) +@Processor(QueueNameEnum.VIDEO_CONVERSION) export class VideoTranscodeProcessor { constructor( @InjectRepository(AssetEntity) diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index e78036840f..955bfe7c88 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1 +1 @@ -{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download":{"get":{"operationId":"downloadFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file":{"get":{"operationId":"serveFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"string","nullable":true,"default":null},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"fileSizeInByte":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"}},"required":["photos","videos"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true}},"required":["successful"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer"},"diskUseRaw":{"type":"integer"},"diskAvailableRaw":{"type":"integer"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}}}}} \ No newline at end of file +{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download":{"get":{"operationId":"downloadFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file":{"get":{"operationId":"serveFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}},"/jobs":{"get":{"operationId":"getAllJobsStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllJobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/jobs/{jobId}":{"get":{"operationId":"getJobStatus","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","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCommandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"integer","nullable":true,"default":null,"format":"int64"},"fileSizeInByte":{"type":"integer","nullable":true,"default":null,"format":"int64"},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"}},"required":["photos","videos"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true}},"required":["successful"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer","format":"int64"},"diskUseRaw":{"type":"integer","format":"int64"},"diskAvailableRaw":{"type":"integer","format":"int64"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}},"JobCounts":{"type":"object","properties":{"active":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"},"delayed":{"type":"number"},"waiting":{"type":"number"}},"required":["active","completed","failed","delayed","waiting"]},"AllJobStatusResponseDto":{"type":"object","properties":{"thumbnailGenerationQueueCount":{"$ref":"#/components/schemas/JobCounts"},"metadataExtractionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"videoConversionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"machineLearningQueueCount":{"$ref":"#/components/schemas/JobCounts"},"isThumbnailGenerationActive":{"type":"boolean"},"isMetadataExtractionActive":{"type":"boolean"},"isVideoConversionActive":{"type":"boolean"},"isMachineLearningActive":{"type":"boolean"}},"required":["thumbnailGenerationQueueCount","metadataExtractionQueueCount","videoConversionQueueCount","machineLearningQueueCount","isThumbnailGenerationActive","isMetadataExtractionActive","isVideoConversionActive","isMachineLearningActive"]},"JobId":{"type":"string","enum":["thumbnail-generation","metadata-extraction","video-conversion","machine-learning"]},"JobStatusResponseDto":{"type":"object","properties":{"isActive":{"type":"boolean"},"queueCount":{"type":"object"}},"required":["isActive","queueCount"]},"JobCommand":{"type":"string","enum":["start","stop"]},"JobCommandDto":{"type":"object","properties":{"command":{"$ref":"#/components/schemas/JobCommand"}},"required":["command"]}}}} \ No newline at end of file diff --git a/server/libs/job/src/constants/job-name.constant.ts b/server/libs/job/src/constants/job-name.constant.ts index 002dd7f5a7..7b32c69028 100644 --- a/server/libs/job/src/constants/job-name.constant.ts +++ b/server/libs/job/src/constants/job-name.constant.ts @@ -20,5 +20,12 @@ export const generateWEBPThumbnailProcessorName = 'generate-webp-thumbnail'; export const exifExtractionProcessorName = 'exif-extraction'; export const videoMetadataExtractionProcessorName = 'extract-video-metadata'; export const reverseGeocodingProcessorName = 'reverse-geocoding'; -export const objectDetectionProcessorName = 'detect-object'; -export const imageTaggingProcessorName = 'tag-image'; + +/** + * Machine learning Queue Jobs + */ + +export enum MachineLearningJobNameEnum { + OBJECT_DETECTION = 'detect-object', + IMAGE_TAGGING = 'tag-image', +} diff --git a/server/libs/job/src/constants/queue-name.constant.ts b/server/libs/job/src/constants/queue-name.constant.ts index 15b7d7a6cc..f0f4c4e053 100644 --- a/server/libs/job/src/constants/queue-name.constant.ts +++ b/server/libs/job/src/constants/queue-name.constant.ts @@ -1,5 +1,8 @@ -export const thumbnailGeneratorQueueName = 'thumbnail-generator-queue'; -export const assetUploadedQueueName = 'asset-uploaded-queue'; -export const metadataExtractionQueueName = 'metadata-extraction-queue'; -export const videoConversionQueueName = 'video-conversion-queue'; -export const generateChecksumQueueName = 'generate-checksum-queue'; +export enum QueueNameEnum { + THUMBNAIL_GENERATION = 'thumbnail-generation-queue', + METADATA_EXTRACTION = 'metadata-extraction-queue', + VIDEO_CONVERSION = 'video-conversion-queue', + CHECKSUM_GENERATION = 'generate-checksum-queue', + ASSET_UPLOADED = 'asset-uploaded-queue', + MACHINE_LEARNING = 'machine-learning-queue', +} diff --git a/server/libs/job/src/interfaces/machine-learning.interface.ts b/server/libs/job/src/interfaces/machine-learning.interface.ts new file mode 100644 index 0000000000..13bf5e19d5 --- /dev/null +++ b/server/libs/job/src/interfaces/machine-learning.interface.ts @@ -0,0 +1,8 @@ +import { AssetEntity } from '@app/database/entities/asset.entity'; + +export interface IMachineLearningJob { + /** + * The Asset entity that was saved in the database + */ + asset: AssetEntity; +} diff --git a/server/package-lock.json b/server/package-lock.json index 39730ad635..62ebb7fd08 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -59,7 +59,7 @@ "@nestjs/testing": "^8.4.7", "@openapitools/openapi-generator-cli": "2.5.1", "@types/bcrypt": "^5.0.0", - "@types/bull": "^3.15.7", + "@types/bull": "^3.15.9", "@types/cookie-parser": "^1.4.3", "@types/cron": "^2.0.0", "@types/express": "^4.17.13", @@ -2339,9 +2339,9 @@ } }, "node_modules/@types/bull": { - "version": "3.15.7", - "resolved": "https://registry.npmjs.org/@types/bull/-/bull-3.15.7.tgz", - "integrity": "sha512-7NC7XN5NoS0A+leJ/dR69ZfKaegOlCZaii/xGgKnCyh1UYisRncibImb7VMwrc3OdJcbDJt6+4om70TeNl3J7g==", + "version": "3.15.9", + "resolved": "https://registry.npmjs.org/@types/bull/-/bull-3.15.9.tgz", + "integrity": "sha512-MPUcyPPQauAmynoO3ezHAmCOhbB0pWmYyijr/5ctaCqhbKWsjW0YCod38ZcLzUBprosfZ9dPqfYIcfdKjk7RNQ==", "dev": true, "dependencies": { "@types/ioredis": "*", @@ -3764,6 +3764,27 @@ "node": ">= 0.8" } }, + "node_modules/cache-manager": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.0.0.tgz", + "integrity": "sha512-1qKdoeoJKmrf95Zvhr3NpBVAgBESt4TuZomBzn4N2gCFZvHjuUXBK1H8EDVsJdba6/grIgi6WGYb/ncJj+wjtg==", + "optional": true, + "peer": true, + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^7.14.0" + } + }, + "node_modules/cache-manager/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -7674,6 +7695,13 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "optional": true, + "peer": true + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -12900,9 +12928,9 @@ } }, "@types/bull": { - "version": "3.15.7", - "resolved": "https://registry.npmjs.org/@types/bull/-/bull-3.15.7.tgz", - "integrity": "sha512-7NC7XN5NoS0A+leJ/dR69ZfKaegOlCZaii/xGgKnCyh1UYisRncibImb7VMwrc3OdJcbDJt6+4om70TeNl3J7g==", + "version": "3.15.9", + "resolved": "https://registry.npmjs.org/@types/bull/-/bull-3.15.9.tgz", + "integrity": "sha512-MPUcyPPQauAmynoO3ezHAmCOhbB0pWmYyijr/5ctaCqhbKWsjW0YCod38ZcLzUBprosfZ9dPqfYIcfdKjk7RNQ==", "dev": true, "requires": { "@types/ioredis": "*", @@ -14073,6 +14101,26 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, + "cache-manager": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.0.0.tgz", + "integrity": "sha512-1qKdoeoJKmrf95Zvhr3NpBVAgBESt4TuZomBzn4N2gCFZvHjuUXBK1H8EDVsJdba6/grIgi6WGYb/ncJj+wjtg==", + "optional": true, + "peer": true, + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^7.14.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "peer": true + } + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -17088,6 +17136,13 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "optional": true, + "peer": true + }, "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", diff --git a/server/package.json b/server/package.json index 368765adc1..62c113b7db 100644 --- a/server/package.json +++ b/server/package.json @@ -78,7 +78,7 @@ "@nestjs/testing": "^8.4.7", "@openapitools/openapi-generator-cli": "2.5.1", "@types/bcrypt": "^5.0.0", - "@types/bull": "^3.15.7", + "@types/bull": "^3.15.9", "@types/cookie-parser": "^1.4.3", "@types/cron": "^2.0.0", "@types/express": "^4.17.13", diff --git a/web/src/api/api.ts b/web/src/api/api.ts index 5e2b8f356d..b621c649f2 100644 --- a/web/src/api/api.ts +++ b/web/src/api/api.ts @@ -4,6 +4,7 @@ import { AuthenticationApi, Configuration, DeviceInfoApi, + JobApi, ServerInfoApi, UserApi } from './open-api'; @@ -15,6 +16,8 @@ class ImmichApi { public authenticationApi: AuthenticationApi; public deviceInfoApi: DeviceInfoApi; public serverInfoApi: ServerInfoApi; + public jobApi: JobApi; + private config = new Configuration({ basePath: '/api' }); constructor() { @@ -24,6 +27,7 @@ class ImmichApi { this.authenticationApi = new AuthenticationApi(this.config); this.deviceInfoApi = new DeviceInfoApi(this.config); this.serverInfoApi = new ServerInfoApi(this.config); + this.jobApi = new JobApi(this.config); } public setAccessToken(accessToken: string) { diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 27ff36ab88..6654d9a328 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -170,6 +170,61 @@ export interface AlbumResponseDto { */ 'assets': Array; } +/** + * + * @export + * @interface AllJobStatusResponseDto + */ +export interface AllJobStatusResponseDto { + /** + * + * @type {JobCounts} + * @memberof AllJobStatusResponseDto + */ + 'thumbnailGenerationQueueCount': JobCounts; + /** + * + * @type {JobCounts} + * @memberof AllJobStatusResponseDto + */ + 'metadataExtractionQueueCount': JobCounts; + /** + * + * @type {JobCounts} + * @memberof AllJobStatusResponseDto + */ + 'videoConversionQueueCount': JobCounts; + /** + * + * @type {JobCounts} + * @memberof AllJobStatusResponseDto + */ + 'machineLearningQueueCount': 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; +} /** * * @export @@ -683,10 +738,16 @@ export type DeviceTypeEnum = typeof DeviceTypeEnum[keyof typeof DeviceTypeEnum]; export interface ExifResponseDto { /** * - * @type {string} + * @type {number} * @memberof ExifResponseDto */ - 'id'?: string | null; + 'id'?: number | null; + /** + * + * @type {number} + * @memberof ExifResponseDto + */ + 'fileSizeInByte'?: number | null; /** * * @type {string} @@ -717,12 +778,6 @@ export interface ExifResponseDto { * @memberof ExifResponseDto */ 'exifImageHeight'?: number | null; - /** - * - * @type {number} - * @memberof ExifResponseDto - */ - 'fileSizeInByte'?: number | null; /** * * @type {string} @@ -828,6 +883,105 @@ export interface GetAssetCountByTimeBucketDto { */ 'timeGroup': TimeGroupEnum; } +/** + * + * @export + * @enum {string} + */ + +export const JobCommand = { + Start: 'start', + Stop: 'stop' +} as const; + +export type JobCommand = typeof JobCommand[keyof typeof JobCommand]; + + +/** + * + * @export + * @interface JobCommandDto + */ +export interface JobCommandDto { + /** + * + * @type {JobCommand} + * @memberof JobCommandDto + */ + 'command': JobCommand; +} +/** + * + * @export + * @interface JobCounts + */ +export interface JobCounts { + /** + * + * @type {number} + * @memberof JobCounts + */ + 'active': number; + /** + * + * @type {number} + * @memberof JobCounts + */ + 'completed': number; + /** + * + * @type {number} + * @memberof JobCounts + */ + 'failed': number; + /** + * + * @type {number} + * @memberof JobCounts + */ + 'delayed': number; + /** + * + * @type {number} + * @memberof JobCounts + */ + 'waiting': number; +} +/** + * + * @export + * @enum {string} + */ + +export const JobId = { + ThumbnailGeneration: 'thumbnail-generation', + MetadataExtraction: 'metadata-extraction', + VideoConversion: 'video-conversion', + MachineLearning: 'machine-learning' +} as const; + +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 @@ -3682,6 +3836,247 @@ export class DeviceInfoApi extends BaseAPI { } +/** + * JobApi - axios parameter creator + * @export + */ +export const JobApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllJobsStatus: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/jobs`; + // 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}; + + 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}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {JobId} jobId + * @param {JobCommandDto} jobCommandDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + sendJobCommand: async (jobId: JobId, jobCommandDto: JobCommandDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'jobId' is not null or undefined + assertParamExists('sendJobCommand', 'jobId', jobId) + // verify required parameter 'jobCommandDto' is not null or undefined + assertParamExists('sendJobCommand', 'jobCommandDto', jobCommandDto) + 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: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(jobCommandDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * JobApi - functional programming interface + * @export + */ +export const JobApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = JobApiAxiosParamCreator(configuration) + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAllJobsStatus(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + 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 + * @param {JobCommandDto} jobCommandDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async sendJobCommand(jobId: JobId, jobCommandDto: JobCommandDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.sendJobCommand(jobId, jobCommandDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + } +}; + +/** + * JobApi - factory interface + * @export + */ +export const JobApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = JobApiFp(configuration) + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + 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 + * @param {JobCommandDto} jobCommandDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + sendJobCommand(jobId: JobId, jobCommandDto: JobCommandDto, options?: any): AxiosPromise { + return localVarFp.sendJobCommand(jobId, jobCommandDto, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * JobApi - object-oriented interface + * @export + * @class JobApi + * @extends {BaseAPI} + */ +export class JobApi extends BaseAPI { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof JobApi + */ + public getAllJobsStatus(options?: AxiosRequestConfig) { + 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 {JobCommandDto} jobCommandDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof JobApi + */ + public sendJobCommand(jobId: JobId, jobCommandDto: JobCommandDto, options?: AxiosRequestConfig) { + return JobApiFp(this.configuration).sendJobCommand(jobId, jobCommandDto, options).then((request) => request(this.axios, this.basePath)); + } +} + + /** * ServerInfoApi - axios parameter creator * @export diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte new file mode 100644 index 0000000000..5d09434f86 --- /dev/null +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -0,0 +1,52 @@ + + +
+
+

{title}

+

{subtitle}

+

+ +

+ + + + + + + + + + + + + + + + +
StatusActiveWaiting
{jobStatus ? 'Active' : 'Idle'}{activeJobCount}{waitingJobCount}
+
+
+ +
+
diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte new file mode 100644 index 0000000000..54b3a4fc8d --- /dev/null +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -0,0 +1,138 @@ + + +
+ + + + + + Note that some asset does not have any object detected, this is normal. + +
diff --git a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte index b4daac2d5f..6ba5621b1e 100644 --- a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte +++ b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte @@ -94,7 +94,7 @@
(isHover = true)} diff --git a/web/src/lib/models/admin-sidebar-selection.ts b/web/src/lib/models/admin-sidebar-selection.ts index a11914c96b..6ffe6ef4e1 100644 --- a/web/src/lib/models/admin-sidebar-selection.ts +++ b/web/src/lib/models/admin-sidebar-selection.ts @@ -1,5 +1,7 @@ export enum AdminSideBarSelection { - USER_MANAGEMENT = 'User management' + USER_MANAGEMENT = 'User management', + JOBS = 'Jobs', + SETTINGS = 'Settings' } export enum AppSideBarSelection { diff --git a/web/src/routes/admin/+layout.svelte b/web/src/routes/admin/+layout.svelte new file mode 100644 index 0000000000..7270e1de67 --- /dev/null +++ b/web/src/routes/admin/+layout.svelte @@ -0,0 +1,3 @@ +
+ +
diff --git a/web/src/routes/admin/+page.svelte b/web/src/routes/admin/+page.svelte index 8d18519c2d..5bde9cadfa 100644 --- a/web/src/routes/admin/+page.svelte +++ b/web/src/routes/admin/+page.svelte @@ -4,6 +4,7 @@ import { AdminSideBarSelection } from '$lib/models/admin-sidebar-selection'; import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte'; import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte'; + import Cog from 'svelte-material-icons/Cog.svelte'; import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte'; import UserManagement from '$lib/components/admin-page/user-management.svelte'; import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; @@ -12,6 +13,7 @@ import StatusBox from '$lib/components/shared-components/status-box.svelte'; import type { PageData } from './$types'; import { api, UserResponseDto } from '@api'; + import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte'; let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT; @@ -104,14 +106,21 @@ {/if}
-
+
+
@@ -132,6 +141,9 @@ on:edit-user={editUserHandler} /> {/if} + {#if selectedAction === AdminSideBarSelection.JOBS} + + {/if}