VideoStudy/VideoStudy.UI/Pages/Home.razor
Ricardo Carneiro c49ec3d5a5 refactor: limpeza geral e troca de Puppeteer por FFmpeg
- Remove projetos mortos: VideoStudy.Native (MAUI), Controllers/LicenseController
- Remove serviços não usados: FFmpegService, HardwareIdService, LicenseManager,
  PdfGeneratorService, ScreenshotService, TranscriptionService do UI
- Remove dependências pesadas do UI: Whisper.net, YoutubeExplode, QuestPDF
- Remove PuppeteerSharp e SkiaSharp do API (150MB Chromium não é mais necessário)
- Screenshots agora usam FFmpeg diretamente (mais simples, mais confiável)
- YouTubeService reescrito para chamar /api/video-info em vez de YoutubeExplode
- Adiciona campo UserContext em AnalysisRequest (contexto livre do usuário)
- UI traduzida para português; aba de arquivo local removida (nunca funcionou)
- YouTubeProcessor simplificado: sem modo Advanced/Whisper
- GetYtDlpPath busca yt-dlp.exe subindo até 7 níveis de diretório
- Cookies do yt-dlp configuráveis via YtDlp:CookiesBrowser no appsettings
- Chave Groq agora lida de env var GROQ_API_KEY (appsettings.json sem segredos)
- VideoStudy.Linux (Photino) adicionado à solução como host multiplataforma
- yt-dlp atualizado de 2025.01.26 para 2026.03.17 (fix do nsig extraction)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 14:08:05 -03:00

219 lines
8.0 KiB
Plaintext

@page "/"
@inject HttpClient Http
@inject YouTubeService YouTubeService
@inject PersistenceService PersistenceService
@inject NavigationManager NavigationManager
@using VideoStudy.Shared
@using System.Net.Http.Json
@using System.Text.Json
@using System.Threading
<PageTitle>VideoStudy</PageTitle>
<div class="container-fluid py-4">
<header class="text-center mb-5">
<h1 class="display-4 fw-bold" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
VideoStudy
</h1>
<p class="lead text-muted">Transforme vídeos em guias de estudo com IA</p>
</header>
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card shadow-sm border-0 rounded-4 overflow-hidden mb-4 p-4 bg-white">
<YouTubeProcessor
OnVideoUrlChanged="HandleVideoUrlChanged"
OnStart="StartAnalysis"
IsProcessing="@isProcessing"
VideoInfo="@currentVideoInfo" />
<!-- Contexto do usuário -->
<div class="mt-3">
<label class="form-label fw-bold text-muted small">
Contexto <span class="fw-normal">(opcional)</span>
</label>
<textarea class="form-control" rows="2"
placeholder="Ex: tutorial técnico de Python para iniciantes / aula escolar sobre fotossíntese / passo a passo para montar um móvel"
@bind="userContext" disabled="@isProcessing"></textarea>
</div>
<div class="row g-3 mt-1">
<div class="col-md-12">
<label class="form-label fw-bold text-muted small">Idioma de saída</label>
<select class="form-select" @bind="selectedLanguage" disabled="@isProcessing">
<option value="pt">Português (BR)</option>
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
</select>
</div>
</div>
</div>
@if (isProcessing || currentStep > 0)
{
<ProgressIndicator Status="@statusMessage" Percent="@progress" CurrentStepIndex="@currentStep" />
}
@if (logs.Count > 0)
{
<div class="card shadow-sm mb-4 border-0 rounded-4 overflow-hidden">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center"
style="cursor: pointer;" @onclick="ToggleLogs">
<small class="fw-bold">Log de execução</small>
<small>@logs.Count eventos @(showLogs ? "▼" : "▶")</small>
</div>
@if (showLogs)
{
<div class="card-body bg-light" style="max-height: 200px; overflow-y: auto; font-family: monospace; font-size: 0.8rem;">
@foreach (var log in logs)
{
<div class="text-dark border-bottom py-1">
<span class="text-muted">[@log.Timestamp.ToString("HH:mm:ss")]</span> @log.Message
</div>
}
</div>
}
</div>
}
@if (!string.IsNullOrEmpty(generatedPdfPath))
{
<PdfPreview PdfPath="@generatedPdfPath" OnCancel="@(() => generatedPdfPath = null)" />
}
</div>
</div>
</div>
@code {
private bool isProcessing = false;
private int progress = 0;
private int currentStep = 0;
private string statusMessage = "Pronto";
private bool showLogs = false;
private List<LogEntry> logs = new();
private string videoUrl = string.Empty;
private string selectedLanguage = "pt";
private string? userContext;
private VideoInfo? currentVideoInfo;
private string? generatedPdfPath;
private class LogEntry
{
public DateTime Timestamp { get; set; } = DateTime.Now;
public string Message { get; set; } = string.Empty;
}
private void ToggleLogs() => showLogs = !showLogs;
private void AddLog(string message, int? eventProgress = null)
{
logs.Insert(0, new LogEntry { Message = message });
if (logs.Count > 100) logs.RemoveAt(logs.Count - 1);
if (eventProgress.HasValue) progress = eventProgress.Value;
StateHasChanged();
}
private async Task HandleVideoUrlChanged(string url)
{
videoUrl = url;
try
{
AddLog($"Buscando informações: {url}");
currentVideoInfo = await YouTubeService.GetVideoInfoAsync(url);
if (currentVideoInfo != null) AddLog($"Encontrado: {currentVideoInfo.Title}");
}
catch (Exception ex)
{
AddLog($"Erro ao buscar vídeo: {ex.Message}");
}
}
private async Task StartAnalysis()
{
isProcessing = true;
progress = 0;
currentStep = 0;
generatedPdfPath = null;
logs.Clear();
showLogs = true;
CancellationTokenSource? cts = null;
try
{
cts = new CancellationTokenSource();
var token = cts.Token;
currentStep = 1;
statusMessage = "Iniciando análise...";
AddLog("Enviando para o servidor...", 5);
var request = new AnalysisRequest
{
VideoUrl = videoUrl,
Mode = "fast",
Language = "en",
OutputLanguage = selectedLanguage,
DurationSeconds = currentVideoInfo?.Duration.TotalSeconds ?? 0,
UserContext = string.IsNullOrWhiteSpace(userContext) ? null : userContext.Trim()
};
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "api/analyze")
{
Content = JsonContent.Create(request)
};
using var response = await Http.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, token);
response.EnsureSuccessStatusCode();
var stream = response.Content.ReadFromJsonAsAsyncEnumerable<AnalysisEvent>(
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }, token);
await foreach (var ev in stream.WithCancellation(token))
{
if (ev == null) continue;
if (ev.IsError) throw new Exception(ev.Message);
AddLog(ev.Message, ev.ProgressPercentage);
statusMessage = ev.Message;
StateHasChanged();
if (ev.Result != null)
{
AddLog("Análise concluída!", 100);
if (ev.Result.PdfData != null && ev.Result.PdfData.Length > 0)
{
AddLog("Salvando PDF na biblioteca...", 100);
var session = await PersistenceService.SaveSessionAsync(
ev.Result.PdfData,
ev.Result.DocumentTitle,
videoUrl);
generatedPdfPath = session.FilePath;
AddLog($"PDF salvo em: {session.FilePath}", 100);
await Task.Delay(2000);
NavigationManager.NavigateTo("library");
}
break;
}
}
}
catch (Exception ex)
{
AddLog($"Erro: {ex.Message}");
statusMessage = "Erro na análise";
}
finally
{
isProcessing = false;
cts?.Dispose();
}
}
}