feat: Ultima versão
This commit is contained in:
parent
7eb1f0096d
commit
9e85f28bf6
@ -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
|
||||
{
|
||||
|
||||
41
YTExtractor/Services/ChainYoutubeService.cs
Normal file
41
YTExtractor/Services/ChainYoutubeService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
83
YTExtractor/Services/Handlers/YouExposeHandler.cs
Normal file
83
YTExtractor/Services/Handlers/YouExposeHandler.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
83
YTExtractor/Services/Handlers/YoutubeExplodeHandler.cs
Normal file
83
YTExtractor/Services/Handlers/YoutubeExplodeHandler.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
25
YTExtractor/Services/Handlers/YoutubeServiceHandler.cs
Normal file
25
YTExtractor/Services/Handlers/YoutubeServiceHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
119
YTExtractor/Services/Handlers/YtDlpHandler.cs
Normal file
119
YTExtractor/Services/Handlers/YtDlpHandler.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
YTExtractor/Services/IYoutubeService.cs
Normal file
29
YTExtractor/Services/IYoutubeService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
272
YTExtractor/Services/VttFixerService.cs
Normal file
272
YTExtractor/Services/VttFixerService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
230
YTExtractor/Services/YouExpose/Client.cs
Normal file
230
YTExtractor/Services/YouExpose/Client.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
72
YTExtractor/Services/YouExposeService.cs
Normal file
72
YTExtractor/Services/YouExposeService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
130
YTExtractor/Services/YoutubeExplode/YoutubeExplodeClient.cs
Normal file
130
YTExtractor/Services/YoutubeExplode/YoutubeExplodeClient.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -19,6 +19,9 @@
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
//"MongoDbConnection": "mongodb://192.168.0.99:27017"
|
||||
"MongoDbConnection": "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/"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user