commit ea0d290deb4ee49c3db6ff95f001309a9bd41173 Author: Ricardo Carneiro Date: Sun Jan 26 18:20:38 2025 -0300 Primeiro commit do projeto VS 2022 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b82ccd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vs/ +bin/ +obj/ diff --git a/YTExtractor.sln b/YTExtractor.sln new file mode 100644 index 0000000..e43e952 --- /dev/null +++ b/YTExtractor.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35327.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YTExtractor", "YTExtractor\YTExtractor.csproj", "{7DA7D783-153F-42EF-87E4-239DEC80F91A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7DA7D783-153F-42EF-87E4-239DEC80F91A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DA7D783-153F-42EF-87E4-239DEC80F91A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DA7D783-153F-42EF-87E4-239DEC80F91A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DA7D783-153F-42EF-87E4-239DEC80F91A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0A995D46-30D9-42A9-BA92-225340DFC214} + EndGlobalSection +EndGlobal diff --git a/YTExtractor/Data/MongoDbConnector.cs b/YTExtractor/Data/MongoDbConnector.cs new file mode 100644 index 0000000..1bc8f10 --- /dev/null +++ b/YTExtractor/Data/MongoDbConnector.cs @@ -0,0 +1,47 @@ +using MongoDB.Driver; + +namespace YTExtractor.Data +{ + public class MongoDBConnector + { + private readonly IMongoDatabase _database; + private readonly IMongoCollection _collection; + + public MongoDBConnector(IConfiguration configuration) + { + var connectionString = configuration.GetSection("MongoDbConnaction").Value; + var client = new MongoClient(connectionString); + _database = client.GetDatabase("YTExtractor"); + _collection = _database.GetCollection("videos"); + } + + public async Task InsertVideo(VideoData video) + { + await _collection.InsertOneAsync(video); + } + + public async Task GetVideoById(string id) + { + return await _collection.Find(x => x.Id == id).FirstOrDefaultAsync(); + } + + public async Task> GetAllVideos() + { + return await _collection.Find(_ => true).ToListAsync(); + } + + public async Task UpdateVideo(VideoData video) + { + await _collection.ReplaceOneAsync(x => x.Id == video.Id, video); + } + + public async Task DeleteVideo(string id) + { + await _collection.DeleteOneAsync(x => x.Id == id); + } + public async Task GetVideoByUrl(string url) + { + return await _collection.Find(x => x.Url == url).FirstOrDefaultAsync(); + } + } +} diff --git a/YTExtractor/Data/VideoData.cs b/YTExtractor/Data/VideoData.cs new file mode 100644 index 0000000..fc3d5e6 --- /dev/null +++ b/YTExtractor/Data/VideoData.cs @@ -0,0 +1,17 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace YTExtractor.Data +{ + public class VideoData + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + + public string Titulo { get; set; } + public string Url { get; set; } + public string ThumbnailUrl { get; set; } + public string TranscText { get; set; } + } +} diff --git a/YTExtractor/Dockerfile b/YTExtractor/Dockerfile new file mode 100644 index 0000000..d98e9c5 --- /dev/null +++ b/YTExtractor/Dockerfile @@ -0,0 +1,19 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY . . +RUN dotnet restore +RUN dotnet publish -c Release -o /app + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app +COPY --from=build /app . + +RUN apt-get update && \ + apt-get install -y python3-pip && \ + pip3 install yt-dlp && \ + mkdir /app/temp && \ + chmod 777 /app/temp && \ + which yt-dlp || (echo "yt-dlp not found" && exit 1) + +EXPOSE 80 +ENTRYPOINT ["dotnet", "YTExtractor.dll"] \ No newline at end of file diff --git a/YTExtractor/Program.cs b/YTExtractor/Program.cs new file mode 100644 index 0000000..7df6745 --- /dev/null +++ b/YTExtractor/Program.cs @@ -0,0 +1,87 @@ +using YTExtractor; +using Serilog; +using Serilog.Sinks.InfluxDB; +using YTExtractor.Data; + +// App configuration and endpoints +var builder = WebApplication.CreateBuilder(args); + +var environment = builder.Environment.EnvironmentName; + +Log.Logger = new LoggerConfiguration() + .WriteTo.InfluxDB( + address: "http://192.168.0.76", + dbName: "telegraf", + source: "YTExtractor-{environment}") + .Enrich.WithProperty("Environment", environment) + .CreateLogger(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddSingleton(); + +var app = builder.Build(); +app.UseSwagger(); +app.UseSwaggerUI(); + + +app.MapPost("/api/video-info", async (VideoRequest request, MongoDBConnector mongo) => +{ + try + { + if (!YoutubeService.IsValidYouTubeUrl(request.Url)) + return Results.BadRequest("Invalid YouTube URL"); + + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + Log.Information($"Obtendo dados do video: {request.Url}"); + + var videoExists = await mongo.GetVideoByUrl(request.Url); + if (videoExists != null) + { + return Results.Ok(new VideoInfo( + videoExists.Url, + videoExists.Titulo, + videoExists.ThumbnailUrl, + videoExists.TranscText + )); + } + + var info = await YoutubeService.GetVideoInfo(request.Url, tempDir); + var subtitles = await YoutubeService.GetSubtitles(request.Url, request.Language, tempDir); + + await mongo.InsertVideo(new VideoData + { + Url = request.Url, + Titulo = info.Title, + ThumbnailUrl = info.ThumbnailUrl, + TranscText = subtitles + }); + + Log.Information($"Dados de video obtidos: {request.Url}"); + return Results.Ok(new VideoInfo( + request.Url, + info.Title, + info.ThumbnailUrl, + subtitles + )); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to get video info"); + return Results.Problem(ex.Message); + } +}) +.WithName("GetVideoInfo") +.WithOpenApi(); + +app.Run(); \ No newline at end of file diff --git a/YTExtractor/Properties/launchSettings.json b/YTExtractor/Properties/launchSettings.json new file mode 100644 index 0000000..b45f1dd --- /dev/null +++ b/YTExtractor/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:49221", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5102", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7082;http://localhost:5102", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/YTExtractor/VideoInfo.cs b/YTExtractor/VideoInfo.cs new file mode 100644 index 0000000..2f4a478 --- /dev/null +++ b/YTExtractor/VideoInfo.cs @@ -0,0 +1,6 @@ +using System; + +namespace YTExtractor +{ + public record VideoInfo(string Url, string Title, string ThumbnailUrl, string Subtitles); +} \ No newline at end of file diff --git a/YTExtractor/VideoRequest.cs b/YTExtractor/VideoRequest.cs new file mode 100644 index 0000000..4a11bd1 --- /dev/null +++ b/YTExtractor/VideoRequest.cs @@ -0,0 +1,4 @@ +namespace YTExtractor +{ + public record VideoRequest(string Url, string Language); +} diff --git a/YTExtractor/YTExtractor.csproj b/YTExtractor/YTExtractor.csproj new file mode 100644 index 0000000..2740d92 --- /dev/null +++ b/YTExtractor/YTExtractor.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/YTExtractor/YTExtractor.csproj.user b/YTExtractor/YTExtractor.csproj.user new file mode 100644 index 0000000..9ff5820 --- /dev/null +++ b/YTExtractor/YTExtractor.csproj.user @@ -0,0 +1,6 @@ + + + + https + + \ No newline at end of file diff --git a/YTExtractor/YTExtractor.http b/YTExtractor/YTExtractor.http new file mode 100644 index 0000000..95626e4 --- /dev/null +++ b/YTExtractor/YTExtractor.http @@ -0,0 +1,6 @@ +@YTExtractor_HostAddress = http://localhost:5102 + +GET {{YTExtractor_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/YTExtractor/YoutubeService.cs b/YTExtractor/YoutubeService.cs new file mode 100644 index 0000000..e099e45 --- /dev/null +++ b/YTExtractor/YoutubeService.cs @@ -0,0 +1,76 @@ +using Microsoft.Extensions.Hosting.Internal; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace YTExtractor +{ + public class YoutubeService + { + public static bool IsValidYouTubeUrl(string urlx) + { + return Regex.IsMatch(urlx, @"^(https?\:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$"); + } + + public static async Task GetVideoInfo(string url, string workingDir) + { + 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"); + + var jsonDoc = JsonDocument.Parse(output); + var root = jsonDoc.RootElement; + + return new YtDlpInfo( + root.GetProperty("title").GetString() ?? "", + root.GetProperty("thumbnail").GetString() ?? "" + ); + } + + public static async Task GetSubtitles(string url, string language, string workingDir) + { + var pathExe = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var exePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Path.Combine(pathExe, "yt-dlp.exe") + : "yt-dlp"; + + //var wokingExe = Path.Combine(pathExe, "yt-dlp.exe"); + 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"); + + return await File.ReadAllTextAsync(subtitleFile); + } + } +} diff --git a/YTExtractor/YtDlpInfo.cs b/YTExtractor/YtDlpInfo.cs new file mode 100644 index 0000000..1d880ae --- /dev/null +++ b/YTExtractor/YtDlpInfo.cs @@ -0,0 +1,6 @@ +using System; + +namespace YTExtractor +{ + public record YtDlpInfo(string Title, string ThumbnailUrl); +} \ No newline at end of file diff --git a/YTExtractor/appsettings.Development.json b/YTExtractor/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/YTExtractor/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/YTExtractor/appsettings.json b/YTExtractor/appsettings.json new file mode 100644 index 0000000..4641055 --- /dev/null +++ b/YTExtractor/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "MongoDbConnaction": "mongodb://admin:c4rn31r0@192.168.0.82:27017,192.168.0.81:27017/?replicaSet=rs0" +} diff --git a/YTExtractor/yt-dlp.exe b/YTExtractor/yt-dlp.exe new file mode 100644 index 0000000..15e3f95 Binary files /dev/null and b/YTExtractor/yt-dlp.exe differ