import { Controller, Get, Post, Param, Body, Logger, NotFoundException, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { InjectQueue } from '@nestjs/bullmq'; import { Queue } from 'bullmq'; import { QUEUES, JobType, JobStatus, } from '../queue/queue.constants'; /** * JobsController * * REST API for managing async AI jobs. * * Endpoints: * - POST /jobs/submit — Submit a new async job * - GET /jobs/:id/status — Check job status & progress * - GET /jobs/:id/result — Get job result * * TR: Asenkron AI işlerini yönetmek için REST API. */ @ApiTags('SkriptAI - Jobs') @ApiBearerAuth() @Controller('skriptai/jobs') export class JobsController { private readonly logger = new Logger(JobsController.name); constructor( @InjectQueue(QUEUES.SCRIPT_GENERATION) private readonly scriptQueue: Queue, @InjectQueue(QUEUES.DEEP_RESEARCH) private readonly researchQueue: Queue, @InjectQueue(QUEUES.ANALYSIS) private readonly analysisQueue: Queue, @InjectQueue(QUEUES.IMAGE_GENERATION) private readonly imageQueue: Queue, ) {} /** * Submit a new async job */ @Post('submit') @ApiOperation({ summary: 'Submit an async AI job' }) async submitJob( @Body() body: { type: JobType; payload: Record; }, ) { const { type, payload } = body; const queue = this.getQueueForJobType(type); const job = await queue.add(type, payload, { attempts: 2, backoff: { type: 'exponential', delay: 5000 }, removeOnComplete: { age: 3600 }, // 1 hour removeOnFail: { age: 86400 }, // 24 hours }); this.logger.log( `Job submitted: ${job.id} (${type}) — payload: ${JSON.stringify(payload)}`, ); return { jobId: job.id, type, status: JobStatus.QUEUED, }; } /** * Check job status and progress */ @Get(':id/status') @ApiOperation({ summary: 'Check job status & progress' }) async getJobStatus(@Param('id') jobId: string) { const job = await this.findJobById(jobId); if (!job) { throw new NotFoundException(`Job ${jobId} not found`); } const state = await job.getState(); const progress = job.progress; return { jobId: job.id, type: job.name, status: this.mapBullState(state), progress: progress || null, createdAt: new Date(job.timestamp).toISOString(), processedOn: job.processedOn ? new Date(job.processedOn).toISOString() : null, finishedOn: job.finishedOn ? new Date(job.finishedOn).toISOString() : null, failedReason: job.failedReason || null, }; } /** * Get job result */ @Get(':id/result') @ApiOperation({ summary: 'Get completed job result' }) async getJobResult(@Param('id') jobId: string) { const job = await this.findJobById(jobId); if (!job) { throw new NotFoundException(`Job ${jobId} not found`); } const state = await job.getState(); if (state !== 'completed') { return { jobId: job.id, status: this.mapBullState(state), result: null, message: 'Job has not completed yet', }; } return { jobId: job.id, status: JobStatus.COMPLETED, result: job.returnvalue, }; } // ========== HELPERS ========== private getQueueForJobType(type: JobType): Queue { if ( type === JobType.GENERATE_SCRIPT || type === JobType.REGENERATE_SEGMENT || type === JobType.REGENERATE_PARTIAL || type === JobType.REWRITE_SEGMENT ) { return this.scriptQueue; } if ( type === JobType.DEEP_RESEARCH || type === JobType.DISCOVER_QUESTIONS ) { return this.researchQueue; } if ( type === JobType.NEURO_ANALYSIS || type === JobType.YOUTUBE_AUDIT || type === JobType.COMMERCIAL_BRIEF || type === JobType.GENERATE_VISUAL_ASSETS ) { return this.analysisQueue; } if ( type === JobType.GENERATE_SEGMENT_IMAGE || type === JobType.GENERATE_THUMBNAIL ) { return this.imageQueue; } throw new Error(`Unknown job type: ${type}`); } private async findJobById(jobId: string) { const queues = [ this.scriptQueue, this.researchQueue, this.analysisQueue, this.imageQueue, ]; for (const queue of queues) { const job = await queue.getJob(jobId); if (job) return job; } return null; } private mapBullState(state: string): JobStatus { switch (state) { case 'completed': return JobStatus.COMPLETED; case 'failed': return JobStatus.FAILED; case 'active': return JobStatus.PROCESSING; default: return JobStatus.QUEUED; } } }