diff --git a/features/video-studio/packages/media-gallery/backend-api/src/modules/processing/processing.controller.ts b/features/video-studio/packages/media-gallery/backend-api/src/modules/processing/processing.controller.ts new file mode 100644 index 000000000..c3ca2a0a9 --- /dev/null +++ b/features/video-studio/packages/media-gallery/backend-api/src/modules/processing/processing.controller.ts @@ -0,0 +1,75 @@ +import { Controller, Post, HttpCode, HttpStatus } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { InjectQueue } from '@nestjs/bullmq'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Queue } from 'bullmq'; +import { IsNull, Not, Repository } from 'typeorm'; + +import { createLogger } from '@/common'; +import { PhotoEntity } from '@/entities'; + +const BACKFILL_BATCH_SIZE = 50; + +@ApiTags('processing') +@Controller('api/admin/processing') +export class ProcessingController { + private readonly logger = createLogger(ProcessingController.name); + + constructor( + @InjectRepository(PhotoEntity) + private readonly photoRepository: Repository, + @InjectQueue('thumbnail-processing') + private readonly thumbnailQueue: Queue, + ) {} + + @Post('backfill') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Enqueue all pending photos for thumbnail generation' }) + @ApiResponse({ + status: 200, + schema: { type: 'object', properties: { enqueued: { type: 'number' } } }, + }) + async backfill(): Promise<{ enqueued: number }> { + let enqueued = 0; + let offset = 0; + + this.logger.logWithData('info', 'Starting processing backfill'); + + for (;;) { + const batch = await this.photoRepository.find({ + where: { + processingStatus: 'pending', + storageKey: Not(IsNull()), + }, + select: ['id', 'storageKey', 'mimeType', 'width', 'height'], + take: BACKFILL_BATCH_SIZE, + skip: offset, + order: { id: 'ASC' }, + }); + + if (batch.length === 0) break; + + const jobs = batch.map((photo) => ({ + name: 'process', + data: { + photoId: photo.id, + storageKey: photo.storageKey!, + mimeType: photo.mimeType ?? 'image/jpeg', + width: photo.width ?? 0, + height: photo.height ?? 0, + }, + })); + + await this.thumbnailQueue.addBulk(jobs); + enqueued += batch.length; + offset += batch.length; + + this.logger.logWithData('info', 'Enqueued processing batch', { batchSize: batch.length, totalEnqueued: enqueued }); + + if (batch.length < BACKFILL_BATCH_SIZE) break; + } + + this.logger.logWithData('info', 'Processing backfill complete', { enqueued }); + return { enqueued }; + } +} diff --git a/features/video-studio/packages/media-gallery/backend-api/src/modules/processing/processing.module.ts b/features/video-studio/packages/media-gallery/backend-api/src/modules/processing/processing.module.ts index baa985646..715d8fd16 100644 --- a/features/video-studio/packages/media-gallery/backend-api/src/modules/processing/processing.module.ts +++ b/features/video-studio/packages/media-gallery/backend-api/src/modules/processing/processing.module.ts @@ -2,7 +2,7 @@ import { BullModule } from '@nestjs/bullmq'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; - +import { ProcessingController } from './processing.controller'; import { ThumbnailProcessor } from './thumbnail.processor'; import { MinioModule } from '@/common/minio'; @@ -21,6 +21,7 @@ import { PhotoEntity } from '@/entities'; defaultBucket: 'media-gallery', }), ], + controllers: [ProcessingController], providers: [ThumbnailProcessor], exports: [ThumbnailProcessor], })