Compare commits
No commits in common. "9e85f28bf627c5a7f58dd8736f306c8c4cd6f1db" and "5ec4eaf7b5e9a96ebbbb92a428b34223a7d8c66a" have entirely different histories.
9e85f28bf6
...
5ec4eaf7b5
@ -3,7 +3,6 @@ using Serilog;
|
|||||||
using YTExtractor.Data;
|
using YTExtractor.Data;
|
||||||
using YTExtractor.Logging.Configuration;
|
using YTExtractor.Logging.Configuration;
|
||||||
using YTExtractor.Services;
|
using YTExtractor.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
// App configuration and endpoints
|
// App configuration and endpoints
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@ -19,30 +18,16 @@ builder.Services.AddEndpointsApiExplorer();
|
|||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
builder.Services.AddSingleton<MongoDBConnector>();
|
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();
|
var app = builder.Build();
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI();
|
||||||
|
|
||||||
|
|
||||||
app.MapPost("/api/video-info", async (VideoRequest request, MongoDBConnector mongo, IYoutubeService youtubeService) =>
|
app.MapPost("/api/video-info", async (VideoRequest request, MongoDBConnector mongo) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!youtubeService.IsValidYouTubeUrl(request.Url))
|
if (!YoutubeService.IsValidYouTubeUrl(request.Url))
|
||||||
return Results.BadRequest("Invalid YouTube URL");
|
return Results.BadRequest("Invalid YouTube URL");
|
||||||
|
|
||||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||||
@ -63,8 +48,8 @@ app.MapPost("/api/video-info", async (VideoRequest request, MongoDBConnector mon
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
var info = await youtubeService.GetVideoInfo(request.Url, tempDir);
|
var info = await YoutubeService.GetVideoInfo(request.Url, tempDir);
|
||||||
var subtitles = service.ExtractPlainText(await youtubeService.GetSubtitles(request.Url, request.Language, tempDir));
|
var subtitles = service.ExtractPlainText(await YoutubeService.GetSubtitles(request.Url, request.Language, tempDir));
|
||||||
|
|
||||||
await mongo.InsertVideo(new VideoData
|
await mongo.InsertVideo(new VideoData
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,119 +0,0 @@
|
|||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,272 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,230 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
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}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,10 +6,6 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Remove="Services\Handlers\YouExposeHandler.cs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.69.0.3707" />
|
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.69.0.3707" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.12" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.12" />
|
||||||
@ -24,7 +20,6 @@
|
|||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
<PackageReference Include="YoutubeExplode" Version="6.3.12" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -4,18 +4,17 @@ using System.Reflection;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using YTExtractor.Services;
|
|
||||||
|
|
||||||
namespace YTExtractor
|
namespace YTExtractor
|
||||||
{
|
{
|
||||||
public class YoutubeService : IYoutubeService
|
public class YoutubeService
|
||||||
{
|
{
|
||||||
public bool IsValidYouTubeUrl(string urlx)
|
public static bool IsValidYouTubeUrl(string urlx)
|
||||||
{
|
{
|
||||||
return Regex.IsMatch(urlx, @"^(https?\:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$");
|
return Regex.IsMatch(urlx, @"^(https?\:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<YtDlpInfo> GetVideoInfo(string url, string workingDir)
|
public static async Task<YtDlpInfo> GetVideoInfo(string url, string workingDir)
|
||||||
{
|
{
|
||||||
var startInfo = new ProcessStartInfo
|
var startInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
@ -43,7 +42,7 @@ namespace YTExtractor
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GetSubtitles(string url, string language, string workingDir)
|
public static async Task<string> GetSubtitles(string url, string language, string workingDir)
|
||||||
{
|
{
|
||||||
var pathExe = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
var pathExe = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||||
var exePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
var exePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||||
|
|||||||
@ -19,9 +19,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"MongoDbConnection": "mongodb://admin:c4rn31r0@192.168.0.82:27017,192.168.0.81:27017/?replicaSet=rs0",
|
"MongoDbConnection": "mongodb://localhost:27017"
|
||||||
"YouExpose": {
|
//"MongoDbConnaction": "mongodb://admin:c4rn31r0@192.168.0.82:27017,192.168.0.81:27017/?replicaSet=rs0"
|
||||||
"ApiKey": "sua-chave-api-aqui",
|
}
|
||||||
"ApiUrl": "https://api.youexpose.com/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user