Adiciona criacao de video

This commit is contained in:
LeoMortari
2025-11-02 00:07:42 -03:00
parent 43be1244c7
commit ca38cee6e3
8 changed files with 101 additions and 38 deletions

2
.gitignore vendored
View File

@@ -57,3 +57,5 @@ pids
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
/generated/prisma /generated/prisma
/generated/prisma

View File

@@ -44,8 +44,7 @@ model videos {
situation video_situation situation video_situation
error_message String? @db.VarChar(244) error_message String? @db.VarChar(244)
clips_quantity Int? clips_quantity Int?
times Json? duration String? @db.VarChar(16)
duration Unsupported("interval")?
title String? @db.VarChar(244) title String? @db.VarChar(244)
filename String? @db.VarChar(244) filename String? @db.VarChar(244)
videoid String? @db.VarChar(244) videoid String? @db.VarChar(244)

View File

@@ -9,9 +9,6 @@ import { VideosController } from './modules/videos/videos.controller';
import { UsuariosModule } from './modules/usuarios/usuarios.module'; import { UsuariosModule } from './modules/usuarios/usuarios.module';
import { LoggerMiddleware } from './middleware/logger.middleware'; import { LoggerMiddleware } from './middleware/logger.middleware';
import { RolesGuard } from './modules/auth/roles.guard'; 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({ @Module({
imports: [ imports: [
@@ -20,11 +17,9 @@ import { GeminiModule } from './gemini/gemini.module';
VideosModule, VideosModule,
AuthModule, AuthModule,
UsuariosModule, UsuariosModule,
OllamaModule,
GeminiModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService, RolesGuard, GeminiService], providers: [AppService, RolesGuard],
}) })
export class AppModule { export class AppModule {
configure(consumer: MiddlewareConsumer) { configure(consumer: MiddlewareConsumer) {

View File

@@ -1,6 +1,10 @@
import axios, { isAxiosError } from 'axios'; import axios, { isAxiosError } from 'axios';
import { Injectable, UnauthorizedException } from '@nestjs/common'; import {
BadRequestException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
@@ -42,10 +46,10 @@ export class AuthService {
} catch (error) { } catch (error) {
if (isAxiosError(error)) { if (isAxiosError(error)) {
console.error(error.response?.data); 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');
} }
} }

View File

@@ -1,7 +1,4 @@
import dayjs from 'dayjs'; import { Type } from 'class-transformer';
import duration from 'dayjs/plugin/duration';
import { Transform } from 'class-transformer';
import { import {
IsArray, IsArray,
IsNumber, IsNumber,
@@ -11,8 +8,6 @@ import {
MaxLength, MaxLength,
} from 'class-validator'; } from 'class-validator';
dayjs.extend(duration);
export class CreateVideoDto { export class CreateVideoDto {
@IsOptional() @IsOptional()
@IsString({ message: 'Id deve ser uma string' }) @IsString({ message: 'Id deve ser uma string' })
@@ -46,34 +41,29 @@ export class CreateVideoDto {
@MaxLength(244) @MaxLength(244)
videoid?: string; videoid?: string;
@IsOptional() @IsNumber()
@IsString({ message: 'Duration deve ser uma string' }) @Type(() => Number)
@Transform(({ value }: { value: number }) => { duration!: number;
const duration = dayjs.duration(value, 'seconds');
return duration.format('HH:mm:ss');
})
duration?: string;
@IsOptional() @IsOptional()
@IsString() @IsString()
@MaxLength(244) @MaxLength(244)
description?: string; description?: string;
@IsString()
@MaxLength(244)
qualidade: string;
@IsOptional() @IsOptional()
@IsUrl() @IsUrl()
@MaxLength(244) @MaxLength(244)
transcriptionUrl?: string; transcriptionUrl?: string;
@IsOptional() @IsOptional()
@Type(() => Number)
@IsNumber( @IsNumber(
{ allowNaN: false, allowInfinity: false }, { allowNaN: false, allowInfinity: false },
{ message: 'Timestamp deve ser um número' }, { message: 'Timestamp deve ser um número' },
) )
@Transform(({ value }: { value: number }) => { timestamp?: number;
const duration = dayjs(value * 1000);
return duration.format('DD/MM/YYYY HH:mm:ss');
})
timestamp?: string;
} }

View File

@@ -1,13 +1,15 @@
import { import {
Body,
Controller, Controller,
Delete,
Get, Get,
Param, Param,
Query,
Patch, Patch,
Body,
Delete,
UseGuards,
Post, Post,
Query,
UseGuards,
UsePipes,
ValidationPipe,
} from '@nestjs/common'; } from '@nestjs/common';
import { Prisma, videos, video_situation } from 'generated/prisma'; import { Prisma, videos, video_situation } from 'generated/prisma';
@@ -57,6 +59,13 @@ export class VideosController {
} }
@Post() @Post()
@UsePipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
)
@Roles('user', 'admin') @Roles('user', 'admin')
async create(@Body() body: CreateVideoDto): Promise<void> { async create(@Body() body: CreateVideoDto): Promise<void> {
await this.videosService.createNewVideo(body); await this.videosService.createNewVideo(body);

View File

@@ -12,6 +12,24 @@ import { PaginatedResponse } from '@shared/dto/paginated';
import { ListVideosQueryDto } from './dto/list-videos-query.dto'; import { ListVideosQueryDto } from './dto/list-videos-query.dto';
import { VideoMetadataDto } from '@shared/dto/video-metadata'; import { VideoMetadataDto } from '@shared/dto/video-metadata';
import { CreateVideoDto } from './dto/create-video-dto'; 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() @Injectable()
export class VideosService { export class VideosService {
@@ -137,17 +155,62 @@ export class VideosService {
} }
async createNewVideo(data: CreateVideoDto): Promise<void> { async createNewVideo(data: CreateVideoDto): Promise<void> {
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({ const searchUrl = await this.prisma.videos.findFirst({
where: { url, videoid }, where: query,
}); });
if (!isEmpty(searchUrl)) { if (searchUrl && !isEmpty(searchUrl)) {
throw new Error( throw new Error(
`Esta video já foi renderizado, e se encontra na situação ${searchUrl.situation}`, `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<videos> { async update(id: number, data: Prisma.videosUpdateInput): Promise<videos> {

1
src/shared/url.ts Normal file
View File

@@ -0,0 +1 @@
export const N8N_REMOTE_SERVER = 'https://becoming-poetic-lamb.ngrok-free.app';