From ca38cee6e3abea722dcdd72f9329816764d5a788 Mon Sep 17 00:00:00 2001 From: LeoMortari Date: Sun, 2 Nov 2025 00:07:42 -0300 Subject: [PATCH] Adiciona criacao de video --- .gitignore | 2 + prisma/schema.prisma | 3 +- src/app.module.ts | 7 +-- src/modules/auth/auth.service.ts | 10 +++- src/modules/videos/dto/create-video-dto.ts | 30 ++++------ src/modules/videos/videos.controller.ts | 17 ++++-- src/modules/videos/videos.service.ts | 69 +++++++++++++++++++++- src/shared/url.ts | 1 + 8 files changed, 101 insertions(+), 38 deletions(-) create mode 100644 src/shared/url.ts diff --git a/.gitignore b/.gitignore index 6adf6fc..3c51326 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,5 @@ pids report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json /generated/prisma + +/generated/prisma diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fe8e078..4574b8b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -44,8 +44,7 @@ model videos { situation video_situation error_message String? @db.VarChar(244) clips_quantity Int? - times Json? - duration Unsupported("interval")? + duration String? @db.VarChar(16) title String? @db.VarChar(244) filename String? @db.VarChar(244) videoid String? @db.VarChar(244) diff --git a/src/app.module.ts b/src/app.module.ts index eff1bcc..c5becc4 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -9,9 +9,6 @@ import { VideosController } from './modules/videos/videos.controller'; import { UsuariosModule } from './modules/usuarios/usuarios.module'; import { LoggerMiddleware } from './middleware/logger.middleware'; import { RolesGuard } from './modules/auth/roles.guard'; -import { OllamaModule } from './modules/ollama/ollama.module'; -import { GeminiService } from './gemini/gemini.service'; -import { GeminiModule } from './gemini/gemini.module'; @Module({ imports: [ @@ -20,11 +17,9 @@ import { GeminiModule } from './gemini/gemini.module'; VideosModule, AuthModule, UsuariosModule, - OllamaModule, - GeminiModule, ], controllers: [AppController], - providers: [AppService, RolesGuard, GeminiService], + providers: [AppService, RolesGuard], }) export class AppModule { configure(consumer: MiddlewareConsumer) { diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index dde50f7..2ec15ce 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,6 +1,10 @@ import axios, { isAxiosError } from 'axios'; -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; import { LoginDto } from './dto/login.dto'; @@ -42,10 +46,10 @@ export class AuthService { } catch (error) { if (isAxiosError(error)) { console.error(error.response?.data); - throw new UnauthorizedException(error.response?.data); + throw new BadRequestException(error.response?.data); } - throw new UnauthorizedException('Usuário ou senha inválidos'); + throw new BadRequestException('Usuário ou senha inválidos'); } } diff --git a/src/modules/videos/dto/create-video-dto.ts b/src/modules/videos/dto/create-video-dto.ts index 27819f3..b8245ee 100644 --- a/src/modules/videos/dto/create-video-dto.ts +++ b/src/modules/videos/dto/create-video-dto.ts @@ -1,7 +1,4 @@ -import dayjs from 'dayjs'; -import duration from 'dayjs/plugin/duration'; - -import { Transform } from 'class-transformer'; +import { Type } from 'class-transformer'; import { IsArray, IsNumber, @@ -11,8 +8,6 @@ import { MaxLength, } from 'class-validator'; -dayjs.extend(duration); - export class CreateVideoDto { @IsOptional() @IsString({ message: 'Id deve ser uma string' }) @@ -46,34 +41,29 @@ export class CreateVideoDto { @MaxLength(244) videoid?: string; - @IsOptional() - @IsString({ message: 'Duration deve ser uma string' }) - @Transform(({ value }: { value: number }) => { - const duration = dayjs.duration(value, 'seconds'); - - return duration.format('HH:mm:ss'); - }) - duration?: string; + @IsNumber() + @Type(() => Number) + duration!: number; @IsOptional() @IsString() @MaxLength(244) description?: string; + @IsString() + @MaxLength(244) + qualidade: string; + @IsOptional() @IsUrl() @MaxLength(244) transcriptionUrl?: string; @IsOptional() + @Type(() => Number) @IsNumber( { allowNaN: false, allowInfinity: false }, { message: 'Timestamp deve ser um número' }, ) - @Transform(({ value }: { value: number }) => { - const duration = dayjs(value * 1000); - - return duration.format('DD/MM/YYYY HH:mm:ss'); - }) - timestamp?: string; + timestamp?: number; } diff --git a/src/modules/videos/videos.controller.ts b/src/modules/videos/videos.controller.ts index 35fa046..156e96a 100644 --- a/src/modules/videos/videos.controller.ts +++ b/src/modules/videos/videos.controller.ts @@ -1,13 +1,15 @@ import { + Body, Controller, + Delete, Get, Param, - Query, Patch, - Body, - Delete, - UseGuards, Post, + Query, + UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { Prisma, videos, video_situation } from 'generated/prisma'; @@ -57,6 +59,13 @@ export class VideosController { } @Post() + @UsePipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ) @Roles('user', 'admin') async create(@Body() body: CreateVideoDto): Promise { await this.videosService.createNewVideo(body); diff --git a/src/modules/videos/videos.service.ts b/src/modules/videos/videos.service.ts index 3d8350f..e7f66a2 100644 --- a/src/modules/videos/videos.service.ts +++ b/src/modules/videos/videos.service.ts @@ -12,6 +12,24 @@ import { PaginatedResponse } from '@shared/dto/paginated'; import { ListVideosQueryDto } from './dto/list-videos-query.dto'; import { VideoMetadataDto } from '@shared/dto/video-metadata'; import { CreateVideoDto } from './dto/create-video-dto'; +import { N8N_REMOTE_SERVER } from '@/shared/url'; + +const formatSecondsToClock = (totalSeconds: number): string => { + if (!Number.isFinite(totalSeconds)) { + return '00:00:00'; + } + + const safeSeconds = Math.max(0, Math.floor(totalSeconds)); + const hours = Math.floor(safeSeconds / 3600) + .toString() + .padStart(2, '0'); + const minutes = Math.floor((safeSeconds % 3600) / 60) + .toString() + .padStart(2, '0'); + const seconds = (safeSeconds % 60).toString().padStart(2, '0'); + + return `${hours}:${minutes}:${seconds}`; +}; @Injectable() export class VideosService { @@ -137,17 +155,62 @@ export class VideosService { } async createNewVideo(data: CreateVideoDto): Promise { - const { url, videoid } = data; + const { url, videoid, duration, qualidade, title, timestamp } = data; + + const query: Prisma.videosWhereInput = { url }; + + if (videoid) { + query.videoid = videoid; + } const searchUrl = await this.prisma.videos.findFirst({ - where: { url, videoid }, + where: query, }); - if (!isEmpty(searchUrl)) { + if (searchUrl && !isEmpty(searchUrl)) { throw new Error( `Esta video já foi renderizado, e se encontra na situação ${searchUrl.situation}`, ); } + + const formattedDuration = Number.isFinite(duration) + ? formatSecondsToClock(duration) + : undefined; + + const createData: Prisma.videosCreateInput = { + url, + situation: video_situation.FILA, + }; + + if (videoid) { + createData.videoid = videoid; + } + + if (title) { + createData.title = title; + } + + if (formattedDuration) { + createData.duration = formattedDuration; + } + + if (typeof timestamp === 'number' && Number.isFinite(timestamp)) { + createData.datetime_download = new Date(timestamp * 1000); + } + + await this.prisma.videos.create({ + data: createData, + }); + + // const params = { + // url, + // videoid, + // qualidade: qualidade === 'automatic' ? 'medium' : qualidade, + // }; + + // await axios.get(`${N8N_REMOTE_SERVER}/webhook/download-video`, { + // params, + // }); } async update(id: number, data: Prisma.videosUpdateInput): Promise { diff --git a/src/shared/url.ts b/src/shared/url.ts new file mode 100644 index 0000000..06b63b6 --- /dev/null +++ b/src/shared/url.ts @@ -0,0 +1 @@ +export const N8N_REMOTE_SERVER = 'https://becoming-poetic-lamb.ngrok-free.app';