Adiciona controller do Ollama e reestrutura arquitetura

This commit is contained in:
LeoMortari
2025-09-30 20:30:22 -03:00
parent 76b7670fba
commit f7857fdcbc
31 changed files with 267 additions and 105 deletions

View File

@@ -0,0 +1,59 @@
import {
IsEnum,
IsInt,
IsISO8601,
IsOptional,
IsString,
IsUrl,
MaxLength,
} from 'class-validator';
import { video_situation } from '@root/generated/prisma';
export class CreateVideoDto {
@IsUrl()
@MaxLength(244)
url!: string;
@IsOptional()
@IsEnum(video_situation)
situation?: video_situation;
@IsOptional()
@IsString()
@MaxLength(244)
error_message?: string;
@IsOptional()
@IsInt()
clips_quantity?: number;
@IsOptional()
times?: unknown;
@IsOptional()
@IsString()
@MaxLength(244)
title?: string;
@IsOptional()
@IsString()
@MaxLength(244)
filename?: string;
@IsOptional()
@IsString()
@MaxLength(244)
videoid?: string;
@IsOptional()
@IsISO8601()
datetime_download?: string;
@IsOptional()
@IsISO8601()
datetime_convert?: string;
@IsOptional()
@IsISO8601()
datetime_posted?: string;
}

View File

@@ -0,0 +1,40 @@
import {
IsEnum,
IsOptional,
IsString,
IsBoolean,
IsNumber,
} from 'class-validator';
import { video_situation } from 'generated/prisma';
import { Transform } from 'class-transformer';
export class ListVideosQueryDto {
@IsEnum(video_situation)
@IsOptional()
@Transform(
({ value }: { value: string }) => value?.toUpperCase() as video_situation,
)
situation?: video_situation;
@IsString()
@IsOptional()
title?: string;
@IsNumber()
@IsOptional()
@Transform(({ value }) => (value ? Number(value) : 1))
page?: number;
@IsNumber()
@IsOptional()
@Transform(({ value }) => (value ? Number(value) : 10))
perPage?: number = 10;
@IsOptional()
direction: 'asc' | 'desc' = 'desc';
@Transform(({ value }) => value !== 'false')
@IsBoolean()
@IsOptional()
pageable: boolean = true;
}

View File

@@ -0,0 +1,41 @@
import { Expose, Transform, TransformFnParams } from 'class-transformer';
import dayjs from 'dayjs';
import { video_situation } from 'generated/prisma';
export class VideoResponseDto {
@Expose()
id!: number;
@Expose()
title!: string | null;
@Expose()
url!: string;
@Expose()
situation!: video_situation;
@Expose()
@Transform(({ value }: TransformFnParams) =>
typeof value === 'number' ? value : 0,
)
clips_quantity!: number;
@Expose()
@Transform(({ value }: TransformFnParams) =>
typeof value === 'string' ? value : '',
)
videoid!: string;
@Expose()
@Transform(({ value }: TransformFnParams) =>
typeof value === 'string' ? value : '',
)
filename!: string;
@Expose()
@Transform(({ value }: TransformFnParams) =>
value ? dayjs(value as Date | string).format('DD/MM/YYYY HH:mm:ss') : '',
)
datetime_download!: string;
}

View File

@@ -0,0 +1,83 @@
import {
Controller,
Get,
Param,
Query,
Patch,
Body,
Delete,
UseGuards,
} from '@nestjs/common';
import { Prisma, videos, video_situation } from 'generated/prisma';
import { VideosService } from './videos.service';
import { VideoResponseDto } from './dto/video-response.dto';
import { KeycloakAuthGuard } from '../auth/keycloak-auth.guard';
import { Roles } from '../auth/decorator/roles.decorator';
import { ListVideosQueryDto } from './dto/list-videos-query.dto';
import { VideoMetadataDto } from 'src/shared/dto/video-metadata';
type PaginatedResponse<T> = {
content: T[];
pagination: {
page: number;
perPage: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
};
@Controller('videos')
@UseGuards(KeycloakAuthGuard)
export class VideosController {
constructor(private readonly videosService: VideosService) {}
@Get('situacoes')
@Roles('user', 'admin')
getSituacao(): video_situation[] {
return Object.values(video_situation) as video_situation[];
}
@Get('search')
@Roles('user', 'admin')
getVideoMetadata(
@Query() { url }: { url: string },
): Promise<VideoMetadataDto> {
return this.videosService.getVideoMetadata(url);
}
@Get()
@Roles('user', 'admin')
async list(
@Query() query: ListVideosQueryDto,
): Promise<PaginatedResponse<VideoResponseDto> | VideoResponseDto[]> {
if (query.pageable || query.page || query.perPage) {
return this.videosService.listPaginated(query);
}
return this.videosService.list({
situation: query.situation,
title: query.title,
});
}
@Get(':id')
async get(@Param('id') id: string): Promise<videos | null> {
return this.videosService.get(Number(id));
}
@Patch(':id')
async update(
@Param('id') id: string,
@Body() body: Prisma.videosUpdateInput,
): Promise<videos> {
return this.videosService.update(Number(id), body);
}
@Delete(':id')
async delete(@Param('id') id: string): Promise<videos> {
return this.videosService.delete(Number(id));
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { VideosService } from './videos.service';
import { VideosController } from './videos.controller';
import { PrismaModule } from '@prisma/prisma.module';
import { KeycloakAuthGuard } from '@modules/auth/keycloak-auth.guard';
@Module({
imports: [PrismaModule],
providers: [VideosService, KeycloakAuthGuard],
controllers: [VideosController],
})
export class VideosModule {}

View File

@@ -0,0 +1,148 @@
import axios from 'axios';
import { Injectable } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { Prisma, videos, video_situation } from 'generated/prisma';
import { PrismaService } from '@prisma/prisma.service';
import { VideoResponseDto } from './dto/video-response.dto';
import { PaginatedResponse } from '@shared/dto/paginated';
import { ListVideosQueryDto } from './dto/list-videos-query.dto';
import { VideoMetadataDto } from '@shared/dto/video-metadata';
@Injectable()
export class VideosService {
constructor(private readonly prisma: PrismaService) {}
async list({
situation,
title,
}: {
situation?: video_situation;
title?: string;
}): Promise<VideoResponseDto[]> {
const where: Prisma.videosWhereInput = situation ? { situation } : {};
if (title) {
where.title = {
contains: title,
};
}
const data = await this.prisma.videos.findMany({
where,
orderBy: { id: 'desc' },
select: {
id: true,
title: true,
url: true,
situation: true,
clips_quantity: true,
videoid: true,
filename: true,
datetime_download: true,
},
});
return plainToInstance(VideoResponseDto, data, {
excludeExtraneousValues: true,
});
}
async listPaginated(
query: ListVideosQueryDto,
): Promise<PaginatedResponse<VideoResponseDto>> {
const page = Number(query.page ?? 1);
const perPage = Number(query.perPage ?? 1);
const direction = query.direction ?? 'desc';
const skip = page > 0 ? (page - 1) * perPage : 0;
const where: Prisma.videosWhereInput = query.situation
? { situation: query.situation }
: {};
if (query.title) {
where.title = {
contains: query.title,
};
}
const [rows, total] = await Promise.all([
this.prisma.videos.findMany({
where,
orderBy: { id: direction },
skip,
take: perPage,
select: {
id: true,
title: true,
url: true,
situation: true,
clips_quantity: true,
videoid: true,
filename: true,
datetime_download: true,
},
}),
this.prisma.videos.count({ where }),
]);
const content: VideoResponseDto[] = plainToInstance(
VideoResponseDto,
rows,
{ excludeExtraneousValues: true },
);
const totalPages = Math.max(1, Math.ceil(total / perPage));
return {
content,
pagination: {
page,
direction,
perPage,
total,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1,
},
};
}
async get(id: number): Promise<videos | null> {
return this.prisma.videos.findUnique({
where: { id },
});
}
async getVideoMetadata(url: string): Promise<VideoMetadataDto> {
try {
const { data } = await axios.get<VideoMetadataDto>(
`${process.env.YOUTUBE_API_URL}/get-video-metadata`,
{
params: {
url,
},
},
);
return plainToInstance(VideoMetadataDto, data, {
excludeExtraneousValues: true,
});
} catch {
throw new Error('Erro ao obter metadados do vídeo');
}
}
async update(id: number, data: Prisma.videosUpdateInput): Promise<videos> {
return this.prisma.videos.update({
where: { id },
data,
});
}
async delete(id: number): Promise<videos> {
return this.prisma.videos.delete({
where: { id },
});
}
}