Compare commits

...

2 Commits

Author SHA1 Message Date
Ricardo Carneiro
511b538ad3 feat: meus resumos com estrutura codificada 2025-04-21 23:01:55 -03:00
Ricardo Carneiro
7b3c63ff37 fix: diversos ajustes para deixar funcional 2025-04-20 23:33:46 -03:00
92 changed files with 4090 additions and 713 deletions

View File

@ -0,0 +1,19 @@
using SumaTube.Infra.MongoDB.Documents.UserPlan;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra.Contracts.Repositories.UserPlan
{
public interface IPersonUserRepository
{
Task<PersonUserDocument> GetByIdAsync(string id);
Task<PersonUserDocument> GetByEmailAsync(string email);
Task<IEnumerable<PersonUserDocument>> GetAllAsync();
Task CreateAsync(PersonUserDocument PersonUserDocument);
Task UpdateAsync(PersonUserDocument PersonUserDocument);
Task DeleteAsync(string id);
}
}

View File

@ -0,0 +1,14 @@
using SumaTube.Domain.Entities.Videos;
namespace SumaTube.Infra.Contracts.Repositories.Videos
{
public interface IVideoSummaryRepository
{
Task<VideoSummary> GetByIdAsync(string id);
Task<List<VideoSummary>> GetByUserIdAsync(string userId);
Task AddAsync(VideoSummary videoSummary);
Task UpdateAsync(VideoSummary videoSummary);
Task<bool> ExistsAsync(string videoId, string userId, string language);
Task<VideoSummary> GetByVideoIdAndUserIdAndLanguageAsync(string videoId, string userId, string language);
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra
{
public class VideoProcessingCompletedMessage
{
public string SessionId { get; set; }
public string UserId { get; set; }
public bool Success { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public string Transcription { get; set; }
public int Duration { get; set; }
public string ErrorMessage { get; set; }
}
}

View File

@ -0,0 +1,43 @@
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra.MongoDB.Documents.UserPlan
{
public class PersonUserDocument
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string Username { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public DateTime LastChanged { get; set; }
public bool IsActive { get; set; }
public bool IsProfileCompleted { get; set; }
public int CountryId { get; set; }
public int BusinessAreaId { get; set; }
public string DesiredName { get; set; }
public DateTime CreatedAt { get; set; }
public UserPaymentDocument CurrentPlan { get; set; }
public List<UserPaymentDocument> PastPlans { get; set; } = new List<UserPaymentDocument>();
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra.MongoDB.Documents.UserPlan
{
public class UserPaymentDocument
{
public string Id { get; set; }
public string Name { get; set; }
public decimal Value { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public bool IsActive { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra.MongoDB.Documents.Videos
{
public class VideoProcessingMessageDocument
{
public string SessionId { get; set; }
public string Url { get; set; }
public string Language { get; set; }
public VideoProcessingMessageDocument(string sessionId, string url, string language)
{
SessionId = sessionId;
Url = url;
Language = language;
}
}
}

View File

@ -0,0 +1,19 @@
using SumaTube.Domain.Entities.UserPlan;
using SumaTube.Crosscutting.Mappers;
using SumaTube.Infra.MongoDB.Documents.UserPlan;
namespace SumaTube.Infra.MongoDB.Mappers.UserPlan
{
public static class PersonUserMapper
{
public static PersonUserDocument ToDocument(this PersonUser entity)
{
return entity.MapTo<PersonUser, PersonUserDocument>();
}
public static PersonUser ToDomain(this PersonUserDocument document)
{
return document.MapTo<PersonUserDocument, PersonUser>();
}
}
}

View File

@ -0,0 +1,76 @@
using SumaTube.Infra.MongoDB;
using MongoDB.Bson.Serialization.Conventions;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Bson.Serialization.IdGenerators;
using MongoDB.Bson;
using SumaTube.Domain;
using SumaTube.Domain.Entities.UserPlan;
namespace SumaTube.Infra.MongoDB
{
public static class MongoConfig
{
public static void Configure()
{
var conventionPack = new ConventionPack { new IgnoreIfNullConvention(true) };
ConventionRegistry.Register("IgnoreIfNull", conventionPack, t => true);
if (!BsonClassMap.IsClassMapRegistered(typeof(PersonUser)))
{
BsonClassMap.RegisterClassMap<PersonUser>(cm =>
{
cm.MapIdProperty(p => p.Id)
.SetSerializer(new StringSerializer(BsonType.ObjectId))
.SetIdGenerator(StringObjectIdGenerator.Instance);
cm.MapProperty(p => p.Username);
cm.MapProperty(p => p.Name);
cm.MapProperty(p => p.Email);
cm.MapProperty(p => p.DateChanged);
cm.MapProperty(p => p.IsProfileCompleted);
cm.MapProperty(p => p.CountryId);
cm.MapProperty(p => p.BusinessAreaId);
cm.MapProperty(p => p.DesiredName);
cm.MapProperty(p => p.CreatedAt);
cm.MapProperty(p => p.Plano);
cm.MapProperty(p => p.PastPlans);
cm.MapCreator(p => new PersonUser(
p.Id.Value,
p.Username,
p.Name,
p.Email,
p.DateChanged,
p.IsProfileCompleted,
p.CountryId,
p.BusinessAreaId,
p.DesiredName,
p.CreatedAt,
p.Plano,
p.PastPlans
));
cm.AutoMap();
});
}
// Configure também as classes ValueObject se necessário
ConfigureValueObjects();
}
private static void ConfigureValueObjects()
{
if (!BsonClassMap.IsClassMapRegistered(typeof(Name)))
{
BsonClassMap.RegisterClassMap<Name>(cm =>
{
cm.AutoMap();
cm.MapCreator(n => new Name(n.FirstName, n.LastName));
});
}
// Adicione mapeamento para outros value objects conforme necessário
}
}
}

View File

@ -4,8 +4,9 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
using SumaTube.Domain.Entities.UserPlan;
namespace Blinks.Infra.MongoDB namespace SumaTube.Infra.MongoDB
{ {
public class MongoDbContext public class MongoDbContext
{ {

View File

@ -0,0 +1,40 @@
using MongoDB.Driver;
using SumaTube.Infra.MongoDB.Documents.UserPlan;
namespace SumaTube.Infra.MongoDB.Repositories.UserPlan
{
public class PersonUserRepository
{
private readonly IMongoCollection<PersonUserDocument> _collection;
public PersonUserRepository(IMongoDatabase database)
{
_collection = database.GetCollection<PersonUserDocument>("PersonUsers");
}
public async Task<PersonUserDocument> GetByIdAsync(string id)
{
return await _collection.Find(p => p.Id == id).FirstOrDefaultAsync();
}
public async Task<IEnumerable<PersonUserDocument>> GetAllAsync()
{
return await _collection.Find(_ => true).ToListAsync();
}
public async Task CreateAsync(PersonUserDocument PersonUserDocument)
{
await _collection.InsertOneAsync(PersonUserDocument);
}
public async Task UpdateAsync(PersonUserDocument PersonUserDocument)
{
await _collection.ReplaceOneAsync(p => p.Id == PersonUserDocument.Id, PersonUserDocument);
}
public async Task DeleteAsync(string id)
{
await _collection.DeleteOneAsync(p => p.Id == id);
}
}
}

View File

@ -0,0 +1,73 @@
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using SumaTube.Domain.Entities.Videos;
using SumaTube.Infra.Contracts.Repositories.Videos;
namespace SumaTube.Infra.MongoDB.Repositories.Videos
{
public class VideoSummaryRepository : IVideoSummaryRepository
{
private readonly IMongoCollection<VideoSummary> _collection;
private readonly ILogger<VideoSummaryRepository> _logger;
public VideoSummaryRepository(IMongoDatabase database, ILogger<VideoSummaryRepository> logger)
{
_collection = database.GetCollection<VideoSummary>("VideoSummaries");
_logger = logger;
}
public async Task<VideoSummary> GetByIdAsync(string id)
{
_logger.LogInformation("Obtendo resumo por ID: {Id}", id);
return await _collection.Find(x => x.Id == id).FirstOrDefaultAsync();
}
public async Task<List<VideoSummary>> GetByUserIdAsync(string userId)
{
_logger.LogInformation("Obtendo resumos do usuário: {UserId}", userId);
return await _collection.Find(x => x.UserId == userId)
.SortByDescending(x => x.RequestDate)
.ToListAsync();
}
public async Task AddAsync(VideoSummary videoSummary)
{
_logger.LogInformation("Adicionando novo resumo. ID: {Id}, VideoId: {VideoId}",
videoSummary.Id, videoSummary.VideoId);
await _collection.InsertOneAsync(videoSummary);
}
public async Task UpdateAsync(VideoSummary videoSummary)
{
_logger.LogInformation("Atualizando resumo. ID: {Id}, Status: {Status}",
videoSummary.Id, videoSummary.Status);
await _collection.ReplaceOneAsync(x => x.Id == videoSummary.Id, videoSummary);
}
public async Task<bool> ExistsAsync(string videoId, string userId, string language)
{
_logger.LogInformation("Verificando existência de resumo. VideoId: {VideoId}, UserId: {UserId}, Language: {Language}",
videoId, userId, language);
var count = await _collection.CountDocumentsAsync(x =>
x.VideoId == videoId &&
x.UserId == userId &&
x.Language == language &&
(x.Status == "REALIZADO" || x.Status == "PROCESSANDO"));
return count > 0;
}
public async Task<VideoSummary> GetByVideoIdAndUserIdAndLanguageAsync(string videoId, string userId, string language)
{
_logger.LogInformation("Obtendo resumo por VideoId: {VideoId}, UserId: {UserId}, Language: {Language}",
videoId, userId, language);
return await _collection.Find(x =>
x.VideoId == videoId &&
x.UserId == userId &&
x.Language == language)
.FirstOrDefaultAsync();
}
}
}

View File

@ -1,10 +1,10 @@
using global::MongoDB.Bson.Serialization.Attributes; using global::MongoDB.Bson.Serialization.Attributes;
using global::MongoDB.Bson; using global::MongoDB.Bson;
namespace Blinks.Infra.MongoDB namespace SumaTube.Infra.MongoDB
{ {
public class PersonUser public class UserPerson
{ {
[BsonId] [BsonId]
[BsonRepresentation(BsonType.ObjectId)] [BsonRepresentation(BsonType.ObjectId)]

View File

@ -0,0 +1,39 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
using SumaTube.Infra.Contracts.Repositories.Videos;
using SumaTube.Infra.MongoDB.Repositories.Videos;
using SumaTube.Infra.VideoSumarizer.Contracts;
using SumaTube.Infra.VideoSumarizer.Videos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra.Register
{
public static class InfraServicesRegister
{
public static IServiceCollection AddInfraServices(this IServiceCollection services, ConfigurationManager configuration)
{
var mongoConnectionString = configuration.GetConnectionString("MongoDB") ?? "mongodb://localhost:27017";
var mongoDatabaseName = configuration.GetValue<string>("MongoDB:DatabaseName") ?? "SumaTube";
services.AddSingleton<IMongoClient>(sp =>
{
return new MongoClient(mongoConnectionString);
});
services.AddScoped<IMongoDatabase>(sp =>
{
var client = sp.GetRequiredService<IMongoClient>();
return client.GetDatabase(mongoDatabaseName);
});
services.AddScoped<IVideoSumarizerService, VideoSumarizerService>();
services.AddScoped<IVideoSummaryRepository, VideoSummaryRepository>();
return services;
}
}
}

View File

@ -9,11 +9,21 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="MongoDB.Bson" Version="2.28.0" /> <PackageReference Include="MongoDB.Bson" Version="2.28.0" />
<PackageReference Include="MongoDB.Driver" Version="2.28.0" /> <PackageReference Include="MongoDB.Driver" Version="2.28.0" />
<PackageReference Include="Serilog" Version="4.0.2" /> <PackageReference Include="RabbitMQ.Client" Version="7.1.2" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" /> <PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Enrichers.Context" Version="4.6.5" /> <PackageReference Include="Serilog.Enrichers.Context" Version="4.6.5" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.0" /> <PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SumaTube.Crosscutting\SumaTube.Crosscutting.csproj" />
<ProjectReference Include="..\SumaTube.Domain\SumaTube.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Stripe\" />
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra.VideoSumarizer.Contracts
{
public interface IVideoSumarizerService
{
Task RequestVideoSummarization(string sessionId, string url, string language);
}
}

View File

@ -0,0 +1,139 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client.Events;
using RabbitMQ.Client;
using SumaTube.Infra.Contracts.Repositories.Videos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static MongoDB.Driver.WriteConcern;
namespace SumaTube.Infra.VideoSumarizer.Videos
{
public class VideoProcessingCompletedListener : BackgroundService
{
private readonly IVideoSummaryRepository _repository;
private readonly ILogger<VideoProcessingCompletedListener> _logger;
private IConnection _connection;
//private IModel _channel;
private static string _hostName = Environment.GetEnvironmentVariable("RABBITMQ_HOST") ?? "localhost";
private static string _userName = Environment.GetEnvironmentVariable("RABBITMQ_USER") ?? "guest";
private static string _password = Environment.GetEnvironmentVariable("RABBITMQ_PASSWORD") ?? "guest";
private static string _queueName = "video-processing-completed";
public VideoProcessingCompletedListener(
IVideoSummaryRepository repository,
ILogger<VideoProcessingCompletedListener> logger)
{
_repository = repository;
_logger = logger;
InitializeRabbitMQ();
}
private void InitializeRabbitMQ()
{
_logger.LogInformation("Inicializando conexão com RabbitMQ em {HostName}...", _hostName);
try
{
//var factory = new ConnectionFactory()
//{
// HostName = _hostName,
// UserName = _userName,
// Password = _password,
// DispatchConsumersAsync = true
//};
//_connection = factory.CreateConnection();
//_channel = _connection.CreateModel();
//_channel.QueueDeclare(
// queue: _queueName,
// durable: true,
// exclusive: false,
// autoDelete: false,
// arguments: null);
_logger.LogInformation("Conexão com RabbitMQ estabelecida. Aguardando mensagens na fila: {QueueName}", _queueName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao inicializar conexão com RabbitMQ: {Message}", ex.Message);
throw;
}
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
//stoppingToken.ThrowIfCancellationRequested();
//var consumer = new AsyncEventingBasicConsumer(_channel);
//consumer.Received += async (model, ea) =>
//{
// var body = ea.Body.ToArray();
// var message = Encoding.UTF8.GetString(body);
// _logger.LogInformation("Mensagem recebida: {Message}", message);
// try
// {
// var completedMessage = JsonConvert.DeserializeObject<VideoProcessingCompletedMessage>(message);
// // Buscar o resumo pelo sessionId
// var summaries = await _repository.GetByUserIdAsync(completedMessage.UserId);
// var summary = summaries.Find(s => s.SessionId == completedMessage.SessionId);
// if (summary != null)
// {
// _logger.LogInformation("Atualizando resumo. ID: {Id}, SessionId: {SessionId}",
// summary.Id, completedMessage.SessionId);
// if (completedMessage.Success)
// {
// summary.SetAsCompleted(
// completedMessage.Title,
// completedMessage.Summary,
// completedMessage.Transcription,
// completedMessage.Duration);
// }
// else
// {
// summary.SetAsFailed(completedMessage.ErrorMessage);
// }
// await _repository.UpdateAsync(summary);
// _logger.LogInformation("Resumo atualizado com sucesso. ID: {Id}, Status: {Status}",
// summary.Id, summary.Status);
// }
// else
// {
// _logger.LogWarning("Resumo não encontrado para SessionId: {SessionId}", completedMessage.SessionId);
// }
// _channel.BasicAck(ea.DeliveryTag, false);
// }
// catch (Exception ex)
// {
// _logger.LogError(ex, "Erro ao processar mensagem: {Message}", ex.Message);
// _channel.BasicNack(ea.DeliveryTag, false, true);
// }
//};
//_channel.BasicConsume(queue: _queueName, autoAck: false, consumer: consumer);
return Task.CompletedTask;
}
public override void Dispose()
{
//_channel?.Close();
//_connection?.Close();
base.Dispose();
}
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra.VideoSumarizer.Videos
{
public class VideoProcessingMessageDocument
{
public VideoProcessingMessageDocument(string sessionId, string url, string language)
{
SessionId = sessionId;
Url = url;
Language = language;
}
public string SessionId { get; private set; }
public string Url { get; private set; }
public string Language { get; private set; }
}
}

View File

@ -0,0 +1,89 @@
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using SumaTube.Infra.VideoSumarizer.Contracts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace SumaTube.Infra.VideoSumarizer.Videos
{
public class VideoSumarizerService : IVideoSumarizerService
{
private static string _hostName = Environment.GetEnvironmentVariable("RABBITMQ_HOST") ?? "localhost";
private static string _userName = Environment.GetEnvironmentVariable("RABBITMQ_USER") ?? "guest";
private static string _password = Environment.GetEnvironmentVariable("RABBITMQ_PASSWORD") ?? "guest";
private static string _queueName = "video-processing-queue";
private readonly ILogger<VideoSumarizerService> _logger;
public VideoSumarizerService(ILogger<VideoSumarizerService> logger)
{
_logger = logger;
}
public async Task RequestVideoSummarization(string sessionId, string url, string language)
{
_logger.LogInformation("### Video Processor Publisher ###");
_logger.LogInformation("Conectando ao RabbitMQ em {HostName}...", _hostName);
try
{
var factory = new ConnectionFactory()
{
HostName = _hostName,
UserName = _userName,
Password = _password
};
using (var connection = await factory.CreateConnectionAsync())
using (var channel = await connection.CreateChannelAsync())
{
await channel.QueueDeclareAsync(
queue: _queueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
var properties = new BasicProperties
{
Persistent = true
};
_logger.LogInformation("Conexão estabelecida com RabbitMQ");
if (string.IsNullOrWhiteSpace(language))
language = "pt";
// Criar objeto de mensagem
var message = new VideoProcessingMessageDocument(
sessionId,
url,
language);
// Serializar para JSON
var messageJson = JsonSerializer.Serialize(message);
var body = Encoding.UTF8.GetBytes(messageJson);
// Publicar mensagem
await channel.BasicPublishAsync(
exchange: string.Empty,
routingKey: _queueName,
mandatory: true,
basicProperties: properties,
body: body);
_logger.LogInformation("Mensagem enviada: {Message}", messageJson);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao enviar mensagem para RabbitMQ: {Message}", ex.Message);
throw;
}
}
}
}

View File

@ -0,0 +1,20 @@
using Microsoft.Extensions.DependencyInjection;
using SumaTube.Application.Videos.ApplicationServices;
using SumaTube.Application.Videos.Contracts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Application.Register
{
public static class ApplicationServiceRegister
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
services.AddScoped<IVideoApplicationService, VideoApplicationService>();
return services;
}
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BaseDomain\BaseDomain.csproj" />
<ProjectReference Include="..\SumaTuba.Infra\SumaTube.Infra.csproj" />
<ProjectReference Include="..\SumaTube.Crosscutting\SumaTube.Crosscutting.csproj" />
<ProjectReference Include="..\SumaTube.Domain\SumaTube.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="UserPlan\Contracts\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,23 @@
using SumaTube.Domain.Entities.UserPlan;
using SumaTube.Infra.Contracts.Repositories.UserPlan;
using SumaTube.Infra.MongoDB.Mappers.UserPlan;
namespace SumaTube.Application.UserPlan.ApplicationServices
{
public class PersonUserAppService
{
private readonly IPersonUserRepository _personUserRepository;
public PersonUserAppService(IPersonUserRepository personUserRepository)
{
_personUserRepository = personUserRepository;
}
public async Task<PersonUser> GetPersonUserByEmailAsync(string email)
{
var document = await _personUserRepository.GetByEmailAsync(email);
return document.ToDomain();
}
}
}

View File

@ -0,0 +1,137 @@
using Microsoft.Extensions.Logging;
using SumaTube.Application.Videos.Contracts;
using SumaTube.Domain.Entities.Videos;
using SumaTube.Infra.Contracts.Repositories.Videos;
using SumaTube.Infra.VideoSumarizer.Contracts;
using SumaTube.Infra.VideoSumarizer.Videos;
namespace SumaTube.Application.Videos.ApplicationServices
{
public class VideoApplicationService : IVideoApplicationService
{
private readonly IVideoSummaryRepository _repository;
private readonly IVideoSumarizerService _sumarizerService;
private readonly ILogger<VideoApplicationService> _logger;
public VideoApplicationService(
IVideoSummaryRepository repository,
IVideoSumarizerService sumarizerService,
ILogger<VideoApplicationService> logger)
{
_repository = repository;
_sumarizerService = sumarizerService;
_logger = logger;
}
public async Task<List<VideoSummary>> GetUserVideosAsync(string userId)
{
_logger.LogInformation("Obtendo videos do usuário: {UserId}", userId);
return await _repository.GetByUserIdAsync(userId);
}
public async Task<VideoSummary> GetVideoSummaryByIdAsync(string id, string userId)
{
_logger.LogInformation("Obtendo resumo com ID: {SummaryId} para usuário: {UserId}", id, userId);
var summary = await _repository.GetByIdAsync(id);
if (summary == null || summary.UserId != userId)
{
_logger.LogWarning("Resumo não encontrado ou acesso não autorizado. ID: {SummaryId}, UserId: {UserId}", id, userId);
return null;
}
return summary;
}
public async Task<VideoSummary> RequestVideoSummaryAsync(string youtubeUrl, string language, string userId)
{
_logger.LogInformation("Solicitando resumo para URL: {Url}, idioma: {Language}, usuário: {UserId}", youtubeUrl, language, userId);
try
{
// Extrair ID do vídeo
string videoId = ExtractVideoId(youtubeUrl);
if (string.IsNullOrEmpty(videoId))
{
_logger.LogWarning("URL do YouTube inválida: {Url}", youtubeUrl);
throw new ArgumentException("URL do YouTube inválida");
}
// Verificar se já existe um resumo para este vídeo/usuário/idioma
bool exists = await _repository.ExistsAsync(videoId, userId, language);
if (exists)
{
_logger.LogInformation("Resumo já existente para vídeo: {VideoId}, usuário: {UserId}, idioma: {Language}",
videoId, userId, language);
return await _repository.GetByVideoIdAndUserIdAndLanguageAsync(videoId, userId, language);
}
// Criar novo resumo
var sessionId = Guid.NewGuid().ToString();
var summary = VideoSummary.Create(videoId, userId, language, sessionId);
// Salvar no repositório
await _repository.AddAsync(summary);
// Enviar para processamento via RabbitMQ
_logger.LogInformation("Enviando solicitação para processamento. SessionId: {SessionId}", sessionId);
await _sumarizerService.RequestVideoSummarization(sessionId, youtubeUrl, language);
return summary;
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao solicitar resumo de vídeo: {Message}", ex.Message);
throw;
}
}
public async Task<object> CheckSummaryStatusAsync(string id, string userId)
{
_logger.LogInformation("Verificando status do resumo. ID: {SummaryId}, usuário: {UserId}", id, userId);
var summary = await _repository.GetByIdAsync(id);
if (summary == null || summary.UserId != userId)
{
_logger.LogWarning("Resumo não encontrado ou acesso não autorizado. ID: {SummaryId}, UserId: {UserId}", id, userId);
return new { status = "NOT_FOUND" };
}
return new
{
status = summary.Status,
title = summary.Title,
thumbnailUrl = summary.ThumbnailUrl,
errorMessage = summary.ErrorMessage
};
}
private string ExtractVideoId(string url)
{
try
{
var uri = new Uri(url);
if (uri.Host.Contains("youtube.com"))
{
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
return query["v"];
}
else if (uri.Host.Contains("youtu.be"))
{
var segments = uri.Segments;
return segments[segments.Length - 1].TrimEnd('/');
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao extrair ID do vídeo da URL: {Url}", url);
}
return null;
}
}
}

View File

@ -0,0 +1,17 @@
using SumaTube.Domain.Entities.Videos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Application.Videos.Contracts
{
public interface IVideoApplicationService
{
Task<List<VideoSummary>> GetUserVideosAsync(string userId);
Task<VideoSummary> GetVideoSummaryByIdAsync(string id, string userId);
Task<VideoSummary> RequestVideoSummaryAsync(string youtubeUrl, string language, string userId);
Task<object> CheckSummaryStatusAsync(string id, string userId);
}
}

View File

@ -0,0 +1,116 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog.Events;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Serilog.Extensions.Hosting;
using Microsoft.AspNetCore.Builder;
namespace SumaTube.Crosscutting.Logging.Configuration
{
public static class SerilogConfiguration
{
public static LoggerConfiguration SetLoggerConfiguration(this WebApplicationBuilder builder, LoggerConfiguration config, IServiceProvider services, IConfiguration configuration)
{
var workspace = configuration["Serilog:Properties:Workspace"];
var seqServer = configuration.GetValue<string>("Serilog:WriteTo:2:Args:serverUrl"); ;
config
.ReadFrom.Configuration(configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.Enrich.WithEnvironmentName()
.Enrich.WithMachineName()
.Enrich.WithProperty("Application", "SumaTube")
.Enrich.WithProperty("Workspace", workspace)
.WriteTo.Seq(seqServer)
;
return config;
}
public static IServiceCollection AddSerilogServices(this IServiceCollection services, IConfiguration configuration, IHostEnvironment environment)
{
// Obtenha o workspace do Seq baseado no ambiente
//var workspace = environment.IsDevelopment() ? "Dev" : "Prod";
services.AddSingleton<DiagnosticContext>();
var workspace = configuration["Serilog:Properties:Workspace"];
// Crie o logger usando a configuração
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
//.Enrich.WithProperty("Workspace", workspace) // Adiciona explicitamente o workspace
.CreateLogger();
// Registra o logger no container de DI
services.AddSingleton(Log.Logger);
return services;
}
// Método opcional para configuração direta sem usar appsettings.json
public static IServiceCollection AddSerilogServicesWithCode(
this IServiceCollection services,
IHostEnvironment environment)
{
services.AddSingleton<DiagnosticContext>();
// Defina as configurações do Seq baseado no ambiente
var (workspace, apiKey) = environment.IsDevelopment()
? ("Dev", "sua-api-key-dev")
: ("Prod", "sua-api-key-prod");
// Configuração básica para ambos ambientes
var loggerConfig = new LoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentUserName()
.Enrich.WithThreadId()
.Enrich.WithProperty("Application", "SumaTube")
.Enrich.WithProperty("Environment", environment.EnvironmentName)
.Enrich.WithProperty("Workspace", workspace);
// Adicione destinos específicos por ambiente
if (environment.IsDevelopment())
{
loggerConfig
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.WriteTo.Console()
.WriteTo.File("logs/dev-app-.log", rollingInterval: RollingInterval.Day);
}
else // Produção ou outros ambientes
{
loggerConfig
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Error)
.WriteTo.File("logs/prod-app-.log", rollingInterval: RollingInterval.Day);
}
// Adicione o Seq para ambos ambientes, mas com configurações diferentes
//loggerConfig.WriteTo.Seq(
// "http://logs-ingest.carneiro.ddnsfree.com",
// apiKey: apiKey);
loggerConfig.WriteTo.Seq(
"http://logs.carneiro.ddnsfree.com:5341");
Log.Logger = loggerConfig.CreateLogger();
// Registra o logger no container de DI
services.AddSingleton(Log.Logger);
return services;
}
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.Extensions.Logging;
namespace SumaTube.Crosscutting.Logging.Extensions
{
public static class LoggerExtensions
{
public static void LogMethodEntry<T>(this ILogger<T> logger, string methodName, params object[] parameters)
{
logger.LogInformation("Entering method {MethodName} with parameters {@Parameters}", methodName, parameters);
}
public static void LogMethodExit<T>(this ILogger<T> logger, string methodName, object result = null)
{
logger.LogInformation("Exiting method {MethodName} with result {@Result}", methodName, result);
}
public static void LogException<T>(this ILogger<T> logger, Exception exception, string message = null)
{
logger.LogError(exception, message ?? "An error occurred: {ErrorMessage}", exception.Message);
}
public static void LogPerformance<T>(this ILogger<T> logger, string operation, long elapsedMilliseconds)
{
logger.LogInformation("Performance: {Operation} took {ElapsedMilliseconds} ms", operation, elapsedMilliseconds);
}
}
}

View File

@ -0,0 +1,254 @@
namespace SumaTube.Crosscutting.Mappers
{
using System;
using System.Linq;
using System.Reflection;
public static class GenericMapper
{
public static TDestination MapTo<TSource, TDestination>(this TSource source)
where TDestination : class
{
if (source == null)
return default;
var destType = typeof(TDestination);
// Verifica se o tipo de destino tem um construtor público
var constructors = destType.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
.OrderByDescending(c => c.GetParameters().Length)
.ToList();
// Se o tipo de destino tem um construtor com parâmetros, tenta mapear para ele
if (constructors.Any() && constructors[0].GetParameters().Length > 0)
{
return MapToImmutableObject<TSource, TDestination>(source, constructors);
}
// Caso contrário, usa a abordagem de mapeamento de propriedades para objetos mutáveis
else
{
return MapToMutableObject<TSource, TDestination>(source);
}
}
private static TDestination MapToImmutableObject<TSource, TDestination>(TSource source, List<ConstructorInfo> constructors)
where TDestination : class
{
var sourceType = typeof(TSource);
var sourceProps = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
// Tenta cada construtor, começando pelo que tem mais parâmetros
foreach (var constructor in constructors)
{
var parameters = constructor.GetParameters();
if (parameters.Length == 0)
{
// Construtor sem parâmetros, cria instância diretamente
var instance = constructor.Invoke(null);
return (TDestination)instance;
}
var parameterValues = new object[parameters.Length];
bool canUseConstructor = true;
// Tenta mapear os parâmetros do construtor
for (int i = 0; i < parameters.Length; i++)
{
var param = parameters[i];
// Procura propriedade com o mesmo nome (case insensitive)
var matchingProp = sourceProps.FirstOrDefault(p =>
string.Equals(p.Name, param.Name, StringComparison.OrdinalIgnoreCase));
if (matchingProp != null)
{
var value = matchingProp.GetValue(source);
// Se os tipos são compatíveis, usa o valor diretamente
if (param.ParameterType.IsAssignableFrom(matchingProp.PropertyType))
{
parameterValues[i] = value;
}
// Verifica se existe uma conversão implícita
else if (value != null && TryImplicitConversion(value, param.ParameterType, out var convertedValue))
{
parameterValues[i] = convertedValue;
}
// Se o valor é um tipo complexo, tenta mapear recursivamente
else if (value != null && !matchingProp.PropertyType.IsPrimitive &&
!matchingProp.PropertyType.Namespace.StartsWith("System"))
{
try
{
var method = typeof(GenericMapper).GetMethod(nameof(MapTo));
var genericMethod = method.MakeGenericMethod(matchingProp.PropertyType, param.ParameterType);
parameterValues[i] = genericMethod.Invoke(null, new[] { value });
}
catch
{
canUseConstructor = false;
break;
}
}
else
{
canUseConstructor = false;
break;
}
}
else
{
// Se não encontrou uma propriedade correspondente, verifica se o parâmetro é opcional
if (param.IsOptional)
{
parameterValues[i] = param.DefaultValue;
}
else
{
canUseConstructor = false;
break;
}
}
}
if (canUseConstructor)
{
try
{
var instance = constructor.Invoke(parameterValues);
return (TDestination)instance;
}
catch
{
// Se falhou ao criar a instância, tenta o próximo construtor
continue;
}
}
}
// Se não conseguiu usar nenhum construtor, lança exceção
throw new InvalidOperationException(
$"Não foi possível mapear {sourceType.Name} para {typeof(TDestination).Name} " +
$"usando os construtores disponíveis. Verifique se os nomes das propriedades " +
$"correspondem aos nomes dos parâmetros do construtor (case insensitive).");
}
private static TDestination MapToMutableObject<TSource, TDestination>(TSource source)
where TDestination : class
{
var destType = typeof(TDestination);
// Tenta criar uma instância usando o construtor sem parâmetros
TDestination destination;
try
{
destination = Activator.CreateInstance<TDestination>();
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Não foi possível criar uma instância de {destType.Name}. " +
$"Certifique-se de que a classe tenha um construtor sem parâmetros acessível.", ex);
}
var sourceType = typeof(TSource);
var sourceProps = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var destProps = destType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty);
foreach (var destProp in destProps)
{
if (!destProp.CanWrite)
continue;
// Procura propriedade com o mesmo nome (case insensitive)
var sourceProp = sourceProps.FirstOrDefault(p =>
string.Equals(p.Name, destProp.Name, StringComparison.OrdinalIgnoreCase));
if (sourceProp != null)
{
var value = sourceProp.GetValue(source);
if (value != null)
{
// Se os tipos são compatíveis, atribui diretamente
if (destProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
{
destProp.SetValue(destination, value);
}
// Verifica se existe uma conversão implícita
else if (TryImplicitConversion(value, destProp.PropertyType, out var convertedValue))
{
destProp.SetValue(destination, convertedValue);
}
// Se o valor é um tipo complexo, tenta mapear recursivamente
else if (!sourceProp.PropertyType.IsPrimitive &&
!sourceProp.PropertyType.Namespace.StartsWith("System"))
{
try
{
var method = typeof(GenericMapper).GetMethod(nameof(MapTo));
var genericMethod = method.MakeGenericMethod(sourceProp.PropertyType, destProp.PropertyType);
var mappedValue = genericMethod.Invoke(null, new[] { value });
destProp.SetValue(destination, mappedValue);
}
catch
{
// Ignora se não conseguir mapear
}
}
}
}
}
return destination;
}
private static bool TryImplicitConversion(object source, Type destinationType, out object result)
{
result = null;
if (source == null) return false;
var sourceType = source.GetType();
// Verifica operador de conversão implícita no tipo de origem
var methodSource = sourceType.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(m =>
m.Name == "op_Implicit" &&
m.ReturnType == destinationType &&
m.GetParameters().Length == 1 &&
m.GetParameters()[0].ParameterType == sourceType);
if (methodSource != null)
{
result = methodSource.Invoke(null, new[] { source });
return true;
}
// Verifica operador de conversão implícita no tipo de destino
var methodDest = destinationType.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(m =>
m.Name == "op_Implicit" &&
m.ReturnType == destinationType &&
m.GetParameters().Length == 1 &&
m.GetParameters()[0].ParameterType == sourceType);
if (methodDest != null)
{
result = methodDest.Invoke(null, new[] { source });
return true;
}
// Tenta converter usando Convert.ChangeType
try
{
if (destinationType.IsValueType || destinationType == typeof(string))
{
result = Convert.ChangeType(source, destinationType);
return true;
}
}
catch { }
return false;
}
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Blinks.Domain.Entities
{
public class BusinessAreas
{
public BusinessAreas() { }
public string Name { get; set; }
}
}

View File

@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Blinks.Domain.Entities
{
internal class BusinessAres
{
}
}

View File

@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Blinks.Domain.Entities
{
public class LinkBio
{
/// <summary>
/// Ordem de exibição
/// </summary>
public int OrderNum { get; set; }
/// <summary>
/// Parte customizada do link
/// </summary>
public string UrlData { get; set; }
/// <summary>
/// Parte fixa do link (obter do service links quando cadastrar)
/// </summary>
public string ServiceUrl { get; set; }
/// <summary>
/// Url/caminho do PNG do link
/// </summary>
public string ServiceIcon { get; set; }
/// <summary>
/// Exibir/nao exibir
/// </summary>
public bool IsVisible { get; set; }
}
}

View File

@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Blinks.Domain.Entities
{
public class PageBio
{
public int Id { get; set; }
public string UrlParte1 { get; set; }
public string UrlParte2 { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<LinkBio>? Links { get; set; }
}
}

View File

@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Blinks.Domain.ValueObjects;
namespace Blinks.Domain.Entities
{
public class PersonUser
{
public Id id { get; private set; }
public Username Username { get; private set; }
public Name Name { get; private set; }
public string FirstName => Name.FirstName;
public string LastName => Name.LastName;
public Email Email { get; private set; }
public DateChanged DateChanged { get; private set; }
public bool IsProfileCompleted { get; private set; }
public int CountryId { get; private set; }
public int BusinessAreaId { get; private set; }
public string DesiredName { get; private set; }
public DateTime CreatedAt { get; private set; }
public UserPlan? Plano { get; private set; }
public List<UserPlan>? PastPlans { get; private set; }
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SumaTube.Domain.ValueObjects;
namespace SumaTube.Domain.Entities.UserPlan
{
public class PersonUser
{
public PersonUser(string id, Username username, Name name, Email email,
DateChanged dateChanged, bool isProfileCompleted, int countryId,
int businessAreaId, string desiredName, DateTime createdAt,
UserPayment plano = null, List<UserPayment> pastPlans = null)
{
Id = id ?? Guid.NewGuid().ToString("N");
Username = username;
Name = name;
Email = email;
DateChanged = dateChanged;
IsProfileCompleted = isProfileCompleted;
CountryId = countryId;
BusinessAreaId = businessAreaId;
DesiredName = desiredName;
CreatedAt = createdAt;
Plano = plano;
PastPlans = pastPlans ?? new List<UserPayment>();
}
public Id Id { get; private set; }
public Username Username { get; private set; }
public Name Name { get; private set; }
public string FirstName => Name.FirstName;
public string LastName => Name.LastName;
public Email Email { get; private set; }
public DateChanged DateChanged { get; private set; }
public bool IsProfileCompleted { get; private set; }
public int CountryId { get; private set; }
public int BusinessAreaId { get; private set; }
public string DesiredName { get; private set; }
public DateTime CreatedAt { get; private set; }
public UserPayment? Plano { get; private set; }
public List<UserPayment>? PastPlans { get; private set; }
public PersonUser AddPlano(UserPayment plano)
{
if (plano == null) throw new ArgumentNullException(nameof(plano));
PastPlans.Add(plano);
return this;
}
}
}

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Blinks.Domain.Entities namespace SumaTube.Domain.Entities.UserPlan.UserPlan
{ {
public class ServiceLinkPart public class ServiceLinkPart
{ {

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Blinks.Domain.Entities namespace SumaTube.Domain.Entities.UserPlan.UserPlan
{ {
/// <summary> /// <summary>
/// Vai ler uma lista estatica do BD com os links dos serviços /// Vai ler uma lista estatica do BD com os links dos serviços

View File

@ -4,9 +4,9 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Blinks.Domain.Entities namespace SumaTube.Domain.Entities.UserPlan
{ {
public class UserPlan public class UserPayment
{ {
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } public string Name { get; set; }

View File

@ -0,0 +1,48 @@
using BaseDomain.Results;
using SumaTube.Domain.ValueObjects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Domain.Entities.Videos
{
public class UserVideo
{
public Id Id { get; private set; }
public string Name { get; private set; }
public List<VideoGroup> VideoGroups { get; private set; } = new List<VideoGroup>();
public UserVideo(int userId, string name)
{
this.Id = Guid.NewGuid();
this.Name = name;
}
public Result<bool> AddVideoGroup(string name, string description)
{
if (!VideoGroups.Any(v => v.Name == name))
{
this.VideoGroups.Add(new VideoGroup(name, description));
return true;
}
return false;
}
public Result<bool> AddVideo(string collectionName, string url)
{
var videoCollectionItem = VideoGroups.Find(v => v.Name == collectionName);
if (videoCollectionItem != null)
{
if (videoCollectionItem.Videos.Any(v => v.Url == url)) return false;
videoCollectionItem.AddVideo(this.Id.Value, url);
return true;
}
return false;
}
public static UserVideo Create(int userId, string name) => new UserVideo(userId, name);
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Domain.Entities.Videos
{
public enum VideoSiteEnum
{
Youtube
}
public class VideoData
{
public string SessionId { get; private set; }
public string Title { get; private set; }
public string Image { get; private set; }
public string Url { get; private set; }
public VideoSiteEnum Site { get; private set; }
public VideoResult VideoResult { get; private set; }
public VideoData(string sessionId, string url)
{
SessionId = sessionId;
Url = url;
}
public void SetVideoResult(VideoResult videoResult)
{
VideoResult = videoResult;
}
public void UpdateData(string title, string iamge)
{
Title = title;
Image = iamge;
}
public void ChangeStatus(VideoStatusEnum videoStatus)
{
this.VideoResult.ChangeStatus(videoStatus);
}
}
}

View File

@ -0,0 +1,45 @@
using SumaTube.Domain.ValueObjects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Domain.Entities.Videos
{
public class VideoGroup
{
public Id Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public DateTime CreatedAt { get; set; }
public List<VideoData> Videos { get; private set; }
public VideoGroup(string name, string description)
{
Id = Guid.NewGuid();
Name = name;
Description = description;
CreatedAt = DateTime.Now;
Videos = new List<VideoData>();
}
public VideoGroup(string id, string name, string description, DateTime createdAt, List<VideoData> videos)
{
Id = id;
Name = name;
Description = description;
CreatedAt = createdAt;
Videos = videos;
}
public void AddVideo(string sessionId, string url)
{
Videos.Add(new VideoData(sessionId, url));
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Domain.Entities.Videos
{
public enum VideoStatusEnum
{
Requested,
Processed,
Error
}
public class VideoResult
{
public string VideoUrl { get; private set; }
public string Summary { get; private set; }
public string ErrorMessage { get; private set; }
public VideoStatusEnum VideoStatus { get; private set; }
public DateTime LastStatusDate { get; private set; }
public void ChangeStatus(VideoStatusEnum status)
{
LastStatusDate = DateTime.Now;
VideoStatus = status;
}
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Domain.Entities.Videos
{
public class VideoSummary
{
public string Id { get; private set; }
public string VideoId { get; private set; }
public string Title { get; private set; }
public string ThumbnailUrl { get; private set; }
public string Status { get; private set; }
public DateTime RequestDate { get; private set; }
public string Language { get; private set; }
public string UserId { get; private set; }
public string Summary { get; private set; }
public string Transcription { get; private set; }
public int Duration { get; private set; }
public string ErrorMessage { get; private set; }
public string SessionId { get; private set; }
// Factory method
public static VideoSummary Create(string videoId, string userId, string language, string sessionId)
{
return new VideoSummary
{
Id = Guid.NewGuid().ToString(),
VideoId = videoId,
Title = "Carregando...",
ThumbnailUrl = $"https://img.youtube.com/vi/{videoId}/maxresdefault.jpg",
Status = "PROCESSANDO",
RequestDate = DateTime.UtcNow,
Language = language,
UserId = userId,
SessionId = sessionId
};
}
// Methods to update state
public void SetAsCompleted(string title, string summary, string transcription, int duration)
{
Title = title;
Summary = summary;
Transcription = transcription;
Duration = duration;
Status = "REALIZADO";
ErrorMessage = null;
}
public void SetAsFailed(string errorMessage)
{
Status = "ERRO";
ErrorMessage = errorMessage;
}
// For MongoDB/ORM
private VideoSummary() { }
}
}

View File

@ -18,4 +18,9 @@
<ProjectReference Include="..\BaseDomain\BaseDomain.csproj" /> <ProjectReference Include="..\BaseDomain\BaseDomain.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Contracts\UserPlan\" />
<Folder Include="DomainServices\" />
</ItemGroup>
</Project> </Project>

View File

@ -4,7 +4,7 @@ using System.Diagnostics.Metrics;
using System.IO; using System.IO;
using System.Reflection.Emit; using System.Reflection.Emit;
namespace Blinks.Domain namespace SumaTube.Domain
{ {
public class DateBirth : AValueObject public class DateBirth : AValueObject
{ {

View File

@ -2,7 +2,7 @@
using BaseDomain; using BaseDomain;
using System.Globalization; using System.Globalization;
namespace Blinks.Domain namespace SumaTube.Domain
{ {
public class DateChanged : AValueObject public class DateChanged : AValueObject
{ {

View File

@ -1,7 +1,7 @@
using BaseDomain; using BaseDomain;
using System.Net.Mail; using System.Net.Mail;
namespace Blinks.Domain namespace SumaTube.Domain
{ {
public class Email : AValueObject public class Email : AValueObject
{ {

View File

@ -1,22 +1,66 @@
using BaseDomain; using BaseDomain;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Blinks.Domain.ValueObjects namespace SumaTube.Domain.ValueObjects
{ {
public class Id : AValueObject public class Id : AValueObject
{ {
private string _value;
public string Value => _value;
public override bool GetValidationExpression() public override bool GetValidationExpression()
{ {
throw new NotImplementedException(); return string.IsNullOrWhiteSpace(_value);
} }
protected override IEnumerable<object> GetEqualityComponents() protected override IEnumerable<object> GetEqualityComponents()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Id(string valor, Guid id)
{
if (string.IsNullOrWhiteSpace(valor))
throw new ArgumentException("O valor não pode ser nulo ou vazio", nameof(valor));
_value = valor;
}
public Id(Guid id)
{
_value = id.ToString("N");
}
private Id(string valor) : this(valor, Guid.NewGuid())
{
}
public static implicit operator Id(string valor)
{
return new Id(valor);
}
public static implicit operator Id(Guid id)
{
return new Id(id);
}
public static explicit operator string(Id vo)
{
return vo.Value.ToString();
}
public static explicit operator Guid(Id vo)
{
return Guid.Parse(vo.Value);
}
} }
} }

View File

@ -1,6 +1,6 @@
using BaseDomain; using BaseDomain;
namespace Blinks.Domain namespace SumaTube.Domain
{ {
public class Name : AValueObject public class Name : AValueObject
{ {
@ -15,9 +15,15 @@ namespace Blinks.Domain
} }
} }
public string FullName { get; set; } public Name(string firstName, string lastName)
public string FirstName { get; set; } {
public string LastName { get; set; } this.FirstName = firstName;
this.LastName = lastName;
}
public string FullName { get; private set; }
public string FirstName { get; private set; }
public string LastName { get; private set; }
public override bool GetValidationExpression() public override bool GetValidationExpression()
{ {

View File

@ -1,7 +1,7 @@
using BaseDomain; using BaseDomain;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Blinks.Domain namespace SumaTube.Domain
{ {
public class Phone : AValueObject public class Phone : AValueObject
{ {

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Blinks.Domain.ValueObjects namespace SumaTube.Domain.ValueObjects
{ {
public class Username public class Username
{ {

View File

@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SumaTube", "SumaTube\SumaTu
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseDomain", "BaseDomain\BaseDomain.csproj", "{8DEA200D-FF43-0D75-15A2-7DA8831449C9}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseDomain", "BaseDomain\BaseDomain.csproj", "{8DEA200D-FF43-0D75-15A2-7DA8831449C9}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SumaTube.Crosscutting", "SumaTube.Crosscutting\SumaTube.Crosscutting.csproj", "{46EE417D-A974-4011-9799-646F31C9C146}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SumaTube.Application", "SumaTube.Application\SumaTube.Application.csproj", "{142F15AA-CD0E-CC29-6972-3FE9A1DB283F}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -33,6 +37,14 @@ Global
{8DEA200D-FF43-0D75-15A2-7DA8831449C9}.Debug|Any CPU.Build.0 = Debug|Any CPU {8DEA200D-FF43-0D75-15A2-7DA8831449C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8DEA200D-FF43-0D75-15A2-7DA8831449C9}.Release|Any CPU.ActiveCfg = Release|Any CPU {8DEA200D-FF43-0D75-15A2-7DA8831449C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8DEA200D-FF43-0D75-15A2-7DA8831449C9}.Release|Any CPU.Build.0 = Release|Any CPU {8DEA200D-FF43-0D75-15A2-7DA8831449C9}.Release|Any CPU.Build.0 = Release|Any CPU
{46EE417D-A974-4011-9799-646F31C9C146}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{46EE417D-A974-4011-9799-646F31C9C146}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46EE417D-A974-4011-9799-646F31C9C146}.Release|Any CPU.ActiveCfg = Release|Any CPU
{46EE417D-A974-4011-9799-646F31C9C146}.Release|Any CPU.Build.0 = Release|Any CPU
{142F15AA-CD0E-CC29-6972-3FE9A1DB283F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{142F15AA-CD0E-CC29-6972-3FE9A1DB283F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{142F15AA-CD0E-CC29-6972-3FE9A1DB283F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{142F15AA-CD0E-CC29-6972-3FE9A1DB283F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Blinks.Controllers namespace SumaTube.Controllers
{ {
public class CartHomeController : Controller public class CartHomeController : Controller
{ {

View File

@ -1,21 +1,23 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Diagnostics; using System.Diagnostics;
using Blinks.Models; using SumaTube.Crosscutting.Logging.Extensions;
using SumaTube.Models;
namespace Blinks.Controllers namespace SumaTube.Controllers
{ {
public class HomeController : Controller public class HomeController : Controller
{ {
private readonly ILogger<HomeController> logger; private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger) public HomeController(ILogger<HomeController> logger)
{ {
this.logger = logger; this._logger = logger;
} }
public IActionResult Index() public IActionResult Index()
{ {
this.logger.LogInformation("Home carregada!"); _logger.LogInformation("Home carregada!");
_logger.LogMethodEntry(nameof(Index));
return View(); return View();
} }

View File

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Globalization; using System.Globalization;
namespace Blinks.Controllers namespace SumaTube.Controllers
{ {
public class LanguageController : Controller public class LanguageController : Controller
{ {

View File

@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Stripe; using Stripe;
namespace Blinks.Controllers namespace SumaTube.Controllers
{ {
public class LoginController : Controller public class LoginController : Controller
{ {

View File

@ -2,7 +2,7 @@
using Stripe; using Stripe;
using Stripe.Checkout; using Stripe.Checkout;
namespace Blinks.Controllers namespace SumaTube.Controllers
{ {
[Route("Pay")] [Route("Pay")]
public class PayController : Controller public class PayController : Controller

View File

@ -2,9 +2,9 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Stripe; using Stripe;
using Stripe.Checkout; using Stripe.Checkout;
using Blinks.Models; using SumaTube.Models;
namespace Blinks.Controllers namespace SumaTube.Controllers
{ {
public class PlansController : Controller public class PlansController : Controller
{ {

View File

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Diagnostics; using System.Diagnostics;
namespace Blinks.Controllers namespace SumaTube.Controllers
{ {
[Route("signin-microsoft")] [Route("signin-microsoft")]
public class SignInController : Controller public class SignInController : Controller

View File

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Blinks.Controllers namespace SumaTube.Controllers
{ {
public class StartupController : Controller public class StartupController : Controller
{ {

View File

@ -0,0 +1,114 @@
using Microsoft.AspNetCore.Mvc;
using SumaTube.Application.Videos.ApplicationServices;
using SumaTube.Application.Videos.Contracts;
using SumaTube.Domain.Entities.Videos;
using SumaTube.Models;
using System.Collections.Generic;
namespace SumaTube.Controllers
{
public class VideoController : Controller
{
private readonly IVideoApplicationService _videoApplicationService;
private readonly ILogger<VideoController> _logger;
public VideoController(
IVideoApplicationService videoApplicationService,
ILogger<VideoController> logger)
{
_videoApplicationService = videoApplicationService;
_logger = logger;
}
public IActionResult Extract()
{
return View();
}
public async Task<IActionResult> VideoSummary(string id)
{
// Aqui você chamaria sua WebAPI para processar o vídeo
// e retornaria o modelo para a view
var model = new VideoSummaryViewModel
{
VideoId = id,
VideoTitle = "Título do Vídeo",
ChannelName = "Nome do Canal",
ChannelThumbnail = "URL da Thumbnail do Canal",
PublishedDate = "Data de Publicação",
ViewCount = "Quantidade de Visualizações",
LikeCount = "Quantidade de Curtidas",
CommentCount = "Quantidade de Comentarios",
SummaryText = "Resumo do Vídeo",
KeyPoints = new List<object> { "Ponto 1", "Ponto 2", "Ponto 3" },
Captions = new List<object> { "Legenda 1", "Legenda 2", "Legenda 3" },
Keywords = new List<string> { "Palavra 1", "Palavra 2", "Palavra 3" },
RelatedTopics = new List<string> { "Tópico 1", "Tópico 2", "Tópico 3" },
RelatedVideos = new List<object> { "Vídeo 1", "Vídeo 2", "Vídeo 3" }
};
return View(model);
}
public async Task<IActionResult> MySummary()
{
var userId = User.Identity.Name; // Ou outra forma de obter o ID do usuário
_logger.LogInformation("Carregando página 'Meus Resumos' para usuário: {UserId}", userId);
var videos = new List<VideoSummary>();
if (userId != null)
videos = await _videoApplicationService.GetUserVideosAsync(userId);
return View(videos);
}
// Endpoint para enviar solicitação de novo resumo
[HttpPost]
public async Task<IActionResult> RequestSummary(string youtubeUrl, string language)
{
var userId = User.Identity.Name; // Ou outra forma de obter o ID do usuário
_logger.LogInformation("Recebido pedido de resumo. URL: {Url}, Idioma: {Language}, Usuário: {UserId}",
youtubeUrl, language, userId);
try
{
var result = await _videoApplicationService.RequestVideoSummaryAsync(youtubeUrl, language, userId);
_logger.LogInformation("Resumo solicitado com sucesso. ID: {Id}", result.Id);
return Json(new { success = true, videoId = result.VideoId, summaryId = result.Id });
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao solicitar resumo: {Message}", ex.Message);
return Json(new { success = false, error = ex.Message });
}
}
// Página para exibir um resumo específico
public async Task<IActionResult> Summary(string id)
{
var userId = User.Identity.Name; // Ou outra forma de obter o ID do usuário
_logger.LogInformation("Acessando resumo. ID: {Id}, Usuário: {UserId}", id, userId);
var summary = await _videoApplicationService.GetVideoSummaryByIdAsync(id, userId);
if (summary == null)
{
_logger.LogWarning("Resumo não encontrado. ID: {Id}", id);
return NotFound();
}
return View(summary);
}
// Endpoint para verificar status de processamento
[HttpGet]
public async Task<IActionResult> CheckStatus(string id)
{
var userId = User.Identity.Name; // Ou outra forma de obter o ID do usuário
_logger.LogInformation("Verificando status do resumo. ID: {Id}, Usuário: {UserId}", id, userId);
var status = await _videoApplicationService.CheckSummaryStatusAsync(id, userId);
return Json(status);
}
}
}

View File

@ -7,16 +7,16 @@ EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src WORKDIR /src
COPY ["Blinks.csproj", "."] COPY ["SumaTube.csproj", "."]
RUN dotnet restore "./Blinks.csproj" RUN dotnet restore "./SumaTube.csproj"
COPY . . COPY . .
WORKDIR "/src/." WORKDIR "/src/."
RUN dotnet build "Blinks.csproj" -c Release -o /app/build RUN dotnet build "SumaTube.csproj" -c Release -o /app/build
FROM build AS publish FROM build AS publish
RUN dotnet publish "Blinks.csproj" -c Release -o /app/publish /p:UseAppHost=false RUN dotnet publish "SumaTube.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final FROM base AS final
WORKDIR /app WORKDIR /app
COPY --from=publish /app/publish . COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Blinks.dll"] ENTRYPOINT ["dotnet", "SumaTube.dll"]

View File

@ -1,6 +1,6 @@
//using Serilog.Sinks.Loki; //using Serilog.Sinks.Loki;
//namespace Blinks.LogConfig //namespace SumaTube.LogConfig
//{ //{
// public class LogCredentials : LokiCredentials // public class LogCredentials : LokiCredentials
// { // {

View File

@ -1,6 +1,6 @@
//using Serilog.Sinks.Loki.Labels; //using Serilog.Sinks.Loki.Labels;
namespace Blinks.LogConfig namespace SumaTube.LogConfig
{ {
//public class LogLabelProvider : ILogLabelProvider //public class LogLabelProvider : ILogLabelProvider
//{ //{
@ -8,7 +8,7 @@ namespace Blinks.LogConfig
// { // {
// return new List<LokiLabel> // return new List<LokiLabel>
// { // {
// new LokiLabel("app", "blinks"), // new LokiLabel("app", "SumaTube"),
// new LokiLabel("namespace", "test") // new LokiLabel("namespace", "test")
// }; // };
// } // }

View File

@ -1,4 +1,4 @@
namespace Blinks.Middle namespace SumaTube.Middle
{ {
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using System.Globalization; using System.Globalization;

View File

@ -1,4 +1,4 @@
namespace Blinks.Models namespace SumaTube.Models
{ {
public class ChangeViewModel public class ChangeViewModel
{ {

View File

@ -1,4 +1,4 @@
namespace Blinks.Models namespace SumaTube.Models
{ {
public class ErrorViewModel public class ErrorViewModel
{ {

View File

@ -1,4 +1,4 @@
namespace Blinks.Models namespace SumaTube.Models
{ {
public class Payment public class Payment
{ {

View File

@ -0,0 +1,23 @@
using System;
namespace SumaTube.Models
{
public class VideoSummaryItem
{
public string Id { get; set; }
public string VideoId { get; set; } // ID do vídeo do YouTube
public string Title { get; set; }
public string ThumbnailUrl { get; set; }
public string Status { get; set; } // PROCESSANDO, REALIZADO, ERRO
public DateTime RequestDate { get; set; }
public string Language { get; set; }
public string UserId { get; set; } // ID do usuário que solicitou o resumo
// Propriedades para quando o resumo for concluído
public string Summary { get; set; }
public string Transcription { get; set; }
public int Duration { get; set; } // Duração do vídeo em segundos
public string ErrorMessage { get; set; }
}
}

View File

@ -0,0 +1,20 @@
namespace SumaTube.Models
{
public class VideoSummaryViewModel
{
public string VideoId { get; set; }
public string VideoTitle { get; set; }
public string ChannelName { get; set; }
public string ChannelThumbnail { get; set; }
public string PublishedDate { get; set; }
public string ViewCount { get; set; }
public string LikeCount { get; set; }
public string CommentCount { get; set; }
public string SummaryText { get; set; }
public List<object> KeyPoints { get; set; }
public List<object> Captions { get; set; }
public List<string> Keywords { get; set; }
public List<string> RelatedTopics { get; set; }
public List<object> RelatedVideos { get; set; }
}
}

View File

@ -32,7 +32,7 @@
}, },
"projects": [ "projects": [
{ {
"path": "Blinks.csproj", "path": "SumaTube.csproj",
"startingProject": true, "startingProject": true,
"issues": 1, "issues": 1,
"storyPoints": 1, "storyPoints": 1,
@ -40,7 +40,7 @@
{ {
"incidentId": "1161414d-b2e4-4447-9d34-e811b563e1c5", "incidentId": "1161414d-b2e4-4447-9d34-e811b563e1c5",
"ruleId": "NuGet.0001", "ruleId": "NuGet.0001",
"projectPath": "Blinks.csproj", "projectPath": "SumaTube.csproj",
"state": "Active", "state": "Active",
"location": { "location": {
"snippetModel": { "snippetModel": {
@ -48,7 +48,7 @@
"protected": "Microsoft.VisualStudio.Azure.Containers.Tools.Targets, 1.19.4\n\nRecommendation:\n\nNo supported version found" "protected": "Microsoft.VisualStudio.Azure.Containers.Tools.Targets, 1.19.4\n\nRecommendation:\n\nNo supported version found"
}, },
"kind": "File", "kind": "File",
"path": "Blinks.csproj", "path": "SumaTube.csproj",
"snippet": "Microsoft.VisualStudio.Azure.Containers.Tools.Targets, 1.19.4\n\nRecommendation:\n\nNo supported version found", "snippet": "Microsoft.VisualStudio.Azure.Containers.Tools.Targets, 1.19.4\n\nRecommendation:\n\nNo supported version found",
"protectedSnippet": "Microsoft.VisualStudio.Azure.Containers.Tools.Targets, 1.19.4\n\nRecommendation:\n\nNo supported version found", "protectedSnippet": "Microsoft.VisualStudio.Azure.Containers.Tools.Targets, 1.19.4\n\nRecommendation:\n\nNo supported version found",
"label": "Microsoft.VisualStudio.Azure.Containers.Tools.Targets 1.19.4" "label": "Microsoft.VisualStudio.Azure.Containers.Tools.Targets 1.19.4"
@ -57,7 +57,7 @@
] ]
}, },
{ {
"path": "C:\\vscode\\Blinks.me.mvc\\Blinks.Domain\\Blinks.Domain.csproj", "path": "C:\\vscode\\SumaTube.me.mvc\\SumaTube.Domain\\SumaTube.Domain.csproj",
"startingProject": true, "startingProject": true,
"issues": 1, "issues": 1,
"storyPoints": 1, "storyPoints": 1,
@ -65,7 +65,7 @@
{ {
"incidentId": "2fdd89a6-9f8d-4428-8845-41d048dcaf73", "incidentId": "2fdd89a6-9f8d-4428-8845-41d048dcaf73",
"ruleId": "Project.0002", "ruleId": "Project.0002",
"projectPath": "C:\\vscode\\Blinks.me.mvc\\Blinks.Domain\\Blinks.Domain.csproj", "projectPath": "C:\\vscode\\SumaTube.me.mvc\\SumaTube.Domain\\SumaTube.Domain.csproj",
"state": "Active", "state": "Active",
"location": { "location": {
"snippetModel": { "snippetModel": {
@ -73,7 +73,7 @@
"protected": "Current: net7.0\nNew: net8.0" "protected": "Current: net7.0\nNew: net8.0"
}, },
"kind": "File", "kind": "File",
"path": "C:\\vscode\\Blinks.me.mvc\\Blinks.Domain\\Blinks.Domain.csproj", "path": "C:\\vscode\\SumaTube.me.mvc\\SumaTube.Domain\\SumaTube.Domain.csproj",
"snippet": "Current: net7.0\nNew: net8.0", "snippet": "Current: net7.0\nNew: net8.0",
"protectedSnippet": "Current: net7.0\nNew: net8.0" "protectedSnippet": "Current: net7.0\nNew: net8.0"
} }

View File

@ -1,4 +1,4 @@
using Blinks.LogConfig; using SumaTube.LogConfig;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.MicrosoftAccount; using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
@ -11,30 +11,23 @@ using Stripe;
using Stripe.Forwarding; using Stripe.Forwarding;
using System.Globalization; using System.Globalization;
using System.Security.Policy; using System.Security.Policy;
using SumaTube.Crosscutting.Logging.Configuration;
using SumaTube.Application.Register;
using SumaTube.Infra.Register;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
//var credentials = new BasicAuthCredentials("http://192.168.0.82:3100"); builder.Host.UseSerilog((context, services, configuration) =>
//var credentials = new LogCredentials("http://192.168.0.82:3100"); {
builder.SetLoggerConfiguration(configuration, services, context.Configuration);
Log.Logger = new LoggerConfiguration() });
.MinimumLevel.Information()
.Enrich.FromLogContext()
.Enrich.WithProperty("app", "blinks")
.WriteTo.Console()
.WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day)
.WriteTo.GrafanaLoki(
uri: "http://192.168.0.82:3100",
propertiesAsLabels: new List<string> { "app", "blinks" },
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Debug
)
.CreateLogger();
builder.Host.UseSerilog();
// Add services to the container. // Add services to the container.
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
builder.Services.AddApplicationServices();
builder.Services.AddInfraServices(builder.Configuration);
var config = builder.Configuration; var config = builder.Configuration;
//builder.Services.AddAuthentication() //builder.Services.AddAuthentication()
@ -86,21 +79,29 @@ builder.Services.Configure<RequestLocalizationOptions>(options =>
StripeConfiguration.ApiKey = builder.Configuration["Stripe:SecretKey"]; StripeConfiguration.ApiKey = builder.Configuration["Stripe:SecretKey"];
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
builder.Services.AddSerilog();
var app = builder.Build(); var app = builder.Build();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"]);
diagnosticContext.Set("ClientIP", httpContext.Connection.RemoteIpAddress);
diagnosticContext.Set("UserName", httpContext.User?.Identity?.Name ?? "Anonymous");
};
});
var locOptions = app.Services.GetService<IOptions<RequestLocalizationOptions>>(); var locOptions = app.Services.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(locOptions.Value); app.UseRequestLocalization(locOptions.Value);
app.UseMiddleware<RequestLocalizationMiddleware>(); app.UseMiddleware<RequestLocalizationMiddleware>();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
{ {
app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage();
app.UseExceptionHandler("/Home/Error"); app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts(); app.UseHsts();
} }
@ -116,8 +117,11 @@ app.MapControllerRoute(
name: "default", name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"); pattern: "{controller=Home}/{action=Index}/{id?}");
app.UseSerilogRequestLogging();
app.UseRequestLocalization(); app.UseRequestLocalization();
Log.Information("Aplicação iniciando");
Log.Warning("Este é um aviso de teste");
Log.Error("Este é um erro de teste para verificar o Seq");
app.Run(); app.Run();

View File

@ -8,7 +8,7 @@
// </auto-generated> // </auto-generated>
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
namespace Blinks { namespace SumaTube {
using System; using System;
@ -39,7 +39,7 @@ namespace Blinks {
public static global::System.Resources.ResourceManager ResourceManager { public static global::System.Resources.ResourceManager ResourceManager {
get { get {
if (object.ReferenceEquals(resourceMan, null)) { if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Blinks.Resource", typeof(Resource).Assembly); global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SumaTube.Resource", typeof(Resource).Assembly);
resourceMan = temp; resourceMan = temp;
} }
return resourceMan; return resourceMan;

View File

@ -8,7 +8,7 @@
// </auto-generated> // </auto-generated>
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
namespace Blinks { namespace SumaTube {
using System; using System;

View File

@ -9,22 +9,38 @@
<DockerfileContext>.</DockerfileContext> <DockerfileContext>.</DockerfileContext>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="Views\CartHome\**" />
<Content Remove="Views\CartHome\**" />
<EmbeddedResource Remove="Views\CartHome\**" />
<None Remove="Views\CartHome\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.7" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="8.0.7" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="8.0.7" />
<PackageReference Include="Serilog" Version="4.0.2" /> <PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" /> <PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Enrichers.Context" Version="4.6.5" /> <PackageReference Include="Serilog.Enrichers.Context" Version="4.6.5" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.0" /> <PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
<PackageReference Include="Stripe.net" Version="45.13.0" /> <PackageReference Include="Stripe.net" Version="45.13.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Views\CartHome\" />
<Folder Include="wwwroot\img\" /> <Folder Include="wwwroot\img\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SumaTube.Application\SumaTube.Application.csproj" />
<ProjectReference Include="..\SumaTube.Crosscutting\SumaTube.Crosscutting.csproj" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Resource.Designer.cs"> <Compile Update="Resource.Designer.cs">
<DesignTime>True</DesignTime> <DesignTime>True</DesignTime>

View File

@ -1,79 +1,279 @@
@{ @{
ViewData["Title"] = "Home Page"; ViewBag.Title = "Resumos Inteligentes de Vídeos";
} }
@section Scripts { <!-- Hero Banner -->
<script type="text/javascript"> <div class="hero-banner mb-5">
</script> <div class="container">
} <div class="row align-items-center">
<div class="col-lg-6">
<h1>Transforme qualquer vídeo em um resumo inteligente</h1>
<p class="lead">Extraímos as legendas e criamos resumos precisos para você economizar tempo e maximizar o aprendizado.</p>
@section Styles { <a href="@Url.Action("Index", "Login")" class="btn btn-hero">
<style type="text/css"> <i class="bi bi-magic mr-2"></i> Fazer login e resumir um vídeo
.text-responsive {
font-size: calc(100% + 1vw + 1vh);
}
</style>
}
<div class="div-body">
<div class="container">
<div id="myCarousel" class="carousel slide" data-ride="carousel">
<ol class="carousel-indicators">
<li data-target="#myCarousel" data-slide-to="0" class="active"></li>
<li data-target="#myCarousel" data-slide-to="1"></li>
<li data-target="#myCarousel" data-slide-to="2"></li>
<li data-target="#myCarousel" data-slide-to="3"></li>
</ol>
<div class="carousel-inner" role="listbox">
<div class="carousel-item active">
<img class="first-slide" src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" alt="First slide">
<div class="container">
<div class="carousel-caption d-md-block text-left">
<h1 class="text-responsive">Example headline.</h1>
<p>Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus. Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
<p><a class="btn btn-lg btn-primary" href="#" role="button">Sign up today</a></p>
</div>
</div>
</div>
<div class="carousel-item">
<img class="second-slide" src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" alt="Second slide">
<div class="container">
<div class="carousel-caption d-md-block">
<h1 class="text-responsive">Another example headline.</h1>
<p>Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus. Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
<p><a class="btn btn-lg btn-primary" href="#" role="button">Learn more</a></p>
</div>
</div>
</div>
<div class="carousel-item">
<img class="third-slide" src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" alt="Third slide">
<div class="container">
<div class="carousel-caption d-md-block text-right">
<h1 class="text-responsive">One more for good measure.</h1>
<p>Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus. Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
<p><a class="btn btn-lg btn-primary" href="#" role="button">Browse gallery</a></p>
</div>
</div>
</div>
<div class="carousel-item">
<img class="fourth-slide" src="/img/teste1.png" alt="Fourth slide">
<div class="container">
<div class="carousel-caption d-md-block text-right text-dark">
<h1 class="text-responsive">One more for good measure.</h1>
<p>Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus. Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
<p><a class="btn btn-lg btn-primary" href="#" role="button">Browse gallery</a></p>
</div>
</div>
</div>
</div>
<a class="carousel-control-prev" href="#myCarousel" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Anterior</span>
</a> </a>
<a class="carousel-control-next" href="#myCarousel" role="button" data-slide="next"> </div>
<span class="carousel-control-next-icon" aria-hidden="true"></span> <div class="col-lg-6 mt-4 mt-lg-0 text-center">
<span class="sr-only">Próximo</span> <div class="hero-image">
<img src="/img/video-summary-demo.png" alt="Ilustração de resumo de vídeo" class="img-fluid" onerror="this.src='https://via.placeholder.com/600x350?text=SumaTube';" />
<span class="demo-badge">
<i class="bi bi-play-circle mr-1"></i> Ver demonstração
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Como funciona -->
<section class="mb-5">
<div class="container">
<h2 class="text-center mb-4">Como funciona</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="suma-card h-100 text-center">
<div class="mb-3">
<i class="bi bi-youtube text-danger" style="font-size: 3rem;"></i>
</div>
<h3 class="suma-card-title">1. Cole o link do YouTube</h3>
<p class="suma-card-body">
Basta colar o URL do vídeo do YouTube que você deseja resumir. Funciona com qualquer vídeo que tenha legendas disponíveis.
</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="suma-card h-100 text-center">
<div class="mb-3">
<i class="bi bi-cpu text-danger" style="font-size: 3rem;"></i>
</div>
<h3 class="suma-card-title">2. Processamento inteligente</h3>
<p class="suma-card-body">
Nossa tecnologia extrai as legendas e utiliza inteligência artificial para identificar os pontos mais importantes do vídeo.
</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="suma-card h-100 text-center">
<div class="mb-3">
<i class="bi bi-file-earmark-text text-danger" style="font-size: 3rem;"></i>
</div>
<h3 class="suma-card-title">3. Receba seu resumo</h3>
<p class="suma-card-body">
Em poucos segundos, você recebe um resumo completo e organizado com os principais pontos abordados no vídeo.
</p>
</div>
</div>
</div>
</div>
</section>
<!-- Recursos -->
<section class="mb-5 bg-light py-5">
<div class="container">
<h2 class="text-center mb-4">Economize tempo com nossos recursos</h2>
<div class="row align-items-center">
<div class="col-lg-6 mb-4 mb-lg-0 order-lg-2">
@*
<img src="/images/features-illustration.png" alt="Recursos do SumaTube" class="img-fluid rounded shadow" onerror="this.src='https://via.placeholder.com/600x400?text=Recursos+SumaTube';" />
*@
</div>
<div class="col-lg-6 order-lg-1">
<div class="d-flex mb-4">
<div class="mr-3">
<i class="bi bi-clock text-danger" style="font-size: 2rem;"></i>
</div>
<div>
<h3>Economize tempo</h3>
<p>Assista apenas o que realmente importa com nossos resumos personalizados.</p>
</div>
</div>
<div class="d-flex mb-4">
<div class="mr-3">
<i class="bi bi-translate text-danger" style="font-size: 2rem;"></i>
</div>
<div>
<h3>Suporte multi-idioma</h3>
<p>Resuma vídeos em diversos idiomas com a mesma precisão e qualidade.</p>
</div>
</div>
<div class="d-flex mb-4">
<div class="mr-3">
<i class="bi bi-bookmark-check text-danger" style="font-size: 2rem;"></i>
</div>
<div>
<h3>Pontos-chave destacados</h3>
<p>Identificamos automaticamente os conceitos e informações mais importantes em cada vídeo.</p>
</div>
</div>
<div class="d-flex">
<div class="mr-3">
<i class="bi bi-cloud-download text-danger" style="font-size: 2rem;"></i>
</div>
<div>
<h3>Salve para consultar depois</h3>
<p>Mantenha todos seus resumos organizados em sua biblioteca pessoal.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Planos -->
<section class="mb-5">
<div class="container">
<div class="text-center mb-5">
<h2>Escolha o plano ideal para você</h2>
<p class="lead">Oferecemos opções flexíveis para atender às suas necessidades</p>
</div>
<div class="row">
<div class="col-md-4 mb-4">
<div class="suma-card h-100 text-center">
<span class="badge badge-pill badge-light mb-3">Gratuito</span>
<h3 class="suma-card-title">Basic</h3>
<div class="my-4">
<span class="h1">R$ 0</span>
<span class="text-muted">/mês</span>
</div>
<ul class="list-unstyled mb-4">
<li class="mb-2">3 resumos por mês</li>
<li class="mb-2">Resumos básicos</li>
<li class="mb-2">Suporte por e-mail</li>
<li class="mb-2 text-muted"><del>Biblioteca personalizada</del></li>
<li class="mb-2 text-muted"><del>Exportação em PDF</del></li>
</ul>
<a href="#" class="btn btn-outline-danger">Começar agora</a>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="suma-card h-100 text-center" style="transform: scale(1.05); border: 2px solid var(--suma-red);">
<span class="badge badge-pill badge-danger mb-3">Popular</span>
<h3 class="suma-card-title">Pro</h3>
<div class="my-4">
<span class="h1">R$ 19,90</span>
<span class="text-muted">/mês</span>
</div>
<ul class="list-unstyled mb-4">
<li class="mb-2">30 resumos por mês</li>
<li class="mb-2">Resumos detalhados</li>
<li class="mb-2">Suporte prioritário</li>
<li class="mb-2">Biblioteca personalizada</li>
<li class="mb-2">Exportação em PDF</li>
</ul>
<a href="#" class="btn btn-danger">Escolher Pro</a>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="suma-card h-100 text-center">
<span class="badge badge-pill badge-light mb-3">Premium</span>
<h3 class="suma-card-title">Business</h3>
<div class="my-4">
<span class="h1">R$ 49,90</span>
<span class="text-muted">/mês</span>
</div>
<ul class="list-unstyled mb-4">
<li class="mb-2">Resumos ilimitados</li>
<li class="mb-2">Resumos avançados</li>
<li class="mb-2">Suporte VIP</li>
<li class="mb-2">Biblioteca com categorias</li>
<li class="mb-2">Exportação em vários formatos</li>
</ul>
<a href="#" class="btn btn-outline-danger">Escolher Business</a>
</div>
</div>
</div>
</div>
</section>
<!-- Depoimentos -->
<section class="mb-5 bg-light py-5">
<div class="container">
<h2 class="text-center mb-5">O que nossos usuários dizem</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="suma-card h-100">
<div class="d-flex mb-3 align-items-center">
<div class="rounded-circle overflow-hidden mr-3" style="width: 50px; height: 50px;">
<img src="https://via.placeholder.com/50x50" alt="Usuário" class="img-fluid" />
</div>
<div>
<h5 class="mb-0">Mariana Silva</h5>
<small class="text-muted">Estudante de Medicina</small>
</div>
</div>
<p class="suma-card-body">
"O SumaTube revolucionou meus estudos. Consigo extrair o essencial de aulas longas em minutos, o que me ajudou a otimizar muito meu tempo de estudo."
</p>
<div class="text-warning">
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="suma-card h-100">
<div class="d-flex mb-3 align-items-center">
<div class="rounded-circle overflow-hidden mr-3" style="width: 50px; height: 50px;">
<img src="https://via.placeholder.com/50x50" alt="Usuário" class="img-fluid" />
</div>
<div>
<h5 class="mb-0">Carlos Mendes</h5>
<small class="text-muted">Profissional de Marketing</small>
</div>
</div>
<p class="suma-card-body">
"Uso o SumaTube para acompanhar webinars e palestras do meu setor. Os resumos são precisos e me ajudam a extrair insights valiosos sem gastar horas assistindo vídeos."
</p>
<div class="text-warning">
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-half"></i>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="suma-card h-100">
<div class="d-flex mb-3 align-items-center">
<div class="rounded-circle overflow-hidden mr-3" style="width: 50px; height: 50px;">
<img src="https://via.placeholder.com/50x50" alt="Usuário" class="img-fluid" />
</div>
<div>
<h5 class="mb-0">Júlia Santos</h5>
<small class="text-muted">Professora</small>
</div>
</div>
<p class="suma-card-body">
"Recomendo o SumaTube para todos meus alunos. É uma ferramenta incrível para complementar os estudos e revisar conteúdos de forma eficiente."
</p>
<div class="text-warning">
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="mb-5">
<div class="container">
<div class="suma-card bg-danger text-white text-center py-5">
<h2 class="mb-4">Comece a economizar tempo hoje mesmo</h2>
<p class="lead mb-4">Transforme a maneira como você assiste vídeos e absorve conhecimento</p>
<a href="@Url.Action("Index", "Login")" class="btn btn-light btn-lg px-4">
<i class="bi bi-play-circle mr-2"></i> Começar gratuitamente
</a> </a>
</div> </div>
</div> </div>
</div> </section>

View File

@ -1,102 +0,0 @@
.btn-primary {
background-color: darkgreen;
color: white;
}
.btn-red {
background-color: darkred;
color: white;
}
.div-body {
padding-top: 3rem;
padding-bottom: 3rem;
color: #5a5a5a;
}
.bg-inverse {
background-color: #292b2c !important;
}
/* CUSTOMIZE THE CAROUSEL
-------------------------------------------------- */
/* Carousel base class */
.carousel {
margin-bottom: 4rem;
}
/* Since positioning the image, we need to help out the caption */
.carousel-caption {
z-index: 10;
bottom: 3rem;
}
/* Declare heights because of positioning of img element */
.carousel-item {
height: 32rem;
background-color: #777;
}
.carousel-item > img {
position: absolute;
top: 0;
left: 0;
min-width: 100%;
height: 32rem;
}
/* MARKETING CONTENT
-------------------------------------------------- */
/* Center align the text within the three columns below the carousel */
.marketing .col-lg-4 {
margin-bottom: 1.5rem;
text-align: center;
}
.marketing h2 {
font-weight: normal;
}
.marketing .col-lg-4 p {
margin-right: .75rem;
margin-left: .75rem;
}
/* Featurettes
------------------------- */
.featurette-divider {
margin: 5rem 0; /* Space out the Bootstrap <hr> more */
}
/* Thin out the marketing headings */
.featurette-heading {
font-weight: 300;
line-height: 1;
letter-spacing: -.05rem;
}
/* RESPONSIVE CSS
-------------------------------------------------- */
@media (min-width: 40em) {
/* Bump up size of carousel content */
.carousel-caption p {
margin-bottom: 1.25rem;
font-size: 1.25rem;
line-height: 1.4;
}
.featurette-heading {
font-size: 50px;
}
}
@media (min-width: 62em) {
.featurette-heading {
margin-top: 7rem;
}
}

View File

@ -1,127 +1,111 @@
@{ @{
ViewBag.Title = "Login / Registro"; ViewBag.Title = "Login / Registro";
} }
@section Styles { @section Styles {
<style> <style>
.box { .login-container {
width: 500px; max-width: 500px;
margin: 200px 0; margin: 60px auto;
background-color: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 30px;
text-align: center;
} }
.shape1 { .login-icon {
position: relative; font-size: 2.5rem;
height: 150px; color: var(--suma-red);
width: 150px; margin-bottom: 25px;
background-color: #0074d9;
border-radius: 80px;
float: left;
margin-right: -50px;
} }
.shape2 { .login-title {
position: relative; font-size: 28px;
height: 150px; font-weight: 700;
width: 150px; color: var(--suma-red);
background-color: #0074d9; margin-bottom: 15px;
border-radius: 80px;
margin-top: -30px;
float: left;
} }
.shape3 { .login-subtitle {
position: relative; font-size: 16px;
height: 150px; color: var(--suma-dark-gray);
width: 150px; margin-bottom: 30px;
background-color: #0074d9;
border-radius: 80px;
margin-top: -30px;
float: left;
margin-left: -31px;
} }
.shape4 { .login-button {
position: relative; display: flex;
height: 150px; align-items: center;
width: 150px; justify-content: center;
background-color: #0074d9; background-color: white;
border-radius: 80px; color: #444;
margin-top: -25px; border: 1px solid #ddd;
float: left; border-radius: 5px;
margin-left: -32px; padding: 12px 20px;
font-size: 16px;
font-weight: 500;
width: 100%;
transition: all 0.3s ease;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
cursor: pointer;
} }
.shape5 { .login-button:hover {
position: relative; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
height: 150px; transform: translateY(-2px);
width: 150px;
background-color: #0074d9;
border-radius: 80px;
float: left;
margin-right: -48px;
margin-left: -32px;
margin-top: -30px;
} }
.shape6 { .login-button i {
position: relative; margin-right: 10px;
height: 150px; color: #4285F4;
width: 150px;
background-color: #0074d9;
border-radius: 80px;
float: left;
margin-right: -20px;
margin-top: -35px;
} }
.shape7 { .login-footer {
position: relative; margin-top: 30px;
height: 150px; font-size: 14px;
width: 150px; color: var(--suma-dark-gray);
background-color: #0074d9;
border-radius: 80px;
float: left;
margin-right: -20px;
margin-top: -57px;
} }
.float { .login-footer a {
position: absolute; color: var(--suma-red);
z-index: 2; text-decoration: none;
} }
.form { .login-footer a:hover {
margin-left: 145px; text-decoration: underline;
}
.video-icon-container {
font-size: 4rem;
margin-bottom: 20px;
color: var(--suma-red);
}
.video-icon-container i {
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
} }
</style> </style>
} }
<div id="login-row" class="row justify-content-center align-items-center"> <div class="login-container">
<div id="login-column" class="col-md-6"> <div class="login-icon">
<div class="box"> <i class="bi bi-play-btn-fill"></i>
<div class="shape1"></div> </div>
<div class="shape2"></div> <h1 class="login-title">Bem-vindo ao SumaTube</h1>
<div class="shape3"></div> <p class="login-subtitle">Faça login para criar resumos inteligentes de vídeos do YouTube</p>
<div class="shape4"></div> <div class="video-icon-container">
<div class="shape5"></div> <i class="bi bi-file-earmark-text"></i>
<div class="shape6"></div> <i class="bi bi-arrow-left-right mx-2"></i>
<div class="shape7"></div> <i class="bi bi-youtube"></i>
<div class="float"> </div>
<br /> @using (Html.BeginForm("ExternalLoginGoogle", "Login", new { provider = "Google" }, FormMethod.Post, true, new { id = "googleLoginForm" }))
<br />
@using (Html.BeginForm("ExternalLogin", "Login", new { provider = "Microsoft" }, FormMethod.Post, true, new { id = "externalLoginForm", style = "margin-left: 150px" }))
{ {
@Html.AntiForgeryToken() @Html.AntiForgeryToken()
<button type="submit" class="btn btn-success btn-block login">Login with Microsoft</button> <button type="submit" class="login-button">
} <i class="bi bi-google"></i>
<br /> Continuar/Entrar com Google
@using (Html.BeginForm("ExternalLoginGoogle", "Login", new { provider = "Google" }, FormMethod.Post, true, new { id = "externalLoginForm", style = "margin-left: 150px" })) </button>
{
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-success btn-block login">Login with Google</button>
} }
</div> <div class="login-footer">
</div> Ao fazer login, você concorda com nossos <a href="/termos">Termos de Serviço</a> e <a href="/privacidade">Política de Privacidade</a>
</div> </div>
</div> </div>

View File

@ -0,0 +1,109 @@
@{
ViewBag.Title = "Planos";
}
<div class="row db-padding-btm db-attached">
<div class="col-4 col-xs-4 col-sm-4 col-md-4 col-lg-4">
<div class="db-wrapper">
<div class="db-pricing-eleven db-bk-color-one">
<div class="price text-price-responsive">
<sup>R$</sup> 0,00
<small>para sempre!</small>
</div>
<div class="type text-type-responsive">
Básico
</div>
<ul class="text-responsive">
<li><i class="bi bi-plus-square-fill text-success"></i>1 Bio/Links <span class="hide-in-cell">pessoal virtual</span></li>
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Exibir imagem </span>Whatsapp *</li>
<li><i class="bi bi-plus-square-fill text-success"></i>Até 3 links </li>
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Sem página de</span> Produtos</li>
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Sem contador de </span>Visualizações</span></li>
</ul>
<div class="pricing-footer">
@if (User.Identity.IsAuthenticated)
{
<div class="btn db-button-color-square btn-lg">Plano Inicial</div>
}
else
{
<div class="btn db-button-color-square btn-lg">Comece com este!</div>
}
</div>
</div>
</div>
</div>
<div class="col-4 col-xs-4 col-sm-4 col-md-4 col-lg-4">
<div class="db-wrapper">
<div class="db-pricing-eleven db-bk-color-two popular">
<div class="price text-price-responsive">
<sup>R$</sup>13
<small>por mês</small>
</div>
<div class="type text-type-responsive">
BIO
</div>
<ul>
<li><i class="bi bi-plus-square-fill text-success"></i>1 Bio/Links <span class="hide-in-cell">pessoal virtual</span></li>
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Exibir cartão </span>Whatsapp *</li>
<li><i class="bi bi-plus-square-fill text-success"></i>100 links</li>
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Sem página de</span> Produtos</li>
@*
<li><i class="bi bi-plus-square-fill text-success"></i>1 Link agendado **</li>
*@
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Contador de</span> Visualizações</li>
@*
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Registro de contatos</span> </li>
*@
</ul>
<div class="pricing-footer">
@if (User.Identity.IsAuthenticated)
{
<a href="/Pay/?plan=bio" class="btn db-button-color-square btn-lg">Assinar</a>
}
else
{
<a href="/Login" class="btn db-button-color-square btn-lg">Login / Registro</a>
}
</div>
</div>
</div>
</div>
<div class="col-4 col-xs-4 col-sm-4 col-md-4 col-lg-4">
<div class="db-wrapper">
<div class="db-pricing-eleven db-bk-color-three">
<div class="price text-price-responsive">
<sup>R$</sup>27
<small>por mês</small>
</div>
<div class="type text-type-responsive">
CATÁLOGO
</div>
<ul>
<li><i class="bi bi-plus-square-fill text-success"></i>1 Bio/Cartão <span class="hide-in-cell">pessoal virtual</span></li>
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Exibir cartão </span>Whatsapp *</li>
<li><i class="bi bi-plus-square-fill text-success"></i>Sem limite de links </li>
<li><i class="bi bi-plus-square-fill text-success"></i>Links de Produtos</li>
<li><i class="bi bi-plus-square-fill text-success"></i>Contador<span class="hide-in-cell"> de visualizações</span> </li>
@*
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Registro de contatos</span> </li>
*@
</ul>
<div class="pricing-footer">
@if (User.Identity.IsAuthenticated)
{
<button type="button" class="btn db-button-color-square btn-lg" onclick="waitingDialog.show('Custom message');window.location.href = '/Pay/?plan=catalogo';">Assinar</button>
@*
<a href="/Pay/?plan=catalogo" class="btn db-button-color-square btn-lg">Assinar</a>
*@
}
else
{
<a href="/Login" class="btn db-button-color-square btn-lg">Login / Registro</a>
}
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,225 @@
/*=============================================================
Authour URL: www.designbootstrap.com
http://www.designbootstrap.com/
License: MIT
======================================================== */
/*============================================================
BACKGROUND COLORS
============================================================*/
.db-bk-color-one {
background-color: #D9EDD2;
}
.db-bk-color-two {
background-color: #B9CEA0;
}
.db-bk-color-three {
background-color: #D9EDD2;
}
.db-bk-color-six {
background-color: #F59B24;
}
.db-padding-btm {
padding-bottom: 50px;
}
.db-button-color-square {
color: #fff;
background-color: rgba(0, 0, 0, 0.50);
border: none;
border-radius: 0px;
-webkit-border-radius: 0px;
-moz-border-radius: 0px;
}
.db-button-color-square:hover {
color: #fff;
background-color: rgba(0, 0, 0, 0.50);
border: none;
}
.db-pricing-eleven {
margin-bottom: 30px;
margin-top: 50px;
text-align: center;
box-shadow: 0 0 5px rgba(0, 0, 0, .5);
border-radius: 7px 7px 7px 7px;
color: #101211;
line-height: 30px;
}
.db-pricing-eleven ul {
list-style: none;
margin: 0;
text-align: left;
padding-left: 5px;
}
.db-pricing-eleven ul li {
padding-top: 20px;
padding-bottom: 20px;
cursor: pointer;
}
.db-pricing-eleven ul li i {
margin-right: 5px;
}
.db-pricing-eleven .price {
background-color: rgba(0, 0, 0, 0);
color: #353000;
}
.db-pricing-eleven .price small {
color: #63783F;
display: block;
font-size: 12px;
margin-top: 22px;
}
.db-pricing-eleven .type {
background-color: #63783F;
padding: 50px 20px;
font-weight: 900;
text-transform: uppercase;
}
.text-price-responsive {
font-size: 30px;
padding: 20px 10px 10px 10px;
font-weight: 500;
}
.text-type-responsive {
color: white;
font-size: 20px;
}
.text-responsive {
font-size: 16px;
}
.hide-in-cell {
display: none
}
@media (min-width: 544px) {
.db-pricing-eleven ul {
list-style: none;
margin: 0;
text-align: left;
padding-left: 5px;
}
.text-price-responsive {
font-size: 30px;
padding: 20px 10px 10px 10px;
font-weight: 500;
}
.text-type-responsive {
font-size: 30px;
color: white;
}
.text-responsive {
font-size: 16px;
}
.hide-in-cell {
display: none
}
}
/* Medium devices (tablets, 768px and up) */
@media (min-width: 768px) {
.text-price-responsive {
padding: 40px 20px 20px 20px;
font-size: 40px;
}
.text-type-responsive {
font-size: 30px;
color: white;
}
.text-responsive {
font-size: 20px;
}
}
/* Large devices (desktops, 992px and up) */
@media (min-width: 992px) {
.text-price-responsive {
padding: 40px 20px 20px 20px;
font-size: 60px;
}
.text-type-responsive {
font-size: 30px;
color: white;
}
.text-responsive {
font-size: 20px;
}
}
/* Extra large devices (large desktops, 1200px and up) */
@media (min-width: 1200px) {
.db-pricing-eleven ul {
list-style: none;
margin: 0;
text-align: left;
padding-left: 20px;
}
.text-price-responsive {
padding: 40px 20px 20px 20px;
font-size: 60px;
}
.text-type-responsive {
color: white;
font-size: 30px;
}
.text-responsive {
font-size: 16px;
}
.hide-in-cell {
display: inline
}
}
.db-pricing-eleven .pricing-footer {
padding: 20px;
}
.db-attached > .col-lg-4,
.db-attached > .col-lg-3,
.db-attached > .col-md-4,
.db-attached > .col-md-3,
.db-attached > .col-sm-4,
.db-attached > .col-sm-3 {
padding-left: 0;
padding-right: 0;
}
.db-pricing-eleven.popular {
margin-top: 10px;
}
.db-pricing-eleven.popular .price {
padding-top: 80px;
}

View File

@ -1,109 +1,394 @@
@{ @{
ViewBag.Title = "Planos"; ViewData["Title"] = "Planos";
} }
<div class="row db-padding-btm db-attached"> @section Styles {
<div class="col-4 col-xs-4 col-sm-4 col-md-4 col-lg-4"> <style type="text/css">
<div class="db-wrapper"> .plans-header {
<div class="db-pricing-eleven db-bk-color-one"> background-color: var(--suma-beige);
<div class="price text-price-responsive"> padding: 3rem 0;
<sup>R$</sup> 0,00 border-radius: 0 0 50% 50% / 20px;
<small>para sempre!</small> margin-bottom: 3rem;
</div>
<div class="type text-type-responsive">
Básico
</div>
<ul class="text-responsive">
<li><i class="bi bi-plus-square-fill text-success"></i>1 Bio/Links <span class="hide-in-cell">pessoal virtual</span></li>
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Exibir imagem </span>Whatsapp *</li>
<li><i class="bi bi-plus-square-fill text-success"></i>Até 3 links </li>
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Sem página de</span> Produtos</li>
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Sem contador de </span>Visualizações</span></li>
</ul>
<div class="pricing-footer">
@if (User.Identity.IsAuthenticated)
{
<div class="btn db-button-color-square btn-lg">Plano Inicial</div>
} }
else
{
<div class="btn db-button-color-square btn-lg">Comece com este!</div>
}
</div>
</div>
</div>
</div>
<div class="col-4 col-xs-4 col-sm-4 col-md-4 col-lg-4">
<div class="db-wrapper">
<div class="db-pricing-eleven db-bk-color-two popular">
<div class="price text-price-responsive">
<sup>R$</sup>13
<small>por mês</small>
</div>
<div class="type text-type-responsive">
BIO
</div>
<ul>
<li><i class="bi bi-plus-square-fill text-success"></i>1 Bio/Links <span class="hide-in-cell">pessoal virtual</span></li>
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Exibir cartão </span>Whatsapp *</li>
<li><i class="bi bi-plus-square-fill text-success"></i>100 links</li>
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Sem página de</span> Produtos</li>
@*
<li><i class="bi bi-plus-square-fill text-success"></i>1 Link agendado **</li>
*@
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Contador de</span> Visualizações</li>
@*
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Registro de contatos</span> </li>
*@
</ul>
<div class="pricing-footer">
@if (User.Identity.IsAuthenticated)
{
<a href="/Pay/?plan=bio" class="btn db-button-color-square btn-lg">Assinar</a>
}
else
{
<a href="/Login" class="btn db-button-color-square btn-lg">Login / Registro</a>
}
</div>
</div>
</div>
</div>
<div class="col-4 col-xs-4 col-sm-4 col-md-4 col-lg-4">
<div class="db-wrapper">
<div class="db-pricing-eleven db-bk-color-three">
<div class="price text-price-responsive">
<sup>R$</sup>27
<small>por mês</small>
</div>
<div class="type text-type-responsive">
CATÁLOGO
</div>
<ul>
<li><i class="bi bi-plus-square-fill text-success"></i>1 Bio/Cartão <span class="hide-in-cell">pessoal virtual</span></li>
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Exibir cartão </span>Whatsapp *</li>
<li><i class="bi bi-plus-square-fill text-success"></i>Sem limite de links </li>
<li><i class="bi bi-plus-square-fill text-success"></i>Links de Produtos</li>
<li><i class="bi bi-plus-square-fill text-success"></i>Contador<span class="hide-in-cell"> de visualizações</span> </li>
@*
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Registro de contatos</span> </li>
*@
</ul>
<div class="pricing-footer">
@if (User.Identity.IsAuthenticated)
{
<button type="button" class="btn db-button-color-square btn-lg" onclick="waitingDialog.show('Custom message');window.location.href = '/Pay/?plan=catalogo';">Assinar</button>
@* .pricing-card {
<a href="/Pay/?plan=catalogo" class="btn db-button-color-square btn-lg">Assinar</a> background-color: white;
*@ border-radius: 12px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
transition: all 0.3s ease;
height: 100%;
border: 2px solid transparent;
} }
else
{ .pricing-card:hover {
<a href="/Login" class="btn db-button-color-square btn-lg">Login / Registro</a> transform: translateY(-10px);
box-shadow: 0 15px 30px rgba(0,0,0,0.1);
} }
.pricing-card.popular {
border-color: var(--suma-red);
position: relative;
}
.popular-badge {
position: absolute;
top: 0;
right: 0;
background-color: var(--suma-red);
color: white;
padding: 5px 15px;
font-size: 0.8rem;
border-radius: 0 12px 0 12px;
}
.pricing-header {
text-align: center;
padding: 1.5rem;
border-bottom: 1px solid var(--suma-gray);
}
.pricing-price {
font-size: 2.5rem;
font-weight: 700;
margin: 1rem 0;
color: var(--suma-red);
}
.pricing-period {
font-size: 0.9rem;
color: var(--suma-dark-gray);
}
.pricing-features {
padding: 1.5rem;
}
.pricing-feature {
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.pricing-feature i {
color: var(--suma-red);
margin-right: 10px;
}
.pricing-feature.disabled {
color: var(--suma-dark-gray);
text-decoration: line-through;
}
.pricing-action {
padding: 1.5rem;
text-align: center;
}
.btn-outline-red {
color: var(--suma-red);
border-color: var(--suma-red);
}
.btn-outline-red:hover {
background-color: var(--suma-red);
color: white;
}
.faq-item {
margin-bottom: 1.5rem;
}
.faq-question {
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--suma-red);
}
</style>
}
<!-- Header da página -->
<div class="plans-header">
<div class="container text-center">
<h1 class="display-4 mb-3">Planos de Assinatura</h1>
<p class="lead mb-4">Escolha o plano ideal para suas necessidades de resumo de vídeos</p>
<div class="d-flex justify-content-center">
<div class="btn-group" role="group">
<button type="button" class="btn btn-light active" id="monthlyBtn">Mensal</button>
<button type="button" class="btn btn-light" id="yearlyBtn">Anual (20% desconto)</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Cards de planos -->
<div class="container mb-5">
<div class="row">
<!-- Plano Gratuito -->
<div class="col-md-4 mb-4">
<div class="pricing-card">
<div class="pricing-header">
<h3>Gratuito</h3>
<div class="pricing-price">R$ 0</div>
<div class="pricing-period">para sempre</div>
</div> </div>
<div class="pricing-features">
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>3 resumos por mês</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Resumos básicos</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Vídeos de até 10 minutos</span>
</div>
<div class="pricing-feature disabled">
<i class="bi bi-x-circle"></i>
<span>Download de resumos</span>
</div>
<div class="pricing-feature disabled">
<i class="bi bi-x-circle"></i>
<span>Transcrição completa</span>
</div>
<div class="pricing-feature disabled">
<i class="bi bi-x-circle"></i>
<span>Suporte prioritário</span>
</div>
</div>
<div class="pricing-action">
<a href="#" class="btn btn-outline-red btn-lg btn-block">Começar grátis</a>
</div>
</div>
</div>
<!-- Plano Premium -->
<div class="col-md-4 mb-4">
<div class="pricing-card popular">
<div class="popular-badge">Mais popular</div>
<div class="pricing-header">
<h3>Premium</h3>
<div class="pricing-price" id="premiumPrice">R$ 29,90</div>
<div class="pricing-period" id="premiumPeriod">por mês</div>
</div>
<div class="pricing-features">
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Resumos ilimitados</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Resumos detalhados</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Vídeos de até 3 horas</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Download de resumos (PDF)</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Transcrição completa</span>
</div>
<div class="pricing-feature disabled">
<i class="bi bi-x-circle"></i>
<span>Suporte prioritário</span>
</div>
</div>
<div class="pricing-action">
<a href="#" class="btn btn-primary btn-lg btn-block">Assinar agora</a>
</div>
</div>
</div>
<!-- Plano Pro -->
<div class="col-md-4 mb-4">
<div class="pricing-card">
<div class="pricing-header">
<h3>Profissional</h3>
<div class="pricing-price" id="proPrice">R$ 59,90</div>
<div class="pricing-period" id="proPeriod">por mês</div>
</div>
<div class="pricing-features">
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Tudo do plano Premium</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Resumos para vídeos de qualquer duração</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Análise avançada de conteúdo</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Extração de pontos-chave personalizados</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>API para integração</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Suporte prioritário 24/7</span>
</div>
</div>
<div class="pricing-action">
<a href="#" class="btn btn-outline-red btn-lg btn-block">Experimente 7 dias grátis</a>
</div>
</div>
</div>
</div>
<!-- Tabela comparativa -->
<div class="mt-5 mb-5">
<h3 class="text-center mb-4">Comparação detalhada dos planos</h3>
<div class="table-responsive">
<table class="table table-hover">
<thead class="thead-light">
<tr>
<th>Recurso</th>
<th class="text-center">Gratuito</th>
<th class="text-center">Premium</th>
<th class="text-center">Profissional</th>
</tr>
</thead>
<tbody>
<tr>
<td>Resumos mensais</td>
<td class="text-center">3</td>
<td class="text-center">Ilimitados</td>
<td class="text-center">Ilimitados</td>
</tr>
<tr>
<td>Duração máxima dos vídeos</td>
<td class="text-center">10 minutos</td>
<td class="text-center">3 horas</td>
<td class="text-center">Sem limite</td>
</tr>
<tr>
<td>Transcrição completa</td>
<td class="text-center"><i class="bi bi-x text-danger"></i></td>
<td class="text-center"><i class="bi bi-check text-success"></i></td>
<td class="text-center"><i class="bi bi-check text-success"></i></td>
</tr>
<tr>
<td>Download de resumos</td>
<td class="text-center"><i class="bi bi-x text-danger"></i></td>
<td class="text-center"><i class="bi bi-check text-success"></i></td>
<td class="text-center"><i class="bi bi-check text-success"></i></td>
</tr>
<tr>
<td>Palavras-chave e tópicos</td>
<td class="text-center">Básico</td>
<td class="text-center">Avançado</td>
<td class="text-center">Personalizado</td>
</tr>
<tr>
<td>Acesso a API</td>
<td class="text-center"><i class="bi bi-x text-danger"></i></td>
<td class="text-center"><i class="bi bi-x text-danger"></i></td>
<td class="text-center"><i class="bi bi-check text-success"></i></td>
</tr>
<tr>
<td>Suporte</td>
<td class="text-center">Email</td>
<td class="text-center">Email e chat</td>
<td class="text-center">Prioritário 24/7</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- FAQs -->
<div class="container mb-5">
<h3 class="text-center mb-4">Perguntas Frequentes</h3>
<div class="row">
<div class="col-md-6">
<div class="faq-item">
<h5 class="faq-question">Como funciona o SumaTube?</h5>
<p>O SumaTube usa tecnologia avançada de IA para extrair e analisar as legendas dos vídeos do YouTube, criando resumos inteligentes que capturam os pontos principais do conteúdo.</p>
</div>
<div class="faq-item">
<h5 class="faq-question">Posso cancelar minha assinatura a qualquer momento?</h5>
<p>Sim, você pode cancelar sua assinatura a qualquer momento. Não há contratos de longo prazo ou taxas de cancelamento.</p>
</div>
<div class="faq-item">
<h5 class="faq-question">Como são processados os pagamentos?</h5>
<p>Processamos pagamentos via cartão de crédito, débito, PayPal e Pix. Todas as transações são seguras e criptografadas.</p>
</div>
</div>
<div class="col-md-6">
<div class="faq-item">
<h5 class="faq-question">E se o vídeo não tiver legendas?</h5>
<p>O SumaTube pode gerar legendas automaticamente para vídeos que não as possuem, embora a precisão possa variar dependendo da qualidade do áudio.</p>
</div>
<div class="faq-item">
<h5 class="faq-question">Quais idiomas são suportados?</h5>
<p>Atualmente suportamos resumos em português, inglês e espanhol. Estamos constantemente adicionando suporte para novos idiomas.</p>
</div>
<div class="faq-item">
<h5 class="faq-question">Como posso entrar em contato com o suporte?</h5>
<p>Você pode entrar em contato conosco através do email suporte@sumatube.com ou pelo chat disponível no site para usuários Premium e Profissional.</p>
</div>
</div>
</div>
<!-- Call to Action -->
<div class="text-center mt-5">
<h3 class="mb-3">Pronto para começar?</h3>
<p class="mb-4">Escolha o plano ideal para suas necessidades e comece a usar o SumaTube hoje mesmo.</p>
<a href="#" class="btn btn-primary btn-lg">Criar conta gratuita</a>
</div>
</div>
@section Scripts {
<script type="text/javascript">
$(document).ready(function() {
// Toggle entre planos mensais e anuais
$('#monthlyBtn').click(function() {
$(this).addClass('active');
$('#yearlyBtn').removeClass('active');
// Atualizar preços para mensais
$('#premiumPrice').text('R$ 29,90');
$('#premiumPeriod').text('por mês');
$('#proPrice').text('R$ 59,90');
$('#proPeriod').text('por mês');
});
$('#yearlyBtn').click(function() {
$(this).addClass('active');
$('#monthlyBtn').removeClass('active');
// Atualizar preços para anuais com desconto
$('#premiumPrice').text('R$ 287,04');
$('#premiumPeriod').text('por ano (R$ 23,92/mês)');
$('#proPrice').text('R$ 575,04');
$('#proPeriod').text('por ano (R$ 47,92/mês)');
});
// Animação aos cards
$('.pricing-card').hover(
function() {
$(this).addClass('shadow-lg');
},
function() {
$(this).removeClass('shadow-lg');
}
);
});
</script>
}

View File

@ -1,118 +1,189 @@
@using System.Security.Claims @using System.Security.Claims
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="pt-br">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Blinks</title> <title>@ViewData["Title"] - SumaTube</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/custom.css" asp-append-version="true" />
<link rel="stylesheet" href="~/SumaTube.styles.css" asp-append-version="true" /> <link rel="stylesheet" href="~/SumaTube.styles.css" asp-append-version="true" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
@await RenderSectionAsync("Styles", required: false) @await RenderSectionAsync("Styles", required: false)
</head> </head>
<body class="hide-body"> <body class="d-flex flex-column min-vh-100 hide-body">
<partial name="_Busy" /> <partial name="_Busy" />
<div id="wrapper">
<header>
<nav id="nav-bar" class="navbar navbar-expand-lg navbar-dark"> <nav id="nav-bar" class="navbar navbar-expand-lg navbar-dark">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">SumaTube</a> <div class="container">
<a class="navbar-brand d-flex align-items-center" asp-area="" asp-controller="Home" asp-action="Index">
<i class="bi bi-play-btn-fill mr-2"></i> SumaTube
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<!-- Campo de busca central -->
<div class="d-none d-md-block mx-auto">
<form class="form-inline my-2 my-lg-0">
<div class="input-group">
<input class="form-control search-input" type="search" placeholder="Buscar vídeos..." aria-label="Buscar">
<div class="input-group-append">
<button class="btn btn-light" type="submit">
<i class="bi bi-search"></i>
</button>
</div>
</div>
</form>
</div>
<div class="navbar-collapse collapse" id="navbarNav"> <div class="navbar-collapse collapse" id="navbarNav">
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Home" asp-action="Index">Home</a> <a class="nav-link text-white" asp-area="" asp-controller="Home" asp-action="Index">
<i class="bi bi-house-door"></i> Home
</a>
</li>
@*
Exibir apenas são não estiver logado.
*@
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Plans" asp-action="Index">
<i class="bi bi-star"></i> Planos
</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> <a class="nav-link text-white" asp-area="" asp-controller="Video" asp-action="MySummary">
<i class="bi bi-file-text"></i> Meus resumos
</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Plans" asp-action="Index">Planos</a> <a class="nav-link text-white" asp-area="" asp-controller="Video" asp-action="Extract">
<i class="bi bi-magic"></i> Extract
</a>
</li> </li>
<partial name="_Language" />
</ul> </ul>
@if (User != null && User.Identity != null && !User.Identity.IsAuthenticated) @if (User != null && User.Identity != null && !User.Identity.IsAuthenticated)
{ {
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<partial name="_Language" /> <li class="nav-item">
<li class="nav-item" style="margin-right: 10px"> <a class="nav-link text-white" asp-area="" asp-controller="Login" asp-action="Index">
<a class="nav-link text-white" asp-area="" asp-controller="Login" asp-action="Index"><i class="bi bi-person"></i> Login</a> <i class="bi bi-box-arrow-in-right"></i> Login
</a>
</li> </li>
</ul> </ul>
} }
else else
{ {
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<partial name="_Language"/>
<li class="nav-item dropdown" style="margin-right: 10px">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="bi bi-person"></i> @(User.FindFirst("FullName")!=null ? User.FindFirst("FullName").Value : "N/A")
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" asp-area="" asp-controller="Login" asp-action="Logout">Sair</a>
@* @*
<a class="dropdown-item" href="#">Action</a> Criar uma tela que permita alterar o plano.
<a class="dropdown-item" href="#">Another action</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Something else here</a>
*@ *@
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-white" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="bi bi-person-circle"></i> @(User.FindFirst("FullName") != null ? User.FindFirst("FullName").Value : "N/A")
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<a class="dropdown-item" asp-controller="Account" asp-action="Profile">
<i class="bi bi-person"></i> Meu Perfil
</a>
<a class="dropdown-item" asp-controller="Videos" asp-action="History">
<i class="bi bi-clock-history"></i> Histórico
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" asp-area="" asp-controller="Login" asp-action="Logout">
<i class="bi bi-box-arrow-right"></i> Sair
</a>
</div> </div>
</li> </li>
@*
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Login" asp-action="Logout">@User.FindFirst("FullName").Value
</a>
</li>
*@
</ul> </ul>
} }
</div> </div>
</nav>
<div class="container">
@RenderBody()
@*
<main role="main" class="pb-3">
</main>
*@
</div> </div>
</nav>
<footer class="border-top footer text-muted"> <!-- Campo de busca para dispositivos móveis -->
<div class="container d-md-none mt-3 mb-3">
<form class="form-inline">
<div class="input-group w-100">
<input class="form-control" type="search" placeholder="Buscar vídeos..." aria-label="Buscar">
<div class="input-group-append">
<button class="btn btn-light" type="submit">
<i class="bi bi-search"></i>
</button>
</div>
</div>
</form>
</div>
</header>
<!-- Conteúdo principal -->
<main class="flex-grow-1">
<div class="container py-4">
@RenderBody()
</div>
</main>
<!-- Footer -->
<footer class="footer mt-auto py-3">
<div class="container"> <div class="container">
&copy; 2024 - Blinks - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> <div class="row align-items-center">
<div class="col-md-4 mb-3 mb-md-0">
<div class="d-flex align-items-center">
<i class="bi bi-play-btn-fill mr-2" style="color: var(--suma-red); font-size: 1.2rem;"></i>
<span class="font-weight-bold">SumaTube</span>
<span class="mx-2">|</span>
<small>Resumos inteligentes de vídeos</small>
</div>
</div>
<div class="col-md-4 mb-3 mb-md-0 text-center">
<div class="d-flex justify-content-center">
<a href="#" class="text-muted mx-2"><i class="bi bi-facebook"></i></a>
<a href="#" class="text-muted mx-2"><i class="bi bi-twitter"></i></a>
<a href="#" class="text-muted mx-2"><i class="bi bi-instagram"></i></a>
<a href="#" class="text-muted mx-2"><i class="bi bi-youtube"></i></a>
</div>
</div>
<div class="col-md-4 text-md-right">
<div class="text-center text-md-right">
<small class="text-muted">&copy; 2024 - Todos os direitos reservados</small>
</div>
</div>
</div>
</div> </div>
</footer> </footer>
</div>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script>
<script src="~/js/site.js" asp-append-version="true"></script> <script src="~/js/site.js" asp-append-version="true"></script>
<script src="~/js/wait.js" asp-append-version="true"></script> <script src="~/js/wait.js" asp-append-version="true"></script>
<script type="text/javascript"> <script type="text/javascript">
$(function () { $(function () {
$(document).ready(function () { $(document).ready(function () {
$('.loading').hide(); $('.loading').hide();
//$('body').fadeIn(1000); $('body').fadeIn(800);
$('body').slideDown('slow');
}); });
$('a[href="#search"]').on('click', function (event) { // Animação para links do menu
event.preventDefault(); $('.navbar-nav .nav-link').hover(
$('#search').addClass('open'); function() { $(this).addClass('pulse'); },
$('#search > form > input[type="search"]').focus(); function() { $(this).removeClass('pulse'); }
inactivateActiveOption(); );
searchActive();
});
$('#search, #search button.close').on('click keyup', function (event) { // Ativar o item de menu atual
if (event.target == this || event.target.className == 'close' || event.keyCode == 27) { $(document).ready(function () {
$(this).removeClass('open');
setActiveByLocation(); setActiveByLocation();
searchInactive();
}
}); });
// Controle de carregamento de página
$(window).on('beforeunload', function () { $(window).on('beforeunload', function () {
displayBusyIndicator(); displayBusyIndicator();
}); });
@ -121,20 +192,10 @@
displayBusyIndicator(); displayBusyIndicator();
}); });
$(document).ready(function () {
$('#wrapper').fadeIn('slow');
$('a.nav-link').click(function () {
$('#wrapper').fadeOut('slow');
});
setActiveByLocation();
});
function setActiveByLocation() { function setActiveByLocation() {
$('ul.navbar-nav').find('a[href="' + location.pathname + '"]') $('ul.navbar-nav').find('a[href="' + location.pathname + '"]')
.closest('li') .closest('li')
.addClass("active") .addClass("active");
$('ul.navbar-nav').find('a[href="' + location.pathname + '"]') $('ul.navbar-nav').find('a[href="' + location.pathname + '"]')
.closest('a') .closest('a')
@ -142,37 +203,9 @@
.addClass('text-black'); .addClass('text-black');
} }
function searchActive() {
$('#searchLi').addClass("active");
$('#searchLi > a')
.removeClass('text-white')
.addClass('text-black');
}
function searchInactive() {
$('#searchLi').removeClass("active");
$('#searchLi > a')
.removeClass('text-black')
.addClass('text-white');
}
function inactivateActiveOption() {
$('ul.navbar-nav li.active')
.closest('li')
.removeClass("active")
$('ul.navbar-nav').find('a[href="' + location.pathname + '"]')
.closest('a')
.removeClass('text-black')
.addClass('text-white');
}
function displayBusyIndicator() { function displayBusyIndicator() {
//$('body').fadeOut(1000); $('body').fadeOut(300);
$('body').slideUp('slow');
} }
//$(".hide-body").fadeOut(2000);
}); });
</script> </script>
@await RenderSectionAsync("Scripts", required: false) @await RenderSectionAsync("Scripts", required: false)

View File

@ -44,14 +44,6 @@ button.accept-policy {
line-height: inherit; line-height: inherit;
} }
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}
#search { #search {
position: fixed; position: fixed;
top: 0px; top: 0px;

View File

@ -11,7 +11,7 @@
<html> <html>
<head> <head>
<title>Blinks</title> <title>SumaTube</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" integrity="sha384-gfdkjb5BdAXd+lj+gudLWI+BXq4IuLW5IT+brZEZsLFm++aCMlF1V92rMkPaX4PP" crossorigin="anonymous"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" integrity="sha384-gfdkjb5BdAXd+lj+gudLWI+BXq4IuLW5IT+brZEZsLFm++aCMlF1V92rMkPaX4PP" crossorigin="anonymous">

View File

@ -1,4 +1,4 @@
@using Blinks @using SumaTube
@* @*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860

View File

@ -0,0 +1,273 @@
@{
ViewData["Title"] = "Extrair Resumo";
}
@section Styles {
<style type="text/css">
.extract-container {
max-width: 800px;
margin: 2rem auto;
}
.extract-card {
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
padding: 2rem;
}
.form-control-lg {
font-size: 1.1rem;
padding: 1rem 1.5rem;
border-radius: 30px 0 0 30px;
}
.btn-extract {
border-radius: 0 30px 30px 0;
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.feature-icon {
background-color: var(--suma-beige);
border-radius: 50%;
width: 70px;
height: 70px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem auto;
}
.feature-icon i {
font-size: 2rem;
color: var(--suma-red);
}
.step-number {
background-color: var(--suma-red);
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 1rem;
}
.progress-container {
display: none;
margin-top: 2rem;
}
.progress {
height: 10px;
border-radius: 5px;
}
</style>
}
<div class="container extract-container">
<div class="text-center mb-5">
<h1 class="display-4 mb-3">Resumir Vídeo do YouTube</h1>
<p class="lead">Cole o link de qualquer vídeo do YouTube para obter um resumo inteligente</p>
</div>
<div class="extract-card mb-5">
<form id="extractForm">
<div class="form-group">
<label for="youtubeUrl"><strong>URL do vídeo</strong></label>
<div class="input-group input-group-lg">
<input type="url" class="form-control form-control-lg" id="youtubeUrl" placeholder="https://www.youtube.com/watch?v=..." required>
<div class="input-group-append">
<button type="submit" class="btn btn-primary btn-lg btn-extract" id="extractButton">
<i class="bi bi-magic mr-2"></i> Resumir
</button>
</div>
</div>
<small class="form-text text-muted">Ex: https://www.youtube.com/watch?v=abcde12345</small>
</div>
<div class="progress-container" id="progressContainer">
<p class="text-center mb-2" id="processingStatus">Processando vídeo...</p>
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-danger" id="progressBar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%"></div>
</div>
</div>
</form>
</div>
<!-- Como funciona -->
<div class="mb-5">
<h3 class="text-center mb-4">Como funciona</h3>
<div class="row">
<!-- Passo 1 -->
<div class="col-md-4 mb-4">
<div class="feature-icon">
<i class="bi bi-link-45deg"></i>
</div>
<h5 class="text-center mb-3">Passo 1</h5>
<div class="d-flex align-items-start">
<div class="step-number">1</div>
<div>
Cole o link do vídeo do YouTube que deseja resumir no campo acima.
</div>
</div>
</div>
<!-- Passo 2 -->
<div class="col-md-4 mb-4">
<div class="feature-icon">
<i class="bi bi-cpu"></i>
</div>
<h5 class="text-center mb-3">Passo 2</h5>
<div class="d-flex align-items-start">
<div class="step-number">2</div>
<div>
Nossa IA extrai as legendas e processa o conteúdo do vídeo automaticamente.
</div>
</div>
</div>
<!-- Passo 3 -->
<div class="col-md-4 mb-4">
<div class="feature-icon">
<i class="bi bi-file-earmark-text"></i>
</div>
<h5 class="text-center mb-3">Passo 3</h5>
<div class="d-flex align-items-start">
<div class="step-number">3</div>
<div>
Receba um resumo completo com os principais pontos, timestamps e transcrição.
</div>
</div>
</div>
</div>
</div>
<!-- Recursos -->
<div class="row">
<div class="col-12 text-center mb-4">
<h3>Recursos incríveis do SumaTube</h3>
</div>
<!-- Recurso 1 -->
<div class="col-md-6 mb-4">
<div class="summary-section d-flex">
<div class="mr-4">
<i class="bi bi-clock" style="font-size: 2rem; color: var(--suma-red);"></i>
</div>
<div>
<h5>Economize tempo</h5>
<p>Extraia o conteúdo mais importante de vídeos longos em questão de minutos.</p>
</div>
</div>
</div>
<!-- Recurso 2 -->
<div class="col-md-6 mb-4">
<div class="summary-section d-flex">
<div class="mr-4">
<i class="bi bi-translate" style="font-size: 2rem; color: var(--suma-red);"></i>
</div>
<div>
<h5>Suporte a múltiplos idiomas</h5>
<p>Resumos disponíveis em português, inglês, espanhol e outros idiomas.</p>
</div>
</div>
</div>
<!-- Recurso 3 -->
<div class="col-md-6 mb-4">
<div class="summary-section d-flex">
<div class="mr-4">
<i class="bi bi-lightning-charge" style="font-size: 2rem; color: var(--suma-red);"></i>
</div>
<div>
<h5>Processamento rápido</h5>
<p>Nossa tecnologia avançada processa os vídeos em segundos.</p>
</div>
</div>
</div>
<!-- Recurso 4 -->
<div class="col-md-6 mb-4">
<div class="summary-section d-flex">
<div class="mr-4">
<i class="bi bi-download" style="font-size: 2rem; color: var(--suma-red);"></i>
</div>
<div>
<h5>Download de resumos</h5>
<p>Salve os resumos em PDF para referência futura e compartilhamento.</p>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script type="text/javascript">
$(document).ready(function() {
$('#extractForm').submit(function(e) {
e.preventDefault();
// Validar URL
var youtubeUrl = $('#youtubeUrl').val();
if (!isValidYouTubeUrl(youtubeUrl)) {
alert('Por favor, insira uma URL válida do YouTube.');
return;
}
// Mostrar progresso
$('#extractButton').prop('disabled', true);
$('#progressContainer').show();
simulateProgress();
// Simulação de processamento - em um projeto real, aqui você faria a chamada AJAX para sua WebAPI
setTimeout(function() {
// Redirecionar para a página de resultado (em um caso real, você redirecionaria após receber a resposta da API)
window.location.href = '/Video/Summary?id=' + extractVideoId(youtubeUrl);
}, 5000);
});
function isValidYouTubeUrl(url) {
var regex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+/;
return regex.test(url);
}
function extractVideoId(url) {
var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
var match = url.match(regExp);
return (match && match[7].length == 11) ? match[7] : false;
}
function simulateProgress() {
var progress = 0;
var interval = setInterval(function() {
progress += 5;
$('#progressBar').css('width', progress + '%');
$('#progressBar').attr('aria-valuenow', progress);
// Atualizar mensagem de status
if (progress <= 20) {
$('#processingStatus').text('Extraindo informações do vídeo...');
} else if (progress <= 40) {
$('#processingStatus').text('Processando legendas...');
} else if (progress <= 60) {
$('#processingStatus').text('Identificando pontos principais...');
} else if (progress <= 80) {
$('#processingStatus').text('Gerando resumo...');
} else {
$('#processingStatus').text('Finalizando...');
}
if (progress >= 100) {
clearInterval(interval);
}
}, 200);
}
});
</script>
}

View File

@ -0,0 +1,83 @@
@using SumaTube.Domain.Entities.Videos
@model List<VideoSummary>
@{
ViewData["Title"] = "Meus Resumos";
}
<div class="container my-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Meus Resumos</h1>
<a href="@Url.Action("Extract", "Video")" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Novo Resumo
</a>
</div>
@if (Model == null || !Model.Any())
{
<div class="text-center py-5">
<i class="bi bi-file-earmark-text" style="font-size: 3rem; color: var(--suma-red);"></i>
<h3 class="mt-3">Você ainda não possui resumos</h3>
<p class="text-muted">Clique em "Novo Resumo" para extrair o conteúdo de um vídeo do YouTube</p>
<a href="@Url.Action("Extract", "Video")" class="btn btn-primary mt-3">
<i class="bi bi-plus-circle"></i> Criar Meu Primeiro Resumo
</a>
</div>
}
else
{
<div class="row">
@foreach (var summary in Model)
{
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 shadow-sm">
<div class="position-relative">
<img src="@summary.ThumbnailUrl" class="card-img-top" alt="@summary.Title">
@if (summary.Status == "PROCESSANDO")
{
<div class="position-absolute top-0 end-0 p-2 bg-warning text-dark rounded-bottom-left">
<i class="bi bi-hourglass-split"></i> Processando
</div>
}
else if (summary.Status == "REALIZADO")
{
<div class="position-absolute top-0 end-0 p-2 bg-success text-white rounded-bottom-left">
<i class="bi bi-check-circle"></i> Concluído
</div>
}
else if (summary.Status == "ERRO")
{
<div class="position-absolute top-0 end-0 p-2 bg-danger text-white rounded-bottom-left">
<i class="bi bi-exclamation-triangle"></i> Erro
</div>
}
</div>
<div class="card-body">
<h5 class="card-title text-truncate" title="@summary.Title">@summary.Title</h5>
<p class="card-text text-muted small">
<i class="bi bi-calendar"></i> @summary.RequestDate.ToString("dd/MM/yyyy HH:mm")
<br>
<i class="bi bi-translate"></i> @summary.Language
</p>
</div>
<div class="card-footer bg-white border-top-0">
<a href="@Url.Action("Summary", "Video", new { id = summary.Id })" class="btn btn-sm btn-outline-primary w-100">
@if (summary.Status == "PROCESSANDO")
{
<span>Ver Progresso</span>
}
else if (summary.Status == "REALIZADO")
{
<span>Ver Resumo</span>
}
else
{
<span>Ver Detalhes</span>
}
</a>
</div>
</div>
</div>
}
</div>
}
</div>

View File

@ -0,0 +1,260 @@
@using SumaTube.Models;
@model VideoSummaryViewModel
@{
ViewData["Title"] = "Resumo do Vídeo";
}
@section Styles {
<style type="text/css">
.timestamp-link {
color: var(--suma-red);
cursor: pointer;
text-decoration: none;
}
.timestamp-link:hover {
text-decoration: underline;
}
.summary-section {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.tag-pill {
background-color: var(--suma-beige);
border-radius: 20px;
padding: 5px 15px;
margin-right: 8px;
margin-bottom: 8px;
display: inline-block;
font-size: 0.9rem;
}
.summary-tab-content {
min-height: 300px;
}
.video-frame {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.transcription-text {
max-height: 300px;
overflow-y: auto;
background-color: var(--suma-light-gray);
padding: 15px;
border-radius: 8px;
line-height: 1.7;
}
</style>
}
<div class="container mt-4">
<div class="row">
<!-- Coluna do vídeo -->
<div class="col-lg-8">
<div class="video-frame mb-4">
<div class="embed-responsive embed-responsive-16by9">
<iframe class="embed-responsive-item" src="https://www.youtube.com/embed/@Model.VideoId" allowfullscreen></iframe>
</div>
</div>
<div class="summary-section">
<h1>@Model.VideoTitle</h1>
<div class="d-flex align-items-center mb-3">
<img src="@Model.ChannelThumbnail" alt="@Model.ChannelName" class="rounded-circle mr-2" style="width: 40px; height: 40px;">
<div>
<h6 class="mb-0">@Model.ChannelName</h6>
<small class="text-muted">@Model.PublishedDate • @Model.ViewCount visualizações</small>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mt-3 mb-3">
<div>
<span class="mr-3">
<i class="bi bi-hand-thumbs-up"></i> @Model.LikeCount
</span>
<span>
<i class="bi bi-chat-left-text"></i> @Model.CommentCount
</span>
</div>
<div>
<button class="btn btn-sm btn-outline-dark mr-2">
<i class="bi bi-share"></i> Compartilhar
</button>
<button class="btn btn-sm btn-outline-dark">
<i class="bi bi-bookmark"></i> Salvar
</button>
</div>
</div>
</div>
<!-- Tabs de navegação -->
<ul class="nav nav-tabs" id="summaryTabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="summary-tab" data-toggle="tab" href="#summary" role="tab" aria-controls="summary" aria-selected="true">
<i class="bi bi-journal-text"></i> Resumo
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="transcription-tab" data-toggle="tab" href="#transcription" role="tab" aria-controls="transcription" aria-selected="false">
<i class="bi bi-body-text"></i> Transcrição
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="keywords-tab" data-toggle="tab" href="#keywords" role="tab" aria-controls="keywords" aria-selected="false">
<i class="bi bi-tags"></i> Palavras-chave
</a>
</li>
</ul>
<!-- Conteúdo das tabs -->
<div class="tab-content summary-tab-content p-3 bg-white border border-top-0 rounded-bottom mb-4" id="summaryTabsContent">
<!-- Tab Resumo -->
<div class="tab-pane fade show active" id="summary" role="tabpanel" aria-labelledby="summary-tab">
<h4 class="mb-3">Principais pontos</h4>
@foreach (var point in Model.KeyPoints)
{
<div class="mb-3">
<p>
@* <a class="timestamp-link" data-time="@point.TimestampSeconds">@point.TimestampFormatted</a>
<strong>@point.Title</strong>
</p>
<p>@point.Description</p>
*@
<a class="timestamp-link" data-time="30">30s</a>
<strong>Titulo</strong>
</p>
<p>Description</p>
</div>
}
<h4 class="mt-4 mb-3">Resumo geral</h4>
<p>@Model.SummaryText</p>
</div>
<!-- Tab Transcrição -->
<div class="tab-pane fade" id="transcription" role="tabpanel" aria-labelledby="transcription-tab">
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center">
<h4 class="mb-3">Transcrição completa</h4>
<button class="btn btn-sm btn-outline-secondary">
<i class="bi bi-download"></i> Baixar
</button>
</div>
<div class="transcription-text">
@foreach (var caption in Model.Captions)
{
<p>
@*
<a class="timestamp-link" data-time="@caption.TimestampSeconds">
@caption.TimestampFormatted
</a>
@caption.Text
*@
<a class="timestamp-link" data-time="30s">
30s
</a>
caption.Text
</p>
}
</div>
</div>
</div>
<!-- Tab Palavras-chave -->
<div class="tab-pane fade" id="keywords" role="tabpanel" aria-labelledby="keywords-tab">
<h4 class="mb-3">Tópicos principais</h4>
<div class="mb-4">
@foreach (var keyword in Model.Keywords)
{
<span class="tag-pill">@keyword</span>
}
</div>
<h4 class="mb-3">Temas relacionados</h4>
<div>
@foreach (var topic in Model.RelatedTopics)
{
<span class="tag-pill">@topic</span>
}
</div>
</div>
</div>
</div>
<!-- Coluna lateral -->
<div class="col-lg-4">
<!-- Vídeos recomendados -->
<div class="summary-section">
<h5 class="mb-3">Vídeos relacionados</h5>
@foreach (var video in Model.RelatedVideos)
{
<div class="media mb-3">
@*
<img src="@video.ThumbnailUrl" class="mr-3" alt="@video.Title" style="width: 120px; border-radius: 4px;">
<div class="media-body">
<h6 class="mt-0">@video.Title</h6>
<small class="text-muted">@video.ChannelName</small><br>
<small class="text-muted">@video.ViewCount visualizações</small>
</div>
*@
<img src="#ThumbnailUrl" class="mr-3" alt="#Title" style="width: 120px; border-radius: 4px;">
<div class="media-body">
<h6 class="mt-0">Title</h6>
<small class="text-muted">video.ChannelName</small><br>
<small class="text-muted">x visualizações</small>
</div>
</div>
}
</div>
<!-- Call to action para planos premium -->
<div class="summary-section text-center" style="background-color: var(--suma-beige);">
<i class="bi bi-star-fill mb-3" style="font-size: 2rem; color: var(--suma-red);"></i>
<h5>Obtenha mais recursos com o plano Premium</h5>
<p class="small mb-3">Acesso ilimitado a resumos, download de textos e muito mais.</p>
<a href="#" class="btn btn-primary btn-block">Ver planos</a>
</div>
</div>
</div>
</div>
@section Scripts {
<script type="text/javascript">
$(document).ready(function() {
// Função para clicar nos timestamps e pular para o momento do vídeo
$('.timestamp-link').click(function() {
var time = $(this).data('time');
var videoFrame = $('iframe')[0];
var videoUrl = videoFrame.src;
// Garantir que a URL tem os parâmetros necessários
if (videoUrl.indexOf('?') === -1) {
videoUrl += '?';
} else {
videoUrl += '&';
}
// Adicionar comando para pular para o tempo específico
videoUrl += 'start=' + time;
// Atualizar o iframe
videoFrame.src = videoUrl;
});
});
</script>
}

View File

@ -1,3 +1,3 @@
@using Blinks @using SumaTube
@using Blinks.Models @using SumaTube.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -1,8 +1,28 @@
{ {
"Logging": { "Serilog": {
"LogLevel": { "WriteTo": [
"Default": "Information", { "Name": "Console" },
"Microsoft.AspNetCore": "Warning" {
"Name": "File",
"Args": {
"path": "logs/dev-app-.log",
"rollingInterval": "Day"
}
},
{
"Name": "Seq",
"Args": {
"serverUrl": "http://192.168.0.76:5341",
"compact": true,
"batchPostingLimit": 100
}
}
],
"Properties": {
"Environment": "Development",
"Workspace": "Dev",
"Application": "SumaTube"
} }
} }
} }

View File

@ -0,0 +1,33 @@
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Error" // Note que em produção aumentamos o nível de logs do sistema
}
},
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "logs/prod-app-.log",
"rollingInterval": "Day"
}
},
{
"Name": "Seq",
"Args": {
"serverUrl": "http://logs-ingest.carneiro.ddnsfree.com",
"compact": true,
"batchPostingLimit": 100
}
}
],
"Properties": {
"Environment": "Production",
"Workspace": "Prod",
"Application": "SumaTube"
}
}
}

View File

@ -1,8 +1,20 @@
{ {
"Logging": { "Serilog": {
"LogLevel": { "MinimumLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"Enrich": [
"FromLogContext",
"WithMachineName",
"WithThreadId",
"WithEnvironmentUserName"
],
"Properties": {
"Application": "SumaTube"
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",

View File

@ -0,0 +1,255 @@
/* Cores principais do SumaTube */
:root {
--suma-red: #cc0000;
--suma-light-red: #e60000;
--suma-dark-red: #990000;
--suma-beige: #f5f5dc;
--suma-light-gray: #f8f8f8;
--suma-gray: #e0e0e0;
--suma-dark-gray: #606060;
--suma-black: #212121;
}
/* Estilos Gerais */
html, body {
height: 100%;
font-family: 'Roboto', 'Segoe UI', sans-serif;
margin: 0;
padding: 0;
}
body {
background-color: var(--suma-light-gray);
color: var(--suma-black);
display: flex;
flex-direction: column;
min-height: 100vh;
}
.hide-body {
display: none;
}
/* Barra de Navegação */
#nav-bar {
background-color: var(--suma-red) !important;
padding: 0.5rem 1rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.navbar-brand {
font-size: 1.5rem;
font-weight: 700;
color: white !important;
}
.navbar-nav .nav-item {
margin: 0 5px;
}
.navbar-nav .nav-item.active {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
.navbar-nav .nav-link {
padding: 8px 16px !important;
border-radius: 3px;
transition: all 0.2s ease;
}
.navbar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.navbar-nav .nav-link.text-black {
color: white !important;
font-weight: 500;
}
/* Conteúdo principal */
main {
flex: 1 0 auto;
display: flex;
flex-direction: column;
}
.container {
padding: 20px 15px;
max-width: 1280px;
}
/* Footer */
.footer {
flex-shrink: 0;
background-color: var(--suma-beige);
color: var(--suma-dark-gray);
padding: 20px 0;
width: 100%;
margin-top: auto;
}
/* Login container */
.login-container {
max-width: 500px;
margin: 60px auto;
background-color: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 30px;
text-align: center;
}
/* Seção de hero banner */
.hero-banner {
background: linear-gradient(135deg, var(--suma-red), var(--suma-dark-red));
color: white;
padding: 2.5rem 0;
margin-bottom: 2rem;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
position: relative;
overflow: hidden;
}
.hero-banner::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 60%;
height: 200%;
background: rgba(255, 255, 255, 0.1);
transform: rotate(-15deg);
pointer-events: none;
}
.hero-banner h1 {
font-weight: 700;
font-size: 2.2rem;
margin-bottom: 1rem;
text-shadow: 0 2px 3px rgba(0, 0, 0, 0.2);
}
.hero-banner .lead {
font-size: 1.15rem;
font-weight: 300;
margin-bottom: 1.5rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
/* Botões */
.btn-primary, .btn-red {
background-color: var(--suma-red) !important;
border-color: var(--suma-dark-red) !important;
padding: 8px 16px;
border-radius: 4px;
transition: all 0.2s ease;
}
.btn-primary:hover, .btn-red:hover {
background-color: var(--suma-light-red) !important;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-hero {
background-color: white;
color: var(--suma-red);
border: none;
padding: 0.8rem 1.5rem;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-hero:hover {
transform: translateY(-2px);
background-color: #f8f8f8;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25);
color: var(--suma-dark-red);
}
/* Responsividade */
@media (max-width: 768px) {
.hero-banner {
padding: 2rem 0;
text-align: center;
}
.hero-banner h1 {
font-size: 1.8rem;
}
.btn-hero {
display: block;
width: 100%;
}
}
/* Animações */
.pulse {
animation: pulse 0.5s ease-in-out;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
/* Efeito de loading */
.loading {
position: fixed;
z-index: 9999;
height: 2em;
width: 2em;
overflow: show;
margin: auto;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.loading:before {
content: '';
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(245, 245, 220, 0.7);
}
.loading:not(:required):after {
content: '';
display: block;
font-size: 10px;
width: 1em;
height: 1em;
margin-top: -0.5em;
animation: spinner 1500ms infinite linear;
border-radius: 0.5em;
box-shadow: rgba(204, 0, 0, 0.75) 1.5em 0 0 0, rgba(204, 0, 0, 0.75) 1.1em 1.1em 0 0, rgba(204, 0, 0, 0.75) 0 1.5em 0 0, rgba(204, 0, 0, 0.75) -1.1em 1.1em 0 0, rgba(204, 0, 0, 0.75) -1.5em 0 0 0, rgba(204, 0, 0, 0.75) -1.1em -1.1em 0 0, rgba(204, 0, 0, 0.75) 0 -1.5em 0 0, rgba(204, 0, 0, 0.75) 1.1em -1.1em 0 0;
}
/* Animação do spinner */
@keyframes spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB