using System.Net; using System.Net.Security; using System.Security.Authentication; using LiveRecorder.Application.Abstractions.Auth; using LiveRecorder.Application.Abstractions.Logging; using LiveRecorder.Application.Abstractions.Notifications; using LiveRecorder.Application.Abstractions.Persistence; using LiveRecorder.Application.Abstractions.Platforms; using LiveRecorder.Application.Abstractions.Recording; using LiveRecorder.Application.Abstractions.Scripting; using LiveRecorder.Application.Abstractions.Settings; using LiveRecorder.Application.Abstractions.Storage; using LiveRecorder.Application.Services; using LiveRecorder.Infrastructure.Persistence; using LiveRecorder.Infrastructure.Persistence.Repositories; using LiveRecorder.Infrastructure.Platforms.Bilibili; using LiveRecorder.Infrastructure.Platforms.Bilibili.Danmaku; using LiveRecorder.Infrastructure.Platforms.Common; using LiveRecorder.Infrastructure.Platforms.Douyu; using LiveRecorder.Infrastructure.Platforms.Douyin; using LiveRecorder.Infrastructure.Platforms.Douyin.Danmaku; using LiveRecorder.Infrastructure.Platforms.Douyin.Signing; using LiveRecorder.Infrastructure.Platforms.Huya; using LiveRecorder.Infrastructure.Platforms.Kuaishou; using LiveRecorder.Infrastructure.Platforms.Migu; using LiveRecorder.Infrastructure.Platforms.PandaTV; using LiveRecorder.Infrastructure.Platforms.TikTok; using LiveRecorder.Infrastructure.Platforms.Twitch; using LiveRecorder.Infrastructure.Platforms.Xiaohongshu; using LiveRecorder.Infrastructure.Platforms.YouTube; using LiveRecorder.Infrastructure.Services; using LiveRecorder.WebApi.Middleware; using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; using Npgsql; var builder = WebApplication.CreateBuilder(args); var resetRecordingData = args.Contains("--reset-recording-data", StringComparer.OrdinalIgnoreCase); var migrateSqlitePath = GetOptionValue(args, "--migrate-sqlite"); var corsOrigins = builder.Configuration.GetSection("Cors:Origins").Get() ?? ["http://localhost:5173"]; builder.Services.AddControllers(); builder.Services.AddMemoryCache(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "Live Recorder API", Version = "v1", Description = "Multi-platform live recording service" }); options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Input token as: Bearer {token}", Name = "Authorization", Type = SecuritySchemeType.ApiKey }); options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty() } }); }); builder.Services.AddCors(options => { options.AddPolicy("frontend", policy => { policy.WithOrigins(corsOrigins) .AllowAnyHeader() .AllowAnyMethod(); }); }); builder.Services.AddHttpClient("douyin", client => { client.Timeout = TimeSpan.FromSeconds(20); client.DefaultRequestVersion = HttpVersion.Version11; client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; }) .ConfigurePrimaryHttpMessageHandler(() => CreateDouyinHttpHandler(useProxy: true)); builder.Services.AddHttpClient("douyin-direct", client => { client.Timeout = TimeSpan.FromSeconds(20); client.DefaultRequestVersion = HttpVersion.Version11; client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; }) .ConfigurePrimaryHttpMessageHandler(() => CreateDouyinHttpHandler(useProxy: false)); builder.Services.AddHttpClient("bilibili", client => { client.Timeout = TimeSpan.FromSeconds(20); client.DefaultRequestVersion = HttpVersion.Version11; client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; }) .ConfigurePrimaryHttpMessageHandler(static () => new SocketsHttpHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli, PooledConnectionLifetime = TimeSpan.FromMinutes(5), PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30), MaxConnectionsPerServer = 8, ConnectTimeout = TimeSpan.FromSeconds(10), UseCookies = false, SslOptions = new SslClientAuthenticationOptions { EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13 } }); var defaultConnection = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("ConnectionStrings:DefaultConnection is required."); var connectionStringBuilder = new NpgsqlConnectionStringBuilder(defaultConnection); if (connectionStringBuilder.Timeout <= 0) { connectionStringBuilder.Timeout = 15; } if (connectionStringBuilder.CommandTimeout <= 0) { connectionStringBuilder.CommandTimeout = 120; } if (connectionStringBuilder.KeepAlive <= 0) { connectionStringBuilder.KeepAlive = 30; } builder.Services.AddDbContext(options => options.UseNpgsql(connectionStringBuilder.ConnectionString, npgsql => { npgsql.MigrationsAssembly(typeof(LiveRecorderDbContext).Assembly.FullName); npgsql.CommandTimeout(connectionStringBuilder.CommandTimeout); npgsql.EnableRetryOnFailure( maxRetryCount: 3, maxRetryDelay: TimeSpan.FromSeconds(15), errorCodesToAdd: null); })); builder.Services.AddScoped(provider => provider.GetRequiredService()); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(provider => provider.GetRequiredService()); builder.Services.AddHostedService(provider => provider.GetRequiredService()); builder.Services.AddHostedService(); builder.Services.AddHostedService(); var app = builder.Build(); app.UseMiddleware(); app.UseSwagger(); app.UseSwaggerUI(); app.UseCors("frontend"); app.UseMiddleware(); app.MapGet("/", () => Results.Redirect("/swagger")); app.MapControllers(); using (var scope = app.Services.CreateScope()) { var initializer = scope.ServiceProvider.GetRequiredService(); await initializer.InitializeAsync(seedDefaults: migrateSqlitePath is null, cancellationToken: default); } if (!string.IsNullOrWhiteSpace(migrateSqlitePath)) { using var scope = app.Services.CreateScope(); var migrator = scope.ServiceProvider.GetRequiredService(); var report = await migrator.MigrateAsync(migrateSqlitePath); Console.WriteLine($"SQLite migration completed. Source={report.SourcePath}"); foreach (var tableName in report.SourceCounts.Keys.OrderBy(static item => item, StringComparer.OrdinalIgnoreCase)) { Console.WriteLine($"{tableName}: {report.SourceCounts[tableName]} -> {report.TargetCounts[tableName]}"); } return; } if (resetRecordingData) { using var scope = app.Services.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var before = await ReadRecordingDataCountsAsync(dbContext); await using var transaction = await dbContext.Database.BeginTransactionAsync(); await dbContext.SystemLogEntries .Where(item => item.LiveRoomId != null || item.RecordSessionId != null || item.RecordTaskId != null) .ExecuteDeleteAsync(); await dbContext.RecordResults.ExecuteDeleteAsync(); await dbContext.RecordTasks.ExecuteDeleteAsync(); await dbContext.RecordSessions.ExecuteDeleteAsync(); await dbContext.LiveRooms.ExecuteDeleteAsync(); await transaction.CommitAsync(); var after = await ReadRecordingDataCountsAsync(dbContext); Console.WriteLine("Recording data reset complete."); foreach (var key in before.Keys) { Console.WriteLine($"{key}: {before[key]} -> {after[key]}"); } return; } app.Run(); static async Task> ReadRecordingDataCountsAsync(LiveRecorderDbContext dbContext) { return new Dictionary(StringComparer.OrdinalIgnoreCase) { ["LiveRooms"] = await dbContext.LiveRooms.LongCountAsync(), ["RecordSessions"] = await dbContext.RecordSessions.LongCountAsync(), ["RecordTasks"] = await dbContext.RecordTasks.LongCountAsync(), ["RecordResults"] = await dbContext.RecordResults.LongCountAsync(), ["SystemLogEntries(Related)"] = await dbContext.SystemLogEntries.LongCountAsync( item => item.LiveRoomId != null || item.RecordSessionId != null || item.RecordTaskId != null) }; } static SocketsHttpHandler CreateDouyinHttpHandler(bool useProxy) { return new SocketsHttpHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli, PooledConnectionLifetime = TimeSpan.FromMinutes(2), PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30), MaxConnectionsPerServer = 8, ConnectTimeout = TimeSpan.FromSeconds(10), UseCookies = false, UseProxy = useProxy, SslOptions = new SslClientAuthenticationOptions { EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13 } }; } static string? GetOptionValue(string[] args, string optionName) { for (var index = 0; index < args.Length - 1; index++) { if (string.Equals(args[index], optionName, StringComparison.OrdinalIgnoreCase)) { return args[index + 1]; } } return null; }