BililiveRecorder/BililiveRecorder.Cli/Program.cs

321 lines
12 KiB
C#
Raw Normal View History

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;
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Net;
2022-05-11 14:24:12 +08:00
using System.Text.RegularExpressions;
using System.Threading;
2021-05-30 19:16:20 +08:00
using System.Threading.Tasks;
using BililiveRecorder.Cli.Configure;
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;
2021-04-14 23:46:24 +08:00
using BililiveRecorder.ToolBox;
2021-05-30 19:16:20 +08:00
using BililiveRecorder.Web;
using Microsoft.AspNetCore.Hosting;
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;
2021-05-02 22:24:57 +08:00
using Serilog.Formatting.Compact;
namespace BililiveRecorder.Cli
{
2020-12-21 19:08:44 +08:00
internal class Program
{
2021-01-26 20:06:00 +08:00
private static int Main(string[] args)
2021-02-23 18:03:37 +08:00
{
var cmd_run = new Command("run", "Run BililiveRecorder in standard mode")
{
2021-05-30 19:16:20 +08:00
new Option<string?>(new []{ "--web-bind", "--bind", "-b" }, () => null, "Bind address for web api"),
2021-07-08 20:51:24 +08:00
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"),
2021-02-23 18:03:37 +08:00
new Argument<string>("path"),
};
cmd_run.AddAlias("r");
2021-05-30 19:16:20 +08:00
cmd_run.Handler = CommandHandler.Create<RunModeArguments>(RunConfigModeAsync);
2021-02-23 18:03:37 +08:00
var cmd_portable = new Command("portable", "Run BililiveRecorder in config-less mode")
{
2022-05-11 14:24:12 +08:00
new Option<string?>(new []{ "--web-bind", "--bind", "-b" }, () => null, "Bind address for web api"),
2021-07-08 20:51:24 +08:00
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"),
2021-11-30 19:29:30 +08:00
new Option<RecordMode>(new []{ "--record-mode", "--mode" }, () => RecordMode.Standard, "Recording mode"),
2021-02-23 18:03:37 +08:00
new Option<string>(new []{ "--cookie", "-c" }, "Cookie string for api requests"),
2021-12-19 21:10:34 +08:00
new Option<string>(new []{ "--filename", "-f" }, "File name format"),
new Option<PortableModeArguments.PortableDanmakuMode>(new []{ "--danmaku", "-d" }, "Flags for danmaku recording"),
2021-08-28 14:26:23 +08:00
new Option<string>("--webhook-url", "URL of webhoook"),
2021-02-23 18:03:37 +08:00
new Option<string>("--live-api-host"),
2021-04-09 15:56:36 +08:00
new Argument<string>("output-path"),
2022-05-11 14:24:12 +08:00
new Argument<int[]>("room-ids", () => Array.Empty<int>())
2021-02-23 18:03:37 +08:00
};
cmd_portable.AddAlias("p");
2022-05-11 14:24:12 +08:00
cmd_portable.Handler = CommandHandler.Create<PortableModeArguments>(RunPortableModeAsync);
2021-02-23 18:03:37 +08:00
var root = new RootCommand("A Stream Recorder For Bilibili Live")
{
cmd_run,
2021-04-14 23:46:24 +08:00
cmd_portable,
new ConfigureCommand(),
2021-04-14 23:46:24 +08:00
new ToolCommand()
2021-02-23 18:03:37 +08:00
};
2021-02-23 18:03:37 +08:00
return root.Invoke(args);
}
2021-05-30 19:16:20 +08:00
private static async Task<int> RunConfigModeAsync(RunModeArguments args)
{
2022-05-16 18:27:00 +08:00
var path = Path.GetFullPath(args.Path);
2021-05-30 19:16:20 +08:00
using var logger = BuildLogger(args.LogLevel, args.LogFileLevel);
2021-02-23 18:03:37 +08:00
Log.Logger = logger;
path = Path.GetFullPath(path);
var config = ConfigParser.LoadFrom(path);
if (config is null)
{
logger.Error("Initialize Error");
return -1;
}
config.Global.WorkDirectory = path;
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
return await RunRecorderAsync(serviceProvider, args.WebBind);
}
private static async Task<int> RunPortableModeAsync(PortableModeArguments args)
{
using var logger = BuildLogger(args.LogLevel, args.LogFileLevel);
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);
return await RunRecorderAsync(serviceProvider, args.WebBind);
}
private static async Task<int> RunRecorderAsync(IServiceProvider serviceProvider, string? webBind)
{
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-05-11 14:24:12 +08:00
if (webBind is null)
2021-05-30 19:16:20 +08:00
{
logger.Information("Web API not enabled");
}
else
{
2022-05-11 14:24:12 +08:00
var bind = FixBindUrl(webBind, logger);
logger.Information("Creating web server on {BindAddress}", bind);
2021-05-30 19:16:20 +08:00
host = new HostBuilder()
2022-05-17 01:50:09 +08:00
.UseSerilog(logger: logger)
2021-05-30 19:16:20 +08:00
.ConfigureServices(services =>
{
services.AddSingleton(recorderAccessProxy);
})
.ConfigureWebHost(webBuilder =>
{
webBuilder
2022-05-11 14:24:12 +08:00
.UseUrls(urls: bind)
.UseKestrel(option =>
{
})
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;
}
2022-05-11 14:24:12 +08:00
private static string FixBindUrl(string bind, ILogger logger)
2020-06-21 21:26:51 +08:00
{
2022-05-11 14:24:12 +08:00
if (Regex.IsMatch(bind, @"^\d+$"))
2021-04-14 23:46:24 +08:00
{
2022-05-11 14:24:12 +08:00
var result = "http://localhost:" + bind;
logger.Warning("标准的参数格式为 {Example} 而不是只有端口号,已自动修正为 {BindUrl}", @"http://{接口地址}:{端口号}", result);
return result;
}
else
if (Regex.IsMatch(bind, @"https?:"))
{
2022-05-11 14:24:12 +08:00
return bind;
}
2022-05-11 14:24:12 +08:00
else
2021-01-04 16:24:36 +08:00
{
2022-05-11 14:24:12 +08:00
var result = "http://" + bind;
logger.Warning("标准的参数格式为 {Example} 而不是只有 IP 和端口号,已自动修正为 {BindUrl}", @"http://{接口地址}:{端口号}", result);
return result;
}
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();
2021-07-08 20:51:24 +08:00
private static Logger BuildLogger(LogEventLevel logLevel, LogEventLevel logFileLevel) => new LoggerConfiguration()
2021-02-23 18:03:37 +08:00
.MinimumLevel.Verbose()
.Enrich.WithProcessId()
.Enrich.WithThreadId()
.Enrich.WithThreadName()
.Enrich.FromLogContext()
.Enrich.WithExceptionDetails()
.Destructure.AsScalar<IPAddress>()
2021-05-02 22:24:57 +08:00
.Destructure.ByTransforming<Flv.Xml.XmlFlvFile.XmlFlvFileMeta>(x => new
{
x.Version,
x.ExportTime,
x.FileSize,
x.FileCreationTime,
x.FileModificationTime,
})
2021-05-25 22:02:54 +08:00
.WriteTo.Console(restrictedToMinimumLevel: logLevel, outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{RoomId}] {Message:lj}{NewLine}{Exception}")
2021-07-08 20:51:24 +08:00
.WriteTo.File(new CompactJsonFormatter(), "./logs/bilirec.txt", restrictedToMinimumLevel: logFileLevel, shared: true, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true)
2021-02-23 18:03:37 +08:00
.CreateLogger();
2021-05-30 19:16:20 +08:00
public class RunModeArguments
{
public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information;
public LogEventLevel LogFileLevel { get; set; } = LogEventLevel.Information;
public string? WebBind { get; set; } = null;
public string Path { get; set; } = string.Empty;
}
2021-02-23 18:03:37 +08:00
public class PortableModeArguments
2020-12-21 04:13:49 +08:00
{
2021-05-25 22:02:54 +08:00
public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information;
2021-07-08 20:51:24 +08:00
public LogEventLevel LogFileLevel { get; set; } = LogEventLevel.Debug;
2022-05-11 14:24:12 +08:00
public string? WebBind { get; set; } = null;
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; }
public PortableDanmakuMode Danmaku { get; set; }
2021-02-23 18:03:37 +08:00
public IEnumerable<int> RoomIds { get; set; } = Enumerable.Empty<int>();
[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
}
}
2020-12-21 19:08:44 +08:00
}