Files
live_recorder/src/LiveRecorder.WebApi/Program.cs
T

243 lines
9.8 KiB
C#

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.Douyin;
using LiveRecorder.Infrastructure.Platforms.Douyin.Danmaku;
using LiveRecorder.Infrastructure.Platforms.Douyin.Signing;
using LiveRecorder.Infrastructure.Platforms.Huya;
using LiveRecorder.Infrastructure.Services;
using LiveRecorder.WebApi.Middleware;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
var resetRecordingData = args.Contains("--reset-recording-data", StringComparer.OrdinalIgnoreCase);
var corsOrigins = builder.Configuration.GetSection("Cors:Origins").Get<string[]>() ?? ["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<string>()
}
});
});
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 sqliteConnectionStringBuilder = new SqliteConnectionStringBuilder(
builder.Configuration.GetConnectionString("DefaultConnection"))
{
Cache = SqliteCacheMode.Shared,
Mode = SqliteOpenMode.ReadWriteCreate,
DefaultTimeout = 30
};
builder.Services.AddDbContext<LiveRecorderDbContext>(options =>
options.UseSqlite(sqliteConnectionStringBuilder.ToString()));
builder.Services.AddScoped<IUnitOfWork>(provider => provider.GetRequiredService<LiveRecorderDbContext>());
builder.Services.AddScoped<IAppSettingRepository, AppSettingRepository>();
builder.Services.AddScoped<ILiveRoomRepository, LiveRoomRepository>();
builder.Services.AddScoped<IRecordSessionRepository, RecordSessionRepository>();
builder.Services.AddScoped<IRecordTaskRepository, RecordTaskRepository>();
builder.Services.AddScoped<IRecordResultRepository, RecordResultRepository>();
builder.Services.AddScoped<ISystemLogRepository, SystemLogRepository>();
builder.Services.AddScoped<IUserAccountRepository, UserAccountRepository>();
builder.Services.AddScoped<IUserSessionRepository, UserSessionRepository>();
builder.Services.AddScoped<ISystemSettingsService, SystemSettingsService>();
builder.Services.AddScoped<ISystemLogService, SystemLogService>();
builder.Services.AddScoped<IEmailNotificationService, EmailNotificationService>();
builder.Services.AddScoped<IEventScriptService, EventScriptService>();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<LiveRoomService>();
builder.Services.AddScoped<LiveRoomStatusService>();
builder.Services.AddScoped<LiveRoomRecordingSettingsResolver>();
builder.Services.AddScoped<RecordService>();
builder.Services.AddScoped<RecordSessionService>();
builder.Services.AddScoped<StoppedOrphanRecordSessionCleanupService>();
builder.Services.AddScoped<DatabaseInitializer>();
builder.Services.AddSingleton<BilibiliWbiSigner>();
builder.Services.AddScoped<BilibiliHttpClient>();
builder.Services.AddScoped<DouyinHttpClient>();
builder.Services.AddSingleton<DouyinXBogusSigner>();
builder.Services.AddSingleton<DouyinLiveWsSignatureSigner>();
builder.Services.AddScoped<ILivePlatformAdapter, DouyinLivePlatformAdapter>();
builder.Services.AddScoped<ILivePlatformAdapter, BilibiliLivePlatformAdapter>();
builder.Services.AddScoped<ILivePlatformAdapter, HuyaLivePlatformAdapter>();
builder.Services.AddScoped<ILivePlatformAdapterFactory, LivePlatformAdapterFactory>();
builder.Services.AddScoped<ILiveDanmakuAdapter, DouyinDanmakuAdapter>();
builder.Services.AddScoped<ILiveDanmakuAdapter, BilibiliDanmakuAdapter>();
builder.Services.AddScoped<ILiveDanmakuAdapterFactory, LiveDanmakuAdapterFactory>();
builder.Services.AddSingleton<IFfmpegService, FfmpegService>();
builder.Services.AddSingleton<IStorageGuardService, StorageGuardService>();
builder.Services.AddScoped<IRecordMediaService, RecordMediaService>();
builder.Services.AddHostedService<LiveRoomPollingBackgroundService>();
var app = builder.Build();
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.UseSwagger();
app.UseSwaggerUI();
app.UseCors("frontend");
app.UseMiddleware<ApiTokenAuthenticationMiddleware>();
app.MapGet("/", () => Results.Redirect("/swagger"));
app.MapControllers();
if (!resetRecordingData)
{
using var scope = app.Services.CreateScope();
var initializer = scope.ServiceProvider.GetRequiredService<DatabaseInitializer>();
await initializer.InitializeAsync();
}
if (resetRecordingData)
{
using var scope = app.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<LiveRecorderDbContext>();
var before = await ReadRecordingDataCountsAsync(dbContext);
await using (var transaction = await dbContext.Database.BeginTransactionAsync())
{
await dbContext.Database.ExecuteSqlRawAsync(
"DELETE FROM SystemLogEntries WHERE LiveRoomId IS NOT NULL OR RecordSessionId IS NOT NULL OR RecordTaskId IS NOT NULL;");
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM RecordResults;");
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM RecordTasks;");
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM RecordSessions;");
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM LiveRooms;");
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<Dictionary<string, long>> ReadRecordingDataCountsAsync(LiveRecorderDbContext dbContext)
{
return new Dictionary<string, long>(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
}
};
}