2020-12-21 19:08:44 +08:00
using System ;
2021-01-04 16:24:36 +08:00
using System.Collections.Generic ;
2021-02-23 18:03:37 +08:00
using System.CommandLine ;
2024-12-01 19:28:19 +08:00
using System.CommandLine.Builder ;
2022-06-07 22:41:09 +08:00
using System.CommandLine.NamingConventionBinder ;
2024-12-01 19:28:19 +08:00
using System.CommandLine.Parsing ;
2024-07-05 18:24:08 +08:00
using System.Diagnostics ;
2021-02-23 18:03:37 +08:00
using System.IO ;
2020-01-05 23:47:38 +08:00
using System.Linq ;
2022-05-09 22:42:08 +08:00
using System.Net ;
2023-12-03 23:17:21 +08:00
using System.Runtime.InteropServices ;
2022-06-04 01:24:05 +08:00
using System.Security.Authentication ;
using System.Security.Cryptography ;
using System.Security.Cryptography.X509Certificates ;
2022-05-11 14:24:12 +08:00
using System.Text.RegularExpressions ;
2020-01-05 23:47:38 +08:00
using System.Threading ;
2021-05-30 19:16:20 +08:00
using System.Threading.Tasks ;
2022-05-09 22:42:08 +08:00
using BililiveRecorder.Cli.Configure ;
2020-01-05 23:47:38 +08:00
using BililiveRecorder.Core ;
2021-02-23 18:03:37 +08:00
using BililiveRecorder.Core.Config ;
2021-12-19 21:10:34 +08:00
using BililiveRecorder.Core.Config.V3 ;
2021-02-08 16:51:19 +08:00
using BililiveRecorder.DependencyInjection ;
2022-06-17 17:42:50 +08:00
using BililiveRecorder.Flv.Pipeline ;
2021-04-14 23:46:24 +08:00
using BililiveRecorder.ToolBox ;
2021-05-30 19:16:20 +08:00
using BililiveRecorder.Web ;
2022-08-31 14:46:35 +08:00
using BililiveRecorder.Web.Models.Rest.Logs ;
2021-05-30 19:16:20 +08:00
using Microsoft.AspNetCore.Hosting ;
2022-06-04 01:24:05 +08:00
using Microsoft.AspNetCore.Server.Kestrel.Core ;
2021-02-08 16:51:19 +08:00
using Microsoft.Extensions.DependencyInjection ;
2021-05-30 19:16:20 +08:00
using Microsoft.Extensions.Hosting ;
2021-02-23 18:03:37 +08:00
using Serilog ;
2021-05-02 22:24:57 +08:00
using Serilog.Core ;
2021-02-23 18:03:37 +08:00
using Serilog.Events ;
using Serilog.Exceptions ;
2022-06-28 23:08:20 +08:00
using Serilog.Filters ;
2021-05-02 22:24:57 +08:00
using Serilog.Formatting.Compact ;
2022-07-07 21:33:46 +08:00
using Serilog.Templates ;
2020-01-05 23:47:38 +08:00
namespace BililiveRecorder.Cli
{
2020-12-21 19:08:44 +08:00
internal class Program
2020-01-05 23:47:38 +08:00
{
2021-01-26 20:06:00 +08:00
private static int Main ( string [ ] args )
2021-02-23 18:03:37 +08:00
{
2023-12-03 23:17:21 +08:00
if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows )
& & string . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "BREC_SKIP_DISABLE_QUICK_EDIT" ) ) )
{
ConsoleModeHelper . SetQuickEditMode ( false ) ;
}
2024-07-05 18:24:08 +08:00
DistributedContextPropagator . Current = DistributedContextPropagator . CreateNoOutputPropagator ( ) ;
AppContext . SetSwitch ( "System.Net.Http.EnableActivityPropagation" , false ) ;
2022-06-26 15:15:48 +08:00
RootCommand root ;
2022-05-17 18:08:53 +08:00
2022-06-26 15:15:48 +08:00
using ( var entrypointLogger = BuildLogger ( LogEventLevel . Fatal , LogEventLevel . Verbose ) )
2021-02-23 18:03:37 +08:00
{
2022-06-26 15:15:48 +08:00
entrypointLogger . Information ( "Starting, {Version}, {CommandLine}" , GitVersionInformation . InformationalVersion , args ) ;
try
{
ServicePointManager . Expect100Continue = false ;
2022-06-04 01:24:05 +08:00
2022-06-26 15:15:48 +08:00
var cmd_run = new Command ( "run" , "Run BililiveRecorder in standard mode" )
{
2022-08-27 17:03:23 +08:00
new Option < string? > ( new [ ] { "--config-override" } , ( ) = > null , "Config path override" ) ,
2022-06-26 15:15:48 +08:00
new Option < string? > ( new [ ] { "--http-bind" , "--bind" , "-b" } , ( ) = > null , "Bind address for http service" ) ,
new Option < string? > ( new [ ] { "--http-basic-user" } , ( ) = > null , "Web interface username" ) ,
new Option < string? > ( new [ ] { "--http-basic-pass" } , ( ) = > null , "Web interface password" ) ,
2024-03-09 21:02:44 +08:00
new Option < bool > ( new [ ] { "--http-open-access" } , ( ) = > false , "Allow open access from the internet" ) ,
2022-06-26 15:15:48 +08:00
new Option < bool > ( new [ ] { "--enable-file-browser" } , ( ) = > true , "Enable file browser located at '/file'" ) ,
new Option < LogEventLevel > ( new [ ] { "--loglevel" , "--log" , "-l" } , ( ) = > LogEventLevel . Information , "Minimal log level output to console" ) ,
new Option < LogEventLevel > ( new [ ] { "--logfilelevel" , "--flog" } , ( ) = > LogEventLevel . Debug , "Minimal log level output to file" ) ,
new Option < string? > ( new [ ] { "--cert-pem-path" , "--pem" } , "Path of the certificate pem file" ) ,
new Option < string? > ( new [ ] { "--cert-key-path" , "--key" } , "Path of the certificate key file" ) ,
new Option < string? > ( new [ ] { "--cert-pfx-path" , "--pfx" } , "Path of the certificate pfx file" ) ,
new Option < string? > ( new [ ] { "--cert-password" } , "Password of the certificate" ) ,
2021-02-23 18:03:37 +08:00
2022-06-26 15:15:48 +08:00
new Argument < string > ( "path" ) ,
} ;
cmd_run . AddAlias ( "r" ) ;
cmd_run . Handler = CommandHandler . Create < RunModeArguments > ( RunConfigModeAsync ) ;
2022-06-04 01:24:05 +08:00
2022-06-26 15:15:48 +08:00
var cmd_portable = new Command ( "portable" , "Run BililiveRecorder in config-less mode" )
{
new Option < string? > ( new [ ] { "--http-bind" , "--bind" , "-b" } , ( ) = > null , "Bind address for http service" ) ,
new Option < string? > ( new [ ] { "--http-basic-user" } , ( ) = > null , "Web interface username" ) ,
new Option < string? > ( new [ ] { "--http-basic-pass" } , ( ) = > null , "Web interface password" ) ,
2024-03-09 21:02:44 +08:00
new Option < bool > ( new [ ] { "--http-open-access" } , ( ) = > false , "Allow open access from the internet" ) ,
2022-06-26 15:15:48 +08:00
new Option < bool > ( new [ ] { "--enable-file-browser" } , ( ) = > true , "Enable file browser located at '/file'" ) ,
new Option < LogEventLevel > ( new [ ] { "--loglevel" , "--log" , "-l" } , ( ) = > LogEventLevel . Information , "Minimal log level output to console" ) ,
new Option < LogEventLevel > ( new [ ] { "--logfilelevel" , "--flog" } , ( ) = > LogEventLevel . Debug , "Minimal log level output to file" ) ,
new Option < string? > ( new [ ] { "--cert-pem-path" , "--pem" } , "Path of the certificate pem file" ) ,
new Option < string? > ( new [ ] { "--cert-key-path" , "--key" } , "Path of the certificate key file" ) ,
new Option < string? > ( new [ ] { "--cert-pfx-path" , "--pfx" } , "Path of the certificate pfx file" ) ,
new Option < string? > ( new [ ] { "--cert-password" } , "Password of the certificate" ) ,
2021-02-23 18:03:37 +08:00
2022-06-26 15:15:48 +08:00
new Option < RecordMode > ( new [ ] { "--record-mode" , "--mode" } , ( ) = > RecordMode . Standard , "Recording mode" ) ,
new Option < string > ( new [ ] { "--cookie" , "-c" } , "Cookie string for api requests" ) ,
new Option < string > ( new [ ] { "--filename" , "-f" } , "File name format" ) ,
new Option < PortableModeArguments . PortableDanmakuMode > ( new [ ] { "--danmaku" , "-d" } , "Flags for danmaku recording" ) ,
new Option < string > ( "--webhook-url" , "URL of webhoook" ) ,
new Option < string > ( "--live-api-host" ) ,
new Argument < string > ( "output-path" ) ,
new Argument < int [ ] > ( "room-ids" , ( ) = > Array . Empty < int > ( ) )
} ;
cmd_portable . AddAlias ( "p" ) ;
cmd_portable . Handler = CommandHandler . Create < PortableModeArguments > ( RunPortableModeAsync ) ;
root = new RootCommand ( "A Stream Recorder For Bilibili Live" )
{
cmd_run ,
cmd_portable ,
new ConfigureCommand ( ) ,
new ToolCommand ( ) ,
} ;
}
catch ( Exception ex )
{
entrypointLogger . Fatal ( ex , "Fatal error during startup" ) ;
return - 1 ;
}
}
2020-01-05 23:47:38 +08:00
2024-12-01 19:28:19 +08:00
var builder = new CommandLineBuilder ( root ) ;
builder . AddMiddleware ( async ( context , next ) = >
{
var isToolCommand = false ;
var tct = typeof ( ToolCommand ) ;
var cr = context . ParseResult . CommandResult ;
while ( cr is not null )
{
if ( cr . Command . GetType ( ) = = tct )
{
isToolCommand = true ;
break ;
}
cr = cr . Parent as CommandResult ;
}
cr = null ;
if ( isToolCommand )
{
// hack to enable logging for tool commands
using var logger = BuildLogger ( LogEventLevel . Fatal , LogEventLevel . Verbose ) ;
Log . Logger = logger ;
await next ( context ) ;
return ;
}
else
{
await next ( context ) ;
}
} ) ;
builder . UseDefaults ( ) ;
var parser = builder . Build ( ) ;
return parser . Invoke ( args ) ;
2021-02-23 18:03:37 +08:00
}
2021-05-30 19:16:20 +08:00
private static async Task < int > RunConfigModeAsync ( RunModeArguments args )
2020-01-05 23:47:38 +08:00
{
2022-05-16 18:27:00 +08:00
var path = Path . GetFullPath ( args . Path ) ;
2021-05-30 19:16:20 +08:00
2022-08-31 14:46:35 +08:00
using var logger = BuildLogger ( args . LogLevel , args . LogFileLevel , enableWebLog : args . HttpBind is not null ) ;
2021-02-23 18:03:37 +08:00
Log . Logger = logger ;
path = Path . GetFullPath ( path ) ;
2022-08-27 17:03:23 +08:00
2022-08-27 17:12:31 +08:00
ConfigV3 ? config ;
2022-08-27 17:03:23 +08:00
if ( args . ConfigOverride is not null )
{
2024-06-22 23:10:06 +08:00
if ( Directory . Exists ( args . ConfigOverride ) )
{
var overrideFile = Path . Combine ( args . ConfigOverride , "config.json" ) ;
logger . Information ( "Using config from {ConfigOverride}" , overrideFile ) ;
config = ConfigParser . LoadFromFile ( overrideFile ) ;
}
else
{
logger . Information ( "Using config from {ConfigOverride}" , args . ConfigOverride ) ;
config = ConfigParser . LoadFromFile ( args . ConfigOverride ) ;
}
2022-08-27 17:12:31 +08:00
}
else
{
config = ConfigParser . LoadFromDirectory ( path ) ;
2022-08-27 17:03:23 +08:00
}
2021-02-23 18:03:37 +08:00
if ( config is null )
{
2022-08-27 17:03:23 +08:00
logger . Error ( "Config Loading Failed" ) ;
2021-02-23 18:03:37 +08:00
return - 1 ;
}
config . Global . WorkDirectory = path ;
2022-08-27 17:03:23 +08:00
config . ConfigPathOverride = args . ConfigOverride ;
2021-02-08 16:51:19 +08:00
2021-02-23 18:03:37 +08:00
var serviceProvider = BuildServiceProvider ( config , logger ) ;
2022-05-11 14:24:12 +08:00
2022-06-04 01:24:05 +08:00
return await RunRecorderAsync ( serviceProvider , args ) ;
2022-05-11 14:24:12 +08:00
}
private static async Task < int > RunPortableModeAsync ( PortableModeArguments args )
{
2022-08-31 14:46:35 +08:00
using var logger = BuildLogger ( args . LogLevel , args . LogFileLevel , enableWebLog : args . HttpBind is not null ) ;
2022-05-11 14:24:12 +08:00
Log . Logger = logger ;
var config = new ConfigV3 ( )
{
DisableConfigSave = true ,
} ;
{
var global = config . Global ;
if ( ! string . IsNullOrWhiteSpace ( args . Cookie ) )
global . Cookie = args . Cookie ;
if ( ! string . IsNullOrWhiteSpace ( args . LiveApiHost ) )
global . LiveApiHost = args . LiveApiHost ;
if ( ! string . IsNullOrWhiteSpace ( args . Filename ) )
global . FileNameRecordTemplate = args . Filename ;
if ( ! string . IsNullOrWhiteSpace ( args . WebhookUrl ) )
global . WebHookUrlsV2 = args . WebhookUrl ;
global . RecordMode = args . RecordMode ;
var danmaku = args . Danmaku ;
global . RecordDanmaku = danmaku ! = PortableModeArguments . PortableDanmakuMode . None ;
global . RecordDanmakuSuperChat = danmaku . HasFlag ( PortableModeArguments . PortableDanmakuMode . SuperChat ) ;
global . RecordDanmakuGuard = danmaku . HasFlag ( PortableModeArguments . PortableDanmakuMode . Guard ) ;
global . RecordDanmakuGift = danmaku . HasFlag ( PortableModeArguments . PortableDanmakuMode . Gift ) ;
global . RecordDanmakuRaw = danmaku . HasFlag ( PortableModeArguments . PortableDanmakuMode . RawData ) ;
2022-05-16 18:27:00 +08:00
global . WorkDirectory = Path . GetFullPath ( args . OutputPath ) ;
2022-05-11 14:24:12 +08:00
config . Rooms = args . RoomIds . Select ( x = > new RoomConfig { RoomId = x , AutoRecord = true } ) . ToList ( ) ;
}
var serviceProvider = BuildServiceProvider ( config , logger ) ;
2022-06-04 01:24:05 +08:00
return await RunRecorderAsync ( serviceProvider , args ) ;
2022-05-11 14:24:12 +08:00
}
2022-06-04 01:24:05 +08:00
private static async Task < int > RunRecorderAsync ( IServiceProvider serviceProvider , SharedArguments sharedArguments )
2022-05-11 14:24:12 +08:00
{
var logger = serviceProvider . GetRequiredService < ILogger > ( ) ;
2021-05-30 19:16:20 +08:00
IRecorder recorderAccessProxy ( IServiceProvider x ) = > serviceProvider . GetRequiredService < IRecorder > ( ) ;
// recorder setup done
// check if web service required
IHost ? host = null ;
2022-06-04 01:24:05 +08:00
if ( sharedArguments . HttpBind is null )
2021-05-30 19:16:20 +08:00
{
logger . Information ( "Web API not enabled" ) ;
}
else
{
2022-09-03 23:11:47 +08:00
#if DEBUG
const LogEventLevel webLogEventLevel = LogEventLevel . Debug ;
#else
const LogEventLevel webLogEventLevel = LogEventLevel . Error ;
#endif
2021-05-30 19:16:20 +08:00
host = new HostBuilder ( )
2022-09-03 23:11:47 +08:00
. UseSerilog ( logger : new LoggerConfiguration ( ) . MinimumLevel . Is ( webLogEventLevel ) . WriteTo . Logger ( logger ) . CreateLogger ( ) , dispose : true )
2021-05-30 19:16:20 +08:00
. ConfigureServices ( services = >
{
services . AddSingleton ( recorderAccessProxy ) ;
2022-06-08 00:15:05 +08:00
2022-06-08 00:58:08 +08:00
services . AddSingleton ( new BililiveRecorderFileExplorerSettings ( sharedArguments . EnableFileBrowser ) ) ;
2023-06-17 23:24:36 +08:00
sharedArguments . HttpBasicUser ? ? = Environment . GetEnvironmentVariable ( "BREC_HTTP_BASIC_USER" ) ;
sharedArguments . HttpBasicPass ? ? = Environment . GetEnvironmentVariable ( "BREC_HTTP_BASIC_PASS" ) ;
2022-06-08 00:15:05 +08:00
if ( sharedArguments . HttpBasicUser is not null | | sharedArguments . HttpBasicPass is not null )
{
services . AddSingleton ( new BasicAuthCredential ( sharedArguments . HttpBasicUser ? ? string . Empty , sharedArguments . HttpBasicPass ? ? string . Empty ) ) ;
}
2024-03-09 21:02:44 +08:00
2024-06-22 23:10:06 +08:00
if ( sharedArguments . HttpOpenAccess | | Environment . GetEnvironmentVariable ( "BREC_HTTP_OPEN_ACCESS" ) is not null )
{
2024-03-09 21:02:44 +08:00
services . AddSingleton ( new DisableOpenAccessWarningConfig ( ) ) ;
}
2021-05-30 19:16:20 +08:00
} )
. ConfigureWebHost ( webBuilder = >
{
webBuilder
2022-05-11 14:24:12 +08:00
. UseKestrel ( option = >
{
2022-06-04 01:24:05 +08:00
( var scheme , var host , var port ) = ParseBindArgument ( sharedArguments . HttpBind , logger ) ;
if ( host . Equals ( "localhost" , StringComparison . OrdinalIgnoreCase ) )
{
option . ListenLocalhost ( port , ListenConfigure ) ;
}
else if ( IPAddress . TryParse ( host , out var ip ) )
{
option . Listen ( ip , port , ListenConfigure ) ;
}
else
{
option . ListenAnyIP ( port , ListenConfigure ) ;
}
void ListenConfigure ( ListenOptions listenOptions )
{
if ( scheme = = "https" )
{
listenOptions . UseHttps ( LoadCertificate ( sharedArguments , logger ) ? ? GenerateSelfSignedCertificate ( logger ) , https = >
{
2024-11-22 22:52:28 +08:00
https . SslProtocols = SslProtocols . Tls12 | SslProtocols . Tls13 ;
2022-06-04 01:24:05 +08:00
} ) ;
}
}
2022-05-11 14:24:12 +08:00
} )
2021-05-30 19:16:20 +08:00
. UseStartup < Startup > ( ) ;
} )
. Build ( ) ;
}
2021-01-04 16:24:36 +08:00
ConsoleCancelEventHandler p = null ! ;
2021-05-30 19:16:20 +08:00
var cts = new CancellationTokenSource ( ) ;
2021-01-04 16:24:36 +08:00
p = ( sender , e ) = >
{
2021-05-30 19:16:20 +08:00
logger . Information ( "Ctrl+C pressed. Exiting" ) ;
2021-01-04 16:24:36 +08:00
Console . CancelKeyPress - = p ;
e . Cancel = true ;
2021-05-30 19:16:20 +08:00
cts . Cancel ( ) ;
2021-01-04 16:24:36 +08:00
} ;
Console . CancelKeyPress + = p ;
2020-06-21 21:26:51 +08:00
2022-05-11 14:24:12 +08:00
IRecorder ? recorder = null ;
try
2021-05-30 19:16:20 +08:00
{
2022-05-11 14:24:12 +08:00
var token = cts . Token ;
if ( host is not null )
{
try
{
await host . StartAsync ( token ) ;
}
catch ( Exception ex )
{
logger . Fatal ( ex , "Failed to start web server." ) ;
return - 1 ;
}
logger . Information ( "Web host started." ) ;
recorder = serviceProvider . GetRequiredService < IRecorder > ( ) ;
await Task . WhenAny ( Task . Delay ( - 1 , token ) , host . WaitForShutdownAsync ( ) ) . ConfigureAwait ( false ) ;
logger . Information ( "Shutdown in progress." ) ;
await host . StopAsync ( ) . ConfigureAwait ( false ) ;
}
else
{
recorder = serviceProvider . GetRequiredService < IRecorder > ( ) ;
await Task . Delay ( - 1 , token ) . ConfigureAwait ( false ) ;
}
2021-05-30 19:16:20 +08:00
}
2022-05-11 14:24:12 +08:00
finally
2021-05-30 19:16:20 +08:00
{
2022-05-11 14:24:12 +08:00
recorder ? . Dispose ( ) ;
// TODO 修复这里 Dispose 之后不会停止房间继续初始化
2021-05-30 19:16:20 +08:00
}
await Task . Delay ( 1000 * 3 ) . ConfigureAwait ( false ) ;
2021-01-04 16:24:36 +08:00
return 0 ;
2020-01-05 23:47:38 +08:00
}
2022-06-04 01:24:05 +08:00
private static X509Certificate2 ? LoadCertificate ( SharedArguments arguments , ILogger logger )
2020-06-21 21:26:51 +08:00
{
2022-06-04 01:24:05 +08:00
if ( arguments . CertPfxPath is not null )
2021-04-14 23:46:24 +08:00
{
2022-06-04 01:24:05 +08:00
if ( arguments . CertPemPath is not null | | arguments . CertKeyPath is not null )
{
logger . Warning ( "Both cert-pfx and cert-pem/cert-key are specified. Using cert-pfx." ) ;
}
if ( ! File . Exists ( arguments . CertPfxPath ) )
{
logger . Error ( "Certificate file {Path} not found." , arguments . CertPfxPath ) ;
return null ;
}
return new X509Certificate2 ( arguments . CertPfxPath , arguments . CertPassword ) ;
}
else if ( arguments . CertPemPath is not null | | arguments . CertKeyPath is not null )
{
if ( arguments . CertPemPath is null )
{
logger . Error ( "Certificate PEM file not specified." ) ;
return null ;
}
if ( arguments . CertKeyPath is null )
{
logger . Error ( "Certificate key file not specified." ) ;
return null ;
}
if ( ! File . Exists ( arguments . CertPemPath ) )
{
logger . Error ( "Certificate PEM file {Path} not found." , arguments . CertPemPath ) ;
return null ;
}
if ( ! File . Exists ( arguments . CertKeyPath ) )
{
logger . Error ( "Certificate key file {Path} not found." , arguments . CertKeyPath ) ;
return null ;
}
var cert = arguments . CertPassword is null
? X509Certificate2 . CreateFromPemFile ( arguments . CertPemPath , arguments . CertKeyPath )
: X509Certificate2 . CreateFromEncryptedPemFile ( arguments . CertPemPath , arguments . CertPassword , arguments . CertKeyPath ) ;
return new X509Certificate2 ( cert . Export ( X509ContentType . Pfx ) ) ;
2022-05-11 14:24:12 +08:00
}
else
2021-05-23 21:44:09 +08:00
{
2022-06-04 01:24:05 +08:00
logger . Debug ( "No certificate specified." ) ;
return null ;
}
}
private static X509Certificate2 GenerateSelfSignedCertificate ( ILogger logger )
{
logger . Warning ( "使用录播姬生成的自签名证书" ) ;
var firstDayofCurrentYear = new DateTimeOffset ( DateTime . Now . Year , 1 , 1 , 0 , 0 , 0 , TimeSpan . Zero ) ;
2022-06-04 01:57:30 +08:00
X509Certificate2 ? CA = null ;
try
{
{
using var key = RSA . Create ( ) ;
var req = new CertificateRequest ( "CN=自签名证书,每次启动都会重新生成" , key , HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ) ;
req . CertificateExtensions . Add ( new X509BasicConstraintsExtension ( true , false , 0 , false ) ) ;
CA = new X509Certificate2 ( req . CreateSelfSigned ( firstDayofCurrentYear , firstDayofCurrentYear . AddYears ( 10 ) ) . Export ( X509ContentType . Pfx ) ) ;
}
{
using var key = RSA . Create ( ) ;
2023-07-12 00:55:47 +08:00
var req = new CertificateRequest ( "CN=mikufans录播姬" , key , HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ) ;
2022-06-04 01:57:30 +08:00
var subjectAltName = new SubjectAlternativeNameBuilder ( ) ;
subjectAltName . AddDnsName ( "BililiveRecorder" ) ;
subjectAltName . AddDnsName ( "localhost" ) ;
subjectAltName . AddIpAddress ( IPAddress . Loopback ) ;
subjectAltName . AddIpAddress ( IPAddress . IPv6Loopback ) ;
subjectAltName . AddDnsName ( "*.nip.io" ) ;
subjectAltName . AddDnsName ( "*.sslip.io" ) ;
req . CertificateExtensions . Add ( subjectAltName . Build ( ) ) ;
using var cert = req . Create ( CA , firstDayofCurrentYear , firstDayofCurrentYear . AddYears ( 10 ) , "BililiveRecorder" . Select ( x = > ( byte ) x ) . ToArray ( ) ) ;
using var withPrivateKey = cert . CopyWithPrivateKey ( key ) ;
return new X509Certificate2 ( withPrivateKey . Export ( X509ContentType . Pfx ) ) ;
}
}
finally
{
CA ? . Dispose ( ) ;
}
2022-06-04 01:24:05 +08:00
}
private static ( string schema , string host , int port ) ParseBindArgument ( string bind , ILogger logger )
{
if ( int . TryParse ( bind , out var value ) )
{
// 只传入了一个端口号
return ( "http" , "localhost" , value ) ;
}
var match = Regex . Match ( bind , @"^(?<schema>https?):\/\/(?<host>[^\:\/\?\#]+)(?:\:(?<port>\d+))?(?:\/.*)?$" , RegexOptions . Singleline | RegexOptions . CultureInvariant , TimeSpan . FromSeconds ( 5 ) ) ;
if ( match . Success )
{
var schema = match . Groups [ "schema" ] . Value . ToLower ( ) ;
var host = match . Groups [ "host" ] . Value ;
var port = match . Groups [ "port" ] . Success ? int . Parse ( match . Groups [ "port" ] . Value ) : 2356 ;
return ( schema , host , port ) ;
2021-05-23 21:44:09 +08:00
}
2022-05-11 14:24:12 +08:00
else
2021-01-04 16:24:36 +08:00
{
2022-06-04 01:24:05 +08:00
logger . Warning ( "侦听参数解析失败,使用默认值 {DefaultBindLocation}" , "http://localhost:2356" ) ;
return ( "http" , "localhost" , 2356 ) ;
2022-05-11 14:24:12 +08:00
}
2020-06-21 21:26:51 +08:00
}
2020-12-21 04:13:49 +08:00
2021-12-19 21:10:34 +08:00
private static IServiceProvider BuildServiceProvider ( ConfigV3 config , ILogger logger ) = > new ServiceCollection ( )
2021-02-23 18:03:37 +08:00
. AddSingleton ( logger )
. AddFlv ( )
. AddRecorderConfig ( config )
. AddRecorder ( )
. BuildServiceProvider ( ) ;
2022-08-31 14:46:35 +08:00
private static Logger BuildLogger ( LogEventLevel logLevel , LogEventLevel logFileLevel , bool enableWebLog = false )
2022-06-07 01:52:59 +08:00
{
var logFilePath = Environment . GetEnvironmentVariable ( "BILILIVERECORDER_LOG_FILE_PATH" ) ;
if ( string . IsNullOrWhiteSpace ( logFilePath ) )
logFilePath = Path . Combine ( AppContext . BaseDirectory , "logs" , "bilirec.txt" ) ;
2022-06-28 23:08:20 +08:00
logFilePath = Path . GetFullPath ( logFilePath ) ;
var logFilePathMicrosoft = Path . Combine ( Path . GetDirectoryName ( logFilePath ) ! , Path . GetFileNameWithoutExtension ( logFilePath ) + "-web" + Path . GetExtension ( logFilePath ) ) ;
var matchMicrosoft = Matching . FromSource ( "Microsoft" ) ;
2022-06-07 01:52:59 +08:00
2022-07-07 21:33:46 +08:00
var ansiColorSupport = ! OperatingSystem . IsWindows ( ) | | ! string . IsNullOrWhiteSpace ( Environment . GetEnvironmentVariable ( "WT_SESSION" ) ) ;
2022-07-01 17:23:37 +08:00
2022-07-07 21:33:46 +08:00
var builder = new LoggerConfiguration ( )
2022-06-07 01:52:59 +08:00
. MinimumLevel . Verbose ( )
. Enrich . WithProcessId ( )
. Enrich . WithThreadId ( )
. Enrich . WithThreadName ( )
. Enrich . FromLogContext ( )
. Enrich . WithExceptionDetails ( )
. Destructure . AsScalar < IPAddress > ( )
2022-06-17 17:42:50 +08:00
. Destructure . AsScalar < ProcessingComment > ( )
2022-06-07 01:52:59 +08:00
. Destructure . ByTransforming < Flv . Xml . XmlFlvFile . XmlFlvFileMeta > ( x = > new
{
x . Version ,
x . ExportTime ,
x . FileSize ,
x . FileCreationTime ,
x . FileModificationTime ,
} )
2022-06-28 23:08:20 +08:00
. WriteTo . Logger ( sl = >
{
sl
. Filter . ByExcluding ( matchMicrosoft )
. WriteTo . File ( new CompactJsonFormatter ( ) , logFilePath , restrictedToMinimumLevel : logFileLevel , shared : true , rollingInterval : RollingInterval . Day , rollOnFileSizeLimit : true )
;
} )
. WriteTo . Logger ( sl = >
{
sl
. Filter . ByIncludingOnly ( matchMicrosoft )
. WriteTo . File ( new CompactJsonFormatter ( ) , logFilePathMicrosoft , restrictedToMinimumLevel : logFileLevel , shared : true , rollingInterval : RollingInterval . Day , rollOnFileSizeLimit : true )
;
2022-07-07 21:33:46 +08:00
} ) ;
2022-08-31 14:46:35 +08:00
if ( enableWebLog )
{
var webSink = new WebApiLogEventSink ( new CompactJsonFormatter ( ) ) ;
WebApiLogEventSink . Instance = webSink ;
builder . WriteTo . Logger ( sl = >
{
sl
. Filter . ByExcluding ( matchMicrosoft )
. WriteTo . Async ( l = > l . Sink ( webSink , restrictedToMinimumLevel : LogEventLevel . Debug ) ) ;
} ) ;
}
2022-07-07 21:33:46 +08:00
if ( ansiColorSupport )
{
builder . WriteTo . Console ( new ExpressionTemplate ( "[{@t:HH:mm:ss} {@l:u3}{#if SourceContext is not null} ({SourceContext}){#end}]{#if RoomId is not null} [{RoomId}]{#end} {@m}{#if ExceptionDetail is not null}\n [{ExceptionDetail['Type']}]: {ExceptionDetail['Message']}{#end}\n" , theme : Serilog . Templates . Themes . TemplateTheme . Code ) , logLevel ) ;
}
else
{
builder . WriteTo . Console ( restrictedToMinimumLevel : logLevel , outputTemplate : "[{Timestamp:HH:mm:ss} {Level:u3} ({SourceContext})] [{RoomId}] {Message:lj}{NewLine}{Exception}" ) ;
}
return builder . CreateLogger ( ) ;
2022-06-07 01:52:59 +08:00
}
2021-02-23 18:03:37 +08:00
2022-06-04 01:24:05 +08:00
public abstract class SharedArguments
2021-05-30 19:16:20 +08:00
{
public LogEventLevel LogLevel { get ; set ; } = LogEventLevel . Information ;
public LogEventLevel LogFileLevel { get ; set ; } = LogEventLevel . Information ;
2022-06-04 01:24:05 +08:00
public string? HttpBind { get ; set ; } = null ;
2021-05-30 19:16:20 +08:00
2022-06-08 00:15:05 +08:00
public string? HttpBasicUser { get ; set ; } = null ;
public string? HttpBasicPass { get ; set ; } = null ;
2021-05-30 19:16:20 +08:00
2024-03-09 21:02:44 +08:00
public bool HttpOpenAccess { get ; set ; } = false ;
2022-06-08 00:58:08 +08:00
public bool EnableFileBrowser { get ; set ; }
2022-06-04 01:24:05 +08:00
public string? CertPemPath { get ; set ; } = null ;
public string? CertKeyPath { get ; set ; } = null ;
2021-05-25 22:02:54 +08:00
2022-06-04 01:24:05 +08:00
public string? CertPfxPath { get ; set ; } = null ;
2021-07-08 20:51:24 +08:00
2022-06-04 01:24:05 +08:00
public string? CertPassword { get ; set ; } = null ;
}
2022-05-11 14:24:12 +08:00
2022-06-04 01:24:05 +08:00
public sealed class RunModeArguments : SharedArguments
{
2022-08-27 17:03:23 +08:00
public string? ConfigOverride { get ; set ; } = null ;
2022-06-04 01:24:05 +08:00
public string Path { get ; set ; } = string . Empty ;
}
public sealed class PortableModeArguments : SharedArguments
{
2021-11-30 19:29:30 +08:00
public RecordMode RecordMode { get ; set ; } = RecordMode . Standard ;
2021-02-23 18:03:37 +08:00
public string OutputPath { get ; set ; } = string . Empty ;
2020-12-21 04:13:49 +08:00
2021-02-23 18:03:37 +08:00
public string? Cookie { get ; set ; }
2020-12-21 04:13:49 +08:00
2021-02-23 18:03:37 +08:00
public string? LiveApiHost { get ; set ; }
2020-12-21 04:13:49 +08:00
2021-12-19 21:10:34 +08:00
public string? Filename { get ; set ; }
2021-01-04 16:24:36 +08:00
2021-08-28 14:26:23 +08:00
public string? WebhookUrl { get ; set ; }
2021-05-23 21:44:09 +08:00
public PortableDanmakuMode Danmaku { get ; set ; }
2021-02-23 18:03:37 +08:00
public IEnumerable < int > RoomIds { get ; set ; } = Enumerable . Empty < int > ( ) ;
2021-05-23 21:44:09 +08:00
[Flags]
public enum PortableDanmakuMode
{
None = 0 ,
Danmaku = 1 < < 0 ,
SuperChat = 1 < < 1 ,
Guard = 1 < < 2 ,
Gift = 1 < < 3 ,
RawData = 1 < < 4 ,
All = Danmaku | SuperChat | Guard | Gift | RawData
}
2021-02-23 18:03:37 +08:00
}
2023-12-03 23:17:21 +08:00
private static class ConsoleModeHelper
{
internal static bool SetQuickEditMode ( bool enable = true )
{
return SetMode ( ConsoleModes . ENABLE_QUICK_EDIT_MODE , enable ) ;
}
private static bool SetMode ( ConsoleModes mode , bool enable = true )
{
IntPtr consoleHandle = GetStdHandle ( STD_INPUT_HANDLE ) ;
if ( consoleHandle = = INVALID_HANDLE_VALUE )
{
return false ;
}
uint consoleMode ;
if ( ! GetConsoleMode ( consoleHandle , out consoleMode ) )
{
return false ;
}
if ( enable )
{
consoleMode | = ( uint ) mode ;
}
else
{
consoleMode & = ~ ( uint ) mode ;
}
return SetConsoleMode ( consoleHandle , consoleMode ) ;
}
const int STD_INPUT_HANDLE = - 10 ;
static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr ( - 1 ) ;
[Flags]
private enum ConsoleModes : uint
{
ENABLE_PROCESSED_INPUT = 0x0001 ,
ENABLE_LINE_INPUT = 0x0002 ,
ENABLE_ECHO_INPUT = 0x0004 ,
ENABLE_WINDOW_INPUT = 0x0008 ,
ENABLE_MOUSE_INPUT = 0x0010 ,
ENABLE_INSERT_MODE = 0x0020 ,
ENABLE_QUICK_EDIT_MODE = 0x0040 ,
ENABLE_EXTENDED_FLAGS = 0x0080 ,
ENABLE_AUTO_POSITION = 0x0100 ,
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 ,
ENABLE_PROCESSED_OUTPUT = 0x0001 ,
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002 ,
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 ,
DISABLE_NEWLINE_AUTO_RETURN = 0x0008 ,
ENABLE_LVB_GRID_WORLDWIDE = 0x0010
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetStdHandle ( int nStdHandle ) ;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetConsoleMode ( IntPtr hConsoleHandle , out uint lpMode ) ;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetConsoleMode ( IntPtr hConsoleHandle , uint dwMode ) ;
}
2020-01-05 23:47:38 +08:00
}
2020-12-21 19:08:44 +08:00
}