import { Controller, Get, Post, Patch, Delete, Param, Body, Query, HttpCode, HttpStatus, Logger, ParseUUIDPipe, Req, UploadedFile, UseInterceptors, BadRequestException, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiConsumes, } from '@nestjs/swagger'; import { FileInterceptor } from '@nestjs/platform-express'; import { ProjectsService } from './projects.service'; import { CreateProjectDto, UpdateProjectDto, CreateFromTweetDto, CreateFromYoutubeDto, CreateFromDocumentDto, CreateFromExtractedTextDto, CreateFromTextDto, } from './dto/project.dto'; @ApiTags('projects') @ApiBearerAuth() @Controller('projects') export class ProjectsController { private readonly logger = new Logger(ProjectsController.name); constructor(private readonly projectsService: ProjectsService) {} /** * Yeni bir video projesi oluşturur (DRAFT durumunda). */ @Post() @ApiOperation({ summary: 'Yeni video projesi oluştur' }) @ApiResponse({ status: 201, description: 'Proje başarıyla oluşturuldu' }) async create(@Body() dto: CreateProjectDto, @Req() req: any) { const userId = req.user?.id || req.user?.sub; this.logger.log(`Yeni proje oluşturuluyor: "${dto.title}"`); return this.projectsService.create(userId, dto); } /** * Kullanıcının tüm projelerini listeler. */ @Get() @ApiOperation({ summary: 'Tüm projeleri listele' }) @ApiQuery({ name: 'page', required: false, type: Number }) @ApiQuery({ name: 'limit', required: false, type: Number }) @ApiQuery({ name: 'status', required: false, type: String }) @ApiResponse({ status: 200, description: 'Proje listesi' }) async findAll( @Req() req: any, @Query('page') page?: number, @Query('limit') limit?: number, @Query('status') status?: string, ) { const userId = req.user?.id || req.user?.sub; return this.projectsService.findAll(userId, { page: page || 1, limit: limit || 10, status, }); } /** * Render kuyruğu genel görünümü — aktif, bekleyen ve son tamamlanan işler. */ @Get('render-queue') @ApiOperation({ summary: 'Render kuyruğu genel görünümünü getir' }) @ApiResponse({ status: 200, description: 'Render kuyruk özeti' }) async getRenderQueue(@Req() req: any) { const userId = req.user?.id || req.user?.sub; return this.projectsService.getRenderQueue(userId); } /** * Tek bir projeyi sahneleri ve medya asset'leriyle birlikte getirir. */ @Get(':id') @ApiOperation({ summary: 'Proje detaylarını getir' }) @ApiResponse({ status: 200, description: 'Proje detayları' }) @ApiResponse({ status: 404, description: 'Proje bulunamadı' }) async findOne(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) { const userId = req.user?.id || req.user?.sub; return this.projectsService.findOne(userId, id); } /** * Projeyi günceller (sadece DRAFT durumundaki projeler güncellenebilir). */ @Patch(':id') @ApiOperation({ summary: 'Projeyi güncelle' }) @ApiResponse({ status: 200, description: 'Proje güncellendi' }) async update( @Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateProjectDto, @Req() req: any, ) { const userId = req.user?.id || req.user?.sub; this.logger.log(`Proje güncelleniyor: ${id}`); return this.projectsService.update(userId, id, dto); } /** * Projeyi soft-delete ile siler. */ @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ summary: 'Projeyi sil (soft delete)' }) @ApiResponse({ status: 204, description: 'Proje silindi' }) async remove(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) { const userId = req.user?.id || req.user?.sub; this.logger.log(`Proje siliniyor: ${id}`); return this.projectsService.remove(userId, id); } /** * AI ile senaryo üretimini tetikler. */ @Post(':id/generate-script') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'AI ile senaryo üret (Gemini)' }) @ApiResponse({ status: 200, description: 'Senaryo üretildi ve kaydedildi' }) async generateScript( @Param('id', ParseUUIDPipe) id: string, @Req() req: any, ) { const userId = req.user?.id || req.user?.sub; this.logger.log(`Senaryo üretimi başlatılıyor: ${id}`); return this.projectsService.generateScript(userId, id); } /** * Senaryoyu onaylar ve video üretim kuyruğuna gönderir. */ @Post(':id/approve') @HttpCode(HttpStatus.ACCEPTED) @ApiOperation({ summary: 'Senaryoyu onayla ve video üretimini başlat' }) @ApiResponse({ status: 202, description: 'Video üretimi kuyruğa eklendi' }) async approveAndStartGeneration( @Param('id', ParseUUIDPipe) id: string, @Body() body: { ttsProvider?: string; visualEffect?: string }, @Req() req: any, ) { const userId = req.user?.id || req.user?.sub; this.logger.log(`Proje onaylanıyor ve kuyruğa gönderiliyor: ${id}`); return this.projectsService.approveAndQueueGeneration(userId, id, body?.ttsProvider, body?.visualEffect); } /** * Aktif render işlemini iptal eder. */ @Post(':id/cancel-render') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Aktif render işlemini iptal et' }) @ApiResponse({ status: 200, description: 'Render işlemi iptal edildi' }) async cancelRender(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) { const userId = req.user?.id || req.user?.sub; this.logger.log(`Render iptal isteği: ${id}`); return this.projectsService.cancelRenderJob(userId, id); } /** * X/Twitter tweet URL'sinden otomatik proje oluşturur ve senaryo üretir. * Tweet çekilir → prompt'a dönüştürülür → AI senaryo üretir → proje kaydedilir. */ @Post('from-tweet') @HttpCode(HttpStatus.CREATED) @ApiOperation({ summary: "X/Twitter tweet'ten proje oluştur" }) @ApiResponse({ status: 201, description: "Tweet'ten proje oluşturuldu ve senaryo üretildi", }) @ApiResponse({ status: 400, description: "Geçersiz tweet URL'si veya tweet bulunamadı", }) async createFromTweet(@Body() dto: CreateFromTweetDto, @Req() req: any) { const userId = req.user?.id || req.user?.sub; this.logger.log(`Tweet'ten proje oluşturuluyor: ${dto.tweetUrl}`); return this.projectsService.createFromTweet(userId, dto); } /** * YouTube URL'sinden otomatik proje oluşturur ve senaryo üretir. */ @Post('from-youtube') @HttpCode(HttpStatus.CREATED) @ApiOperation({ summary: 'YouTube videosundan proje oluştur' }) @ApiResponse({ status: 201, description: 'YouTube videosundan proje oluşturuldu ve senaryo üretildi', }) @ApiResponse({ status: 400, description: "Geçersiz YouTube URL'si veya video bulunamadı", }) async createFromYoutube(@Body() dto: CreateFromYoutubeDto, @Req() req: any) { const userId = req.user?.id || req.user?.sub; this.logger.log(`YouTube'dan proje oluşturuluyor: ${dto.youtubeUrl}`); return this.projectsService.createFromYoutube(userId, dto); } /** * Serbest metin veya fikir üzerinden proje oluşturur. */ @Post('from-text') @HttpCode(HttpStatus.CREATED) @ApiOperation({ summary: 'Serbest metinden proje oluştur' }) @ApiResponse({ status: 201, description: 'Metinden proje oluşturuldu ve senaryo üretildi', }) async createFromText(@Body() dto: CreateFromTextDto, @Req() req: any) { const userId = req.user?.id || req.user?.sub; this.logger.log(`Serbest metinden proje oluşturuluyor...`); return this.projectsService.createFromText(userId, dto); } /** * Yüklenen dokümandan (Word, PDF, Excel vb.) otomatik proje oluşturur. */ @Post('from-document') @HttpCode(HttpStatus.CREATED) @UseInterceptors(FileInterceptor('file')) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Dosyadan/Dokümandan proje oluştur' }) @ApiResponse({ status: 201, description: 'Belgeden proje oluşturuldu ve senaryo üretildi', }) async createFromDocument( @UploadedFile() file: Express.Multer.File, @Body() dto: CreateFromDocumentDto, @Req() req: any, ) { const userId = req.user?.id || req.user?.sub; this.logger.log(`Dosyadan proje oluşturuluyor: ${file?.originalname}`); if (!file) { throw new BadRequestException('Dosya yüklenmedi'); } return this.projectsService.createFromDocument(userId, file, dto); } /** * Doküman yüklenip metni çıkarılır ve video konu önerileri üretilir. */ @Post('extract-document-topics') @HttpCode(HttpStatus.OK) @UseInterceptors(FileInterceptor('file')) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Dosyadan metin çıkar ve konu önerileri al' }) @ApiResponse({ status: 200, description: 'Metin ve konular başarıyla çıkarıldı', }) async extractDocumentTopics(@UploadedFile() file: Express.Multer.File) { this.logger.log( `Dosyadan metin ve konular çıkarılıyor: ${file?.originalname}`, ); if (!file) { throw new BadRequestException('Dosya yüklenmedi'); } return this.projectsService.extractDocumentTopics(file); } /** * Extracted text ve seçilen konu üzerinden doğrudan proje oluşturur. */ @Post('document-from-topic') @HttpCode(HttpStatus.CREATED) @ApiOperation({ summary: 'Seçilen konu ve metin ile proje oluştur' }) @ApiResponse({ status: 201, description: 'Seçilen konu baz alınarak proje oluşturuldu', }) async createFromTopic( @Body() dto: CreateFromExtractedTextDto, @Req() req: any, ) { const userId = req.user?.id || req.user?.sub; this.logger.log( `Metin ve konu üzerinden proje oluşturuluyor. Konu: ${dto.topic}`, ); return this.projectsService.createFromExtractedText(userId, dto); } /** * Tekil sahne güncelleme (narrasyon, görsel prompt, süre). */ @Patch(':id/scenes/:sceneId') @ApiOperation({ summary: 'Sahneyi güncelle' }) @ApiResponse({ status: 200, description: 'Sahne güncellendi' }) async updateScene( @Param('id', ParseUUIDPipe) id: string, @Param('sceneId', ParseUUIDPipe) sceneId: string, @Body() body: { narrationText?: string; visualPrompt?: string; subtitleText?: string; duration?: number; }, @Req() req: any, ) { const userId = req.user?.id || req.user?.sub; this.logger.log(`Sahne güncelleniyor: ${sceneId} (proje: ${id})`); return this.projectsService.updateScene(userId, id, sceneId, body); } /** * Tekil sahneyi AI ile yeniden üretir. */ @Post(':id/scenes/:sceneId/regenerate') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Sahneyi AI ile yeniden üret' }) @ApiResponse({ status: 200, description: 'Sahne yeniden üretildi' }) async regenerateScene( @Param('id', ParseUUIDPipe) id: string, @Param('sceneId', ParseUUIDPipe) sceneId: string, @Req() req: any, ) { const userId = req.user?.id || req.user?.sub; this.logger.log(`Sahne yeniden üretiliyor: ${sceneId} (proje: ${id})`); return this.projectsService.regenerateScene(userId, id, sceneId); } @Delete(':id/media/:mediaId') @ApiOperation({ summary: 'Sahnede bulunan bir medyayı (MediaAsset) sil' }) @ApiResponse({ status: 200, description: 'Medya başarıyla silindi' }) async deleteMedia( @Param('id', ParseUUIDPipe) id: string, @Param('mediaId', ParseUUIDPipe) mediaId: string, @Req() req: any, ) { const userId = req.user?.id || req.user?.sub; this.logger.log(`Medya silme isteği. Proje: ${id}, Medya: ${mediaId}`); return this.projectsService.deleteSceneMedia(userId, id, mediaId); } /** * Proje için 5 yeni SEO-optimized başlık üretir (Gemini AI). */ @Post(':id/generate-seo-titles') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'AI ile 5 yeni SEO başlığı üret' }) @ApiResponse({ status: 200, description: 'SEO başlıkları başarıyla üretildi', }) async generateSeoTitles( @Param('id', ParseUUIDPipe) id: string, @Req() req: any, ) { const userId = req.user?.id || req.user?.sub; this.logger.log(`SEO başlık üretimi isteniyor: ${id}`); return this.projectsService.generateSeoTitles(userId, id); } /** * Proje için kapsamlı SEO ve Sosyal Medya içeriklerini yeniden üretir (Gemini AI). */ @Post(':id/generate-social-content') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'AI ile SEO ve Sosyal Medya içeriklerini üret' }) @ApiResponse({ status: 200, description: 'İçerikler başarıyla üretildi', }) async generateSocialContent( @Param('id', ParseUUIDPipe) id: string, @Req() req: any, ) { const userId = req.user?.id || req.user?.sub; this.logger.log(`SEO ve Sosyal Medya üretim isteği: ${id}`); return this.projectsService.generateSocialAndSeoContent(userId, id); } /** * Alternatif SEO başlıklarından birini seçerek projenin ana başlığını günceller. */ @Patch(':id/select-title') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'SEO başlığı seç ve proje başlığını güncelle' }) @ApiResponse({ status: 200, description: 'Başlık başarıyla güncellendi' }) async selectSeoTitle( @Param('id', ParseUUIDPipe) id: string, @Body('title') title: string, @Req() req: any, ) { const userId = req.user?.id || req.user?.sub; if (!title) { throw new BadRequestException('Başlık (title) belirtilmelidir.'); } this.logger.log(`SEO başlık seçimi: ${id} — "${title}"`); return this.projectsService.selectSeoTitle(userId, id, title); } /** * Projeyi farklı bir dile çevirir. */ @Post(':id/translate') @HttpCode(HttpStatus.CREATED) @ApiOperation({ summary: 'Projeyi farklı bir dile çevir ve kopyasını oluştur', }) @ApiResponse({ status: 201, description: 'Proje çevirisi başarıyla tamamlandı', }) async translateProject( @Param('id', ParseUUIDPipe) id: string, @Body('targetLanguage') targetLanguage: string, @Req() req: any, ) { if (!targetLanguage) { throw new BadRequestException( 'Hedef dil (targetLanguage) belirtilmelidir.', ); } const userId = req.user?.id || req.user?.sub; this.logger.log(`Proje çevirisi isteniyor: ${id} -> ${targetLanguage}`); return this.projectsService.translateProject(userId, id, targetLanguage); } /** * Sahne için ID bazında görsel üret (Gemini AI). * Kullanıcı custom prompt sağlarsa, önce prompt güncellenir ardından resim üretilir. */ @Post(':id/scenes/:sceneId/generate-image') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Sahne görselini üret' }) @ApiResponse({ status: 200, description: 'Görsel üretildi' }) async generateSceneImage( @Param('id', ParseUUIDPipe) id: string, @Param('sceneId', ParseUUIDPipe) sceneId: string, @Body() body: { customPrompt?: string }, @Req() req: any, ) { const userId = req.user?.id || req.user?.sub; this.logger.log(`Sahne görseli üretiliyor: ${sceneId} (proje: ${id})`); return this.projectsService.generateSceneImage( userId, id, sceneId, body?.customPrompt, ); } /** * Sahne görselini 4K olarak yeniden boyutlandırır (Upscale) */ @Post(':id/scenes/:sceneId/upscale-image') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Sahne görselini upscale (4K) yap' }) @ApiResponse({ status: 200, description: 'Görsel başarıyla upscale edildi' }) async upscaleSceneImage( @Param('id', ParseUUIDPipe) id: string, @Param('sceneId', ParseUUIDPipe) sceneId: string, @Req() req: any, ) { const userId = req.user?.id || req.user?.sub; this.logger.log( `Sahne görseli upscale ediliyor: ${sceneId} (proje: ${id})`, ); return this.projectsService.upscaleSceneImage(userId, id, sceneId); } }