Compare commits

..

2 Commits

Author SHA1 Message Date
Ricardo Carneiro
9e85f28bf6 feat: Ultima versão 2025-05-05 19:50:24 -03:00
Ricardo Carneiro
7eb1f0096d fix: mongodb server 2025-04-28 09:39:37 -03:00
14 changed files with 1119 additions and 11 deletions

View File

@ -3,6 +3,7 @@ using Serilog;
using YTExtractor.Data;
using YTExtractor.Logging.Configuration;
using YTExtractor.Services;
using Microsoft.Extensions.DependencyInjection;
// App configuration and endpoints
var builder = WebApplication.CreateBuilder(args);
@ -18,16 +19,30 @@ builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<MongoDBConnector>();
// Register VTT processing service
builder.Services.AddSingleton<VttFixerService>();
// Register YoutubeExplode client
builder.Services.AddSingleton<YTExtractor.Services.YoutubeExplode.YoutubeExplodeClient>();
// Register YouTube service handlers and service
builder.Services.AddSingleton<YoutubeService>();
builder.Services.AddSingleton<YTExtractor.Services.Handlers.YoutubeExplodeHandler>();
builder.Services.AddSingleton<YTExtractor.Services.Handlers.YtDlpHandler>();
// Register Chain of Responsibility implementation
builder.Services.AddSingleton<IYoutubeService, ChainYoutubeService>();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.MapPost("/api/video-info", async (VideoRequest request, MongoDBConnector mongo) =>
app.MapPost("/api/video-info", async (VideoRequest request, MongoDBConnector mongo, IYoutubeService youtubeService) =>
{
try
{
if (!YoutubeService.IsValidYouTubeUrl(request.Url))
if (!youtubeService.IsValidYouTubeUrl(request.Url))
return Results.BadRequest("Invalid YouTube URL");
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
@ -48,8 +63,8 @@ app.MapPost("/api/video-info", async (VideoRequest request, MongoDBConnector mon
));
}
var info = await YoutubeService.GetVideoInfo(request.Url, tempDir);
var subtitles = service.ExtractPlainText(await YoutubeService.GetSubtitles(request.Url, request.Language, tempDir));
var info = await youtubeService.GetVideoInfo(request.Url, tempDir);
var subtitles = service.ExtractPlainText(await youtubeService.GetSubtitles(request.Url, request.Language, tempDir));
await mongo.InsertVideo(new VideoData
{

View File

@ -0,0 +1,41 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using YTExtractor.Services.Handlers;
namespace YTExtractor.Services
{
public class ChainYoutubeService : IYoutubeService
{
private readonly ILogger<ChainYoutubeService> _logger;
private readonly YoutubeServiceHandler _firstHandler;
public ChainYoutubeService(
ILogger<ChainYoutubeService> logger,
YoutubeExplodeHandler youtubeExplodeHandler,
YtDlpHandler ytDlpHandler)
{
_logger = logger;
// Set up the chain of responsibility
_firstHandler = youtubeExplodeHandler;
youtubeExplodeHandler.SetNext(ytDlpHandler);
}
public bool IsValidYouTubeUrl(string url)
{
return Regex.IsMatch(url, @"^(https?\:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$");
}
public async Task<YtDlpInfo> GetVideoInfo(string url, string workingDir)
{
_logger.LogInformation("Starting chain of responsibility for video info: {Url}", url);
return await _firstHandler.HandleVideoInfo(url, workingDir);
}
public async Task<string> GetSubtitles(string url, string language, string workingDir)
{
_logger.LogInformation("Starting chain of responsibility for subtitles: {Url}, language: {Language}", url, language);
return await _firstHandler.HandleSubtitles(url, language, workingDir);
}
}
}

View File

@ -0,0 +1,83 @@
using Microsoft.Extensions.Logging;
using YTExtractor.Services.YoutubeExplode;
namespace YTExtractor.Services.Handlers
{
public class YoutubeExplodeHandler : YoutubeServiceHandler
{
private readonly YoutubeExplodeClient _youtubeExplodeClient;
private readonly VttFixerService _vttFixerService;
public YoutubeExplodeHandler(
ILogger<YoutubeExplodeHandler> logger,
YoutubeExplodeClient youtubeExplodeClient,
VttFixerService vttFixerService) : base(logger)
{
_youtubeExplodeClient = youtubeExplodeClient;
_vttFixerService = vttFixerService;
}
public override async Task<YtDlpInfo> HandleVideoInfo(string url, string workingDir)
{
try
{
_logger.LogInformation("Getting video info using YoutubeExplode for {Url}", url);
var videoInfo = await _youtubeExplodeClient.GetVideoInfoAsync(url);
if (videoInfo != null && !string.IsNullOrEmpty(videoInfo.Title))
{
_logger.LogInformation("Successfully retrieved video info using YoutubeExplode for {Url}", url);
return videoInfo;
}
_logger.LogInformation("No video info found with YoutubeExplode, passing to next handler for {Url}", url);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error getting video info with YoutubeExplode, passing to next handler for {Url}", url);
}
// Pass to the next handler if YoutubeExplode fails or returns no data
if (_nextHandler != null)
{
return await _nextHandler.HandleVideoInfo(url, workingDir);
}
throw new Exception("Failed to get video info. No more handlers available.");
}
public override async Task<string> HandleSubtitles(string url, string language, string workingDir)
{
try
{
_logger.LogInformation("Getting subtitles using YoutubeExplode for {Url} in language {Language}", url, language);
var subtitles = await _youtubeExplodeClient.GetSubtitlesAsync(url, language);
if (!string.IsNullOrEmpty(subtitles))
{
_logger.LogInformation("Successfully retrieved subtitles using YoutubeExplode for {Url}", url);
// Fix the subtitles with VttFixer
var fixedSubtitles = _vttFixerService.FixYoutubeVtt(subtitles);
return fixedSubtitles;
}
_logger.LogInformation("No subtitles found with YoutubeExplode, passing to next handler for {Url}", url);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error getting subtitles with YoutubeExplode, passing to next handler for {Url}", url);
}
// Pass to the next handler if YoutubeExplode fails or returns no data
if (_nextHandler != null)
{
return await _nextHandler.HandleSubtitles(url, language, workingDir);
}
throw new Exception("Failed to get subtitles. No more handlers available.");
}
}
}

View File

@ -0,0 +1,83 @@
using Microsoft.Extensions.Logging;
using YTExtractor.Services.YoutubeExplode;
namespace YTExtractor.Services.Handlers
{
public class YoutubeExplodeHandler : YoutubeServiceHandler
{
private readonly YoutubeExplodeClient _youtubeExplodeClient;
private readonly VttFixerService _vttFixerService;
public YoutubeExplodeHandler(
ILogger<YoutubeExplodeHandler> logger,
YoutubeExplodeClient youtubeExplodeClient,
VttFixerService vttFixerService) : base(logger)
{
_youtubeExplodeClient = youtubeExplodeClient;
_vttFixerService = vttFixerService;
}
public override async Task<YtDlpInfo> HandleVideoInfo(string url, string workingDir)
{
try
{
_logger.LogInformation("Getting video info using YoutubeExplode for {Url}", url);
var videoInfo = await _youtubeExplodeClient.GetVideoInfoAsync(url);
if (videoInfo != null && !string.IsNullOrEmpty(videoInfo.Title))
{
_logger.LogInformation("Successfully retrieved video info using YoutubeExplode for {Url}", url);
return videoInfo;
}
_logger.LogInformation("No video info found with YoutubeExplode, passing to next handler for {Url}", url);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error getting video info with YoutubeExplode, passing to next handler for {Url}", url);
}
// Pass to the next handler if YoutubeExplode fails or returns no data
if (_nextHandler != null)
{
return await _nextHandler.HandleVideoInfo(url, workingDir);
}
throw new Exception("Failed to get video info. No more handlers available.");
}
public override async Task<string> HandleSubtitles(string url, string language, string workingDir)
{
try
{
_logger.LogInformation("Getting subtitles using YoutubeExplode for {Url} in language {Language}", url, language);
var subtitles = await _youtubeExplodeClient.GetSubtitlesAsync(url, language);
if (!string.IsNullOrEmpty(subtitles))
{
_logger.LogInformation("Successfully retrieved subtitles using YoutubeExplode for {Url}", url);
// Fix the subtitles with VttFixer
var fixedSubtitles = _vttFixerService.FixYoutubeVtt(subtitles);
return fixedSubtitles;
}
_logger.LogInformation("No subtitles found with YoutubeExplode, passing to next handler for {Url}", url);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error getting subtitles with YoutubeExplode, passing to next handler for {Url}", url);
}
// Pass to the next handler if YoutubeExplode fails or returns no data
if (_nextHandler != null)
{
return await _nextHandler.HandleSubtitles(url, language, workingDir);
}
throw new Exception("Failed to get subtitles. No more handlers available.");
}
}
}

View File

@ -0,0 +1,25 @@
using Microsoft.Extensions.Logging;
namespace YTExtractor.Services.Handlers
{
public abstract class YoutubeServiceHandler
{
protected readonly ILogger _logger;
protected YoutubeServiceHandler? _nextHandler;
protected YoutubeServiceHandler(ILogger logger)
{
_logger = logger;
}
public YoutubeServiceHandler SetNext(YoutubeServiceHandler handler)
{
_nextHandler = handler;
return handler;
}
public abstract Task<YtDlpInfo> HandleVideoInfo(string url, string workingDir);
public abstract Task<string> HandleSubtitles(string url, string language, string workingDir);
}
}

View File

@ -0,0 +1,119 @@
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
namespace YTExtractor.Services.Handlers
{
public class YtDlpHandler : YoutubeServiceHandler
{
private readonly VttFixerService _vttFixerService;
public YtDlpHandler(ILogger<YtDlpHandler> logger, VttFixerService vttFixerService) : base(logger)
{
_vttFixerService = vttFixerService;
}
public override async Task<YtDlpInfo> HandleVideoInfo(string url, string workingDir)
{
try
{
_logger.LogInformation("Getting video info using yt-dlp for {Url}", url);
var startInfo = new ProcessStartInfo
{
FileName = "yt-dlp",
Arguments = $"--dump-json {url}",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = workingDir
};
using var process = Process.Start(startInfo);
var output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
if (process.ExitCode != 0)
throw new Exception("Failed to get video info using yt-dlp");
var jsonDoc = JsonDocument.Parse(output);
var root = jsonDoc.RootElement;
return new YtDlpInfo(
root.GetProperty("title").GetString() ?? "",
root.GetProperty("thumbnail").GetString() ?? ""
);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting video info with yt-dlp for {Url}", url);
// Pass to the next handler if yt-dlp fails
if (_nextHandler != null)
{
return await _nextHandler.HandleVideoInfo(url, workingDir);
}
throw new Exception("Failed to get video info. No more handlers available.");
}
}
public override async Task<string> HandleSubtitles(string url, string language, string workingDir)
{
try
{
_logger.LogInformation("Getting subtitles using yt-dlp for {Url} in language {Language}", url, language);
var pathExe = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var exePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Path.Combine(pathExe, "yt-dlp.exe")
: "yt-dlp";
var startInfo = new ProcessStartInfo
{
FileName = exePath,
Arguments = $"--write-sub --write-auto-sub --sub-lang {language} --skip-download {url}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = workingDir
};
using var process = Process.Start(startInfo);
var output = await process.StandardOutput.ReadToEndAsync();
var error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
var subtitleFile = Directory.GetFiles(workingDir, "*.vtt").FirstOrDefault();
if (subtitleFile == null)
throw new Exception("No subtitles found using yt-dlp");
// Read the VTT file
var vttContent = await File.ReadAllTextAsync(subtitleFile);
// Fix the VTT content
_logger.LogInformation("Fixing VTT subtitles for {Url}", url);
var serviceSrt = new ConvertTranscriptService();
var srt = serviceSrt.ConvertToSrt(vttContent);
var fixedContent = _vttFixerService.FixYoutubeVtt(vttContent);
return fixedContent;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting subtitles with yt-dlp for {Url}", url);
// Pass to the next handler if yt-dlp fails
if (_nextHandler != null)
{
return await _nextHandler.HandleSubtitles(url, language, workingDir);
}
throw new Exception("Failed to get subtitles. No more handlers available.");
}
}
}
}

View File

@ -0,0 +1,29 @@
namespace YTExtractor.Services
{
public interface IYoutubeService
{
/// <summary>
/// Validates if the provided URL is a valid YouTube URL
/// </summary>
/// <param name="url">The URL to validate</param>
/// <returns>True if the URL is a valid YouTube URL, otherwise false</returns>
bool IsValidYouTubeUrl(string url);
/// <summary>
/// Gets video information from a YouTube URL
/// </summary>
/// <param name="url">The YouTube video URL</param>
/// <param name="workingDir">The working directory for temporary files</param>
/// <returns>Basic information about the video</returns>
Task<YtDlpInfo> GetVideoInfo(string url, string workingDir);
/// <summary>
/// Gets subtitles for a YouTube video
/// </summary>
/// <param name="url">The YouTube video URL</param>
/// <param name="language">The language code for subtitles</param>
/// <param name="workingDir">The working directory for temporary files</param>
/// <returns>The subtitles content as a string</returns>
Task<string> GetSubtitles(string url, string language, string workingDir);
}
}

View File

@ -0,0 +1,272 @@
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
namespace YTExtractor.Services
{
public class VttFixerService
{
private readonly ILogger<VttFixerService> _logger;
public VttFixerService(ILogger<VttFixerService> logger)
{
_logger = logger;
}
/// <summary>
/// Fixes YouTube's autogenerated VTT subtitles with HTML tags and returns an SRT-formatted string
/// </summary>
/// <param name="vttContent">The VTT content as a string</param>
/// <returns>The fixed subtitles in SRT format</returns>
public string FixYoutubeVtt(string vttContent)
{
try
{
_logger.LogInformation("Starting to fix YouTube VTT content with HTML tags");
// Define regex patterns to extract timing and text
var timeRegex = new Regex(@"(\d{2}:\d{2}:\d{2}\.\d{3}) --> (\d{2}:\d{2}:\d{2}\.\d{3})");
var htmlTagRegex = new Regex(@"<[^>]+>");
// Split the content by WEBVTT header and timestamps
var lines = vttContent.Split('\n');
var resultBuilder = new StringBuilder();
List<Caption> captions = new List<Caption>();
Caption currentCaption = null;
bool isHeader = true;
StringBuilder textBuilder = new StringBuilder();
// First pass: Parse the VTT file and extract captions
foreach (var line in lines)
{
// Skip header lines
if (isHeader)
{
if (line.Trim().StartsWith("00:"))
isHeader = false;
else
continue;
}
// Check if this is a timing line
var timeMatch = timeRegex.Match(line);
if (timeMatch.Success)
{
// If we already have a caption in progress, save it
if (currentCaption != null && textBuilder.Length > 0)
{
currentCaption.Text = textBuilder.ToString().Trim();
captions.Add(currentCaption);
textBuilder.Clear();
}
// Create a new caption
currentCaption = new Caption
{
Start = timeMatch.Groups[1].Value.Replace(".", ","),
End = timeMatch.Groups[2].Value.Replace(".", ",")
};
}
// Otherwise, this is caption text
else if (currentCaption != null && !string.IsNullOrWhiteSpace(line))
{
// Remove HTML tags and add the cleaned text
string cleanText = htmlTagRegex.Replace(line, "");
if (!string.IsNullOrWhiteSpace(cleanText))
{
textBuilder.AppendLine(cleanText);
}
}
}
// Add the last caption if there is one
if (currentCaption != null && textBuilder.Length > 0)
{
currentCaption.Text = textBuilder.ToString().Trim();
captions.Add(currentCaption);
}
// Second pass: Process captions to remove duplicates
Caption previousCaption = null;
int counter = 1;
foreach (var caption in captions)
{
// Clean the text
string cleanText = CleanText(caption.Text);
if (previousCaption != null)
{
string prevCleanText = CleanText(previousCaption.Text);
// Check if this caption is a duplicate of the previous one
if (prevCleanText.Equals(cleanText, StringComparison.OrdinalIgnoreCase))
{
// Update the end time of the previous caption and skip this one
previousCaption.End = caption.End;
continue;
}
// Check if the previous caption text appears at the start of this caption
if (cleanText.StartsWith(prevCleanText, StringComparison.OrdinalIgnoreCase))
{
// Extract only the new part
string newText = cleanText.Substring(prevCleanText.Length).Trim();
if (!string.IsNullOrWhiteSpace(newText))
{
caption.Text = newText;
}
else
{
// If there's no new text, skip this caption
continue;
}
}
// Write the previous caption
resultBuilder.AppendLine(counter.ToString());
resultBuilder.AppendLine($"{previousCaption.Start} --> {previousCaption.End}");
resultBuilder.AppendLine(prevCleanText);
resultBuilder.AppendLine();
counter++;
}
previousCaption = new Caption
{
Start = caption.Start,
End = caption.End,
Text = caption.Text
};
}
// Write the last caption
if (previousCaption != null)
{
string prevCleanText = CleanText(previousCaption.Text);
resultBuilder.AppendLine(counter.ToString());
resultBuilder.AppendLine($"{previousCaption.Start} --> {previousCaption.End}");
resultBuilder.AppendLine(prevCleanText);
resultBuilder.AppendLine();
}
_logger.LogInformation("Successfully fixed YouTube VTT content with HTML tags");
return resultBuilder.ToString();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fixing YouTube VTT content with HTML tags");
throw;
}
}
/// <summary>
/// Cleans VTT text by removing HTML tags, position markers, and other formatting
/// </summary>
private string CleanText(string text)
{
if (string.IsNullOrEmpty(text))
return string.Empty;
// Remove HTML tags
text = Regex.Replace(text, @"<[^>]+>", "");
// Remove position markers like "align:start position:0%"
text = Regex.Replace(text, @"align:[a-z]+ position:\d+%", "");
// Remove timestamps in format <00:00:00.000>
text = Regex.Replace(text, @"<\d{2}:\d{2}:\d{2}\.\d{3}>", "");
// Clean up whitespace
text = Regex.Replace(text, @"\s+", " ").Trim();
return text;
}
private string LimpaTexto(string texto)
{
var textoLimpo = Regex.Replace(texto, @"<\d{2}:\d{2}:\d{2}\.\d{3}>", "");
textoLimpo = Regex.Replace(textoLimpo, @"<\/?c>", "");
return textoLimpo;
}
/// <summary>
/// Fixes YouTube's autogenerated VTT subtitles file and returns an SRT-formatted string
/// </summary>
/// <param name="vttFilePath">Path to the VTT file</param>
/// <returns>The fixed subtitles in SRT format</returns>
public string FixYoutubeVttFile(string vttFilePath)
{
_logger.LogInformation("Reading VTT file: {VttFilePath}", vttFilePath);
var vttContent = File.ReadAllText(vttFilePath);
return FixYoutubeVtt(vttContent);
}
private List<Caption> ParseVtt(string vttContent)
{
var captions = new List<Caption>();
// Skip the WEBVTT header
var lines = vttContent.Split('\n')
.Skip(1) // Skip WEBVTT line
.Select(l => l.Trim())
.Where(l => !string.IsNullOrWhiteSpace(l))
.ToList();
// Pattern to match timestamp lines like "00:00:00.000 --> 00:00:05.000"
var timestampPattern = new Regex(@"(\d{2}:\d{2}:\d{2}\.\d{3})\s-->\s(\d{2}:\d{2}:\d{2}\.\d{3})");
Caption? currentCaption = null;
StringBuilder textBuilder = new StringBuilder();
foreach (var line in lines)
{
var match = timestampPattern.Match(line);
if (match.Success)
{
// If we were building a caption, add it to the list
if (currentCaption != null && textBuilder.Length > 0)
{
currentCaption.Text = textBuilder.ToString().Trim();
captions.Add(currentCaption);
textBuilder.Clear();
}
// Start a new caption
currentCaption = new Caption
{
Start = match.Groups[1].Value,
End = match.Groups[2].Value
};
}
else if (currentCaption != null && !line.Contains("-->") && !string.IsNullOrWhiteSpace(line))
{
// Add text to the current caption
if (textBuilder.Length > 0)
{
textBuilder.AppendLine();
}
textBuilder.Append(line);
}
}
// Add the last caption if there is one
if (currentCaption != null && textBuilder.Length > 0)
{
currentCaption.Text = textBuilder.ToString().Trim();
captions.Add(currentCaption);
}
return captions;
}
private class Caption
{
public string Start { get; set; } = string.Empty;
public string End { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
}
}
}

View File

@ -0,0 +1,230 @@
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
namespace YTExtractor.Services.YouExpose
{
/// <summary>
/// Cliente para a API YouExpose - biblioteca para acessar dados do YouTube
/// </summary>
public class Client
{
private readonly ILogger<Client> _logger;
private readonly HttpClient _httpClient;
private readonly string _apiKey;
private readonly JsonSerializerOptions _jsonOptions;
/// <summary>
/// Construtor do cliente YouExpose
/// </summary>
/// <param name="logger">Logger para registro de eventos</param>
/// <param name="httpClient">Cliente HTTP opcional (útil para testes)</param>
/// <param name="apiKey">Chave de API do YouExpose (opcional)</param>
public Client(ILogger<Client> logger, HttpClient? httpClient = null, string? apiKey = null)
{
_logger = logger;
_httpClient = httpClient ?? new HttpClient();
_apiKey = apiKey ?? Environment.GetEnvironmentVariable("YOUEXPOSE_API_KEY") ?? "";
// Configurar base URL se necessário
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("YOUEXPOSE_API_URL")))
{
_httpClient.BaseAddress = new Uri(Environment.GetEnvironmentVariable("YOUEXPOSE_API_URL"));
}
else
{
_httpClient.BaseAddress = new Uri("https://api.youexpose.com/");
}
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
}
/// <summary>
/// Obtém informações de um vídeo do YouTube
/// </summary>
/// <param name="url">URL do vídeo do YouTube</param>
/// <returns>Informações básicas do vídeo</returns>
public async Task<VideoInfo?> GetVideoInfoAsync(string url)
{
try
{
_logger.LogInformation("Obtendo informações do vídeo via YouExpose: {Url}", url);
// Extrair ID do vídeo da URL
var videoId = ExtractVideoId(url);
if (string.IsNullOrEmpty(videoId))
{
_logger.LogWarning("ID do vídeo não pôde ser extraído da URL: {Url}", url);
return null;
}
// Fazer requisição à API
var response = await _httpClient.GetAsync($"api/videos/{videoId}?key={_apiKey}");
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("Falha ao obter informações do vídeo: {StatusCode}", response.StatusCode);
return null;
}
// Deserializar resposta
var result = await response.Content.ReadFromJsonAsync<ApiResponse<VideoInfo>>(_jsonOptions);
return result?.Data;
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao obter informações do vídeo: {Url}", url);
return null;
}
}
/// <summary>
/// Obtém legendas de um vídeo do YouTube
/// </summary>
/// <param name="url">URL do vídeo do YouTube</param>
/// <param name="language">Código do idioma (ex: "pt", "en")</param>
/// <returns>Conteúdo das legendas</returns>
public async Task<string?> GetSubtitlesAsync(string url, string language)
{
try
{
_logger.LogInformation("Obtendo legendas via YouExpose: {Url}, Idioma: {Language}", url, language);
// Extrair ID do vídeo da URL
var videoId = ExtractVideoId(url);
if (string.IsNullOrEmpty(videoId))
{
_logger.LogWarning("ID do vídeo não pôde ser extraído da URL: {Url}", url);
return null;
}
// Fazer requisição à API
var response = await _httpClient.GetAsync($"api/videos/{videoId}/subtitles?lang={language}&key={_apiKey}");
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("Falha ao obter legendas: {StatusCode}", response.StatusCode);
return null;
}
// Obter conteúdo das legendas
var result = await response.Content.ReadFromJsonAsync<ApiResponse<SubtitleResponse>>(_jsonOptions);
return result?.Data?.Content;
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao obter legendas: {Url}, Idioma: {Language}", url, language);
return null;
}
}
/// <summary>
/// Extrai o ID do vídeo de uma URL do YouTube
/// </summary>
/// <param name="url">URL do YouTube</param>
/// <returns>ID do vídeo ou null se não encontrado</returns>
private string? ExtractVideoId(string url)
{
try
{
// Verificar se é uma URL válida
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
return null;
}
// Para URLs como: https://www.youtube.com/watch?v=VIDEO_ID
if (uri.Host.Contains("youtube.com") && uri.AbsolutePath.Contains("watch"))
{
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
return query["v"];
}
// Para URLs como: https://youtu.be/VIDEO_ID
if (uri.Host.Contains("youtu.be"))
{
return uri.AbsolutePath.TrimStart('/');
}
return null;
}
catch
{
return null;
}
}
}
/// <summary>
/// Informações básicas de um vídeo
/// </summary>
public class VideoInfo
{
/// <summary>
/// Título do vídeo
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// URL da miniatura do vídeo
/// </summary>
public string ThumbnailUrl { get; set; } = string.Empty;
/// <summary>
/// Duração do vídeo em segundos
/// </summary>
public int Duration { get; set; }
/// <summary>
/// Nome do canal
/// </summary>
public string ChannelName { get; set; } = string.Empty;
}
/// <summary>
/// Resposta de legendas
/// </summary>
public class SubtitleResponse
{
/// <summary>
/// Idioma das legendas
/// </summary>
public string Language { get; set; } = string.Empty;
/// <summary>
/// Conteúdo das legendas
/// </summary>
public string Content { get; set; } = string.Empty;
/// <summary>
/// Indica se são legendas geradas automaticamente
/// </summary>
public bool IsAutoGenerated { get; set; }
}
/// <summary>
/// Estrutura genérica para resposta da API
/// </summary>
/// <typeparam name="T">Tipo de dados na resposta</typeparam>
public class ApiResponse<T>
{
/// <summary>
/// Status da requisição
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Mensagem (em caso de erro)
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// Dados retornados
/// </summary>
public T? Data { get; set; }
}
}

View File

@ -0,0 +1,72 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using YTExtractor.Services;
namespace YTExtractor.Services
{
public class YouExposeService : IYoutubeService
{
private readonly ILogger<YouExposeService> _logger;
private readonly YoutubeService _fallbackService;
public YouExposeService(ILogger<YouExposeService> logger, YoutubeService fallbackService)
{
_logger = logger;
_fallbackService = fallbackService;
}
public bool IsValidYouTubeUrl(string url)
{
return Regex.IsMatch(url, @"^(https?\:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$");
}
public async Task<YtDlpInfo> GetVideoInfo(string url, string workingDir)
{
try
{
_logger.LogInformation("Attempting to get video info using YouExpose for {Url}", url);
// TODO: Implement YouExpose video info retrieval
// This is where you would use the YouExpose library to get video information
// Example code (to be replaced with actual implementation):
// var youExposeClient = new YouExposeClient();
// var videoInfo = await youExposeClient.GetVideoInfoAsync(url);
// return new YtDlpInfo(videoInfo.Title, videoInfo.ThumbnailUrl);
// For now, fall back to yt-dlp
_logger.LogInformation("YouExpose implementation not available, falling back to yt-dlp for {Url}", url);
return await _fallbackService.GetVideoInfo(url, workingDir);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error getting video info with YouExpose, falling back to yt-dlp for {Url}", url);
return await _fallbackService.GetVideoInfo(url, workingDir);
}
}
public async Task<string> GetSubtitles(string url, string language, string workingDir)
{
try
{
_logger.LogInformation("Attempting to get subtitles using YouExpose for {Url} in language {Language}", url, language);
// TODO: Implement YouExpose subtitles retrieval
// This is where you would use the YouExpose library to get subtitles
// Example code (to be replaced with actual implementation):
// var youExposeClient = new YouExposeClient();
// var subtitles = await youExposeClient.GetSubtitlesAsync(url, language);
// if (!string.IsNullOrEmpty(subtitles))
// return subtitles;
// For now, fall back to yt-dlp
_logger.LogInformation("YouExpose implementation not available, falling back to yt-dlp for {Url}", url);
return await _fallbackService.GetSubtitles(url, language, workingDir);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error getting subtitles with YouExpose, falling back to yt-dlp for {Url}", url);
return await _fallbackService.GetSubtitles(url, language, workingDir);
}
}
}
}

View File

@ -0,0 +1,130 @@
using Microsoft.Extensions.Logging;
using System.Text;
using YoutubeExplode;
using YoutubeExplode.Videos;
using YoutubeExplode.Videos.ClosedCaptions;
namespace YTExtractor.Services.YoutubeExplode
{
/// <summary>
/// Client using the YoutubeExplode library to extract data from YouTube
/// </summary>
public class YoutubeExplodeClient
{
private readonly ILogger<YoutubeExplodeClient> _logger;
private readonly YoutubeClient _youtube;
public YoutubeExplodeClient(ILogger<YoutubeExplodeClient> logger)
{
_logger = logger;
_youtube = new YoutubeClient();
}
/// <summary>
/// Gets video information from a YouTube URL
/// </summary>
/// <param name="url">The YouTube video URL</param>
/// <returns>Video information or null if it fails</returns>
public async Task<YtDlpInfo?> GetVideoInfoAsync(string url)
{
try
{
_logger.LogInformation("Getting video info using YoutubeExplode for {Url}", url);
// Get the video ID from the URL
var videoId = VideoId.TryParse(url);
if (videoId == null)
{
_logger.LogWarning("Invalid YouTube URL: {Url}", url);
return null;
}
// Get the video metadata
var video = await _youtube.Videos.GetAsync(videoId.Value);
return new YtDlpInfo(
video.Title,
video.Thumbnails.LastOrDefault()?.Url ?? ""
);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting video info with YoutubeExplode for {Url}", url);
return null;
}
}
/// <summary>
/// Gets subtitles for a YouTube video
/// </summary>
/// <param name="url">The YouTube video URL</param>
/// <param name="language">The language code (e.g., "en", "pt")</param>
/// <returns>The subtitle content or null if it fails</returns>
public async Task<string?> GetSubtitlesAsync(string url, string language)
{
try
{
_logger.LogInformation("Getting subtitles using YoutubeExplode for {Url} in language {Language}", url, language);
// Get the video ID from the URL
var videoId = VideoId.TryParse(url);
if (videoId == null)
{
_logger.LogWarning("Invalid YouTube URL: {Url}", url);
return null;
}
// Get the closed caption tracks available for the video
var trackManifest = await _youtube.Videos.ClosedCaptions.GetManifestAsync(videoId.Value);
// Find the track for the requested language
var trackInfo = trackManifest.GetByLanguage(language);
if (trackInfo == null)
{
// If not found, try to find an auto-generated track for the language
trackInfo = trackManifest.Tracks
.FirstOrDefault(t => t.Language.Code == language && t.IsAutoGenerated);
if (trackInfo == null)
{
_logger.LogWarning("No subtitles found for language {Language} for video {Url}", language, url);
return null;
}
}
// Get the actual captions
var track = await _youtube.Videos.ClosedCaptions.GetAsync(trackInfo);
// Convert the captions to WebVTT format
var vttBuilder = new StringBuilder();
vttBuilder.AppendLine("WEBVTT");
vttBuilder.AppendLine();
foreach (var caption in track.Captions)
{
var startTime = FormatTime(caption.Offset);
var endTime = FormatTime(caption.Offset + caption.Duration);
vttBuilder.AppendLine($"{startTime} --> {endTime}");
vttBuilder.AppendLine(caption.Text);
vttBuilder.AppendLine();
}
return vttBuilder.ToString();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting subtitles with YoutubeExplode for {Url} in language {Language}", url, language);
return null;
}
}
/// <summary>
/// Format TimeSpan as WebVTT time format (HH:MM:SS.mmm)
/// </summary>
private string FormatTime(TimeSpan time)
{
return $"{time.Hours:D2}:{time.Minutes:D2}:{time.Seconds:D2}.{time.Milliseconds:D3}";
}
}
}

View File

@ -6,6 +6,10 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Services\Handlers\YouExposeHandler.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.69.0.3707" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.12" />
@ -20,6 +24,7 @@
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="YoutubeExplode" Version="6.3.12" />
</ItemGroup>
<ItemGroup>

View File

@ -4,17 +4,18 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;
using YTExtractor.Services;
namespace YTExtractor
{
public class YoutubeService
public class YoutubeService : IYoutubeService
{
public static bool IsValidYouTubeUrl(string urlx)
public bool IsValidYouTubeUrl(string urlx)
{
return Regex.IsMatch(urlx, @"^(https?\:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$");
}
public static async Task<YtDlpInfo> GetVideoInfo(string url, string workingDir)
public async Task<YtDlpInfo> GetVideoInfo(string url, string workingDir)
{
var startInfo = new ProcessStartInfo
{
@ -42,7 +43,7 @@ namespace YTExtractor
);
}
public static async Task<string> GetSubtitles(string url, string language, string workingDir)
public async Task<string> GetSubtitles(string url, string language, string workingDir)
{
var pathExe = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var exePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)

View File

@ -19,6 +19,9 @@
}
},
"AllowedHosts": "*",
"MongoDbConnection": "mongodb://localhost:27017"
//"MongoDbConnaction": "mongodb://admin:c4rn31r0@192.168.0.82:27017,192.168.0.81:27017/?replicaSet=rs0"
"MongoDbConnection": "mongodb://admin:c4rn31r0@192.168.0.82:27017,192.168.0.81:27017/?replicaSet=rs0",
"YouExpose": {
"ApiKey": "sua-chave-api-aqui",
"ApiUrl": "https://api.youexpose.com/"
}
}