From db7f2872f80819f8b9d9a4ee3fbbdd1690c73346 Mon Sep 17 00:00:00 2001 From: Genteure Date: Sun, 30 May 2021 19:16:20 +0800 Subject: [PATCH] Add Web API --- .../BililiveRecorder.Cli.csproj | 3 + BililiveRecorder.Cli/Program.cs | 79 +++++- BililiveRecorder.Core/IRecorder.cs | 4 +- BililiveRecorder.Core/Recorder.cs | 10 +- .../BililiveRecorder.Web.Schemas.csproj | 22 ++ .../RecorderMutation.cs | 229 ++++++++++++++++++ BililiveRecorder.Web.Schemas/RecorderQuery.cs | 49 ++++ .../RecorderSchema.cs | 16 ++ .../RecorderSubscription.cs | 8 + .../Types/Config.gen.cs | 214 ++++++++++++++++ .../Types/CuttingModeEnum.cs | 9 + .../Types/HierarchicalOptionalInputType.cs | 19 ++ .../Types/HierarchicalOptionalType.cs | 19 ++ .../Types/RecordModeEnum.cs | 9 + .../Types/RecordingStatsType.cs | 19 ++ .../Types/RoomType.cs | 24 ++ .../BililiveRecorder.Web.csproj | 15 ++ BililiveRecorder.Web/FakeRecorderForWeb.cs | 65 +++++ BililiveRecorder.Web/Program.cs | 23 ++ .../Properties/launchSettings.json | 28 +++ BililiveRecorder.Web/Startup.cs | 82 +++++++ .../appsettings.Development.json | 9 + BililiveRecorder.Web/appsettings.json | 10 + BililiveRecorder.sln | 22 +- config_gen/data.ts | 2 +- config_gen/generators/code.ts | 2 +- config_gen/generators/codeWeb.ts | 122 +++++++++- config_gen/generators/index.ts | 10 - config_gen/index.ts | 9 - config_gen/package-lock.json | 14 +- config_gen/package.json | 5 +- config_gen/types.ts | 2 + 32 files changed, 1102 insertions(+), 51 deletions(-) create mode 100644 BililiveRecorder.Web.Schemas/BililiveRecorder.Web.Schemas.csproj create mode 100644 BililiveRecorder.Web.Schemas/RecorderMutation.cs create mode 100644 BililiveRecorder.Web.Schemas/RecorderQuery.cs create mode 100644 BililiveRecorder.Web.Schemas/RecorderSchema.cs create mode 100644 BililiveRecorder.Web.Schemas/RecorderSubscription.cs create mode 100644 BililiveRecorder.Web.Schemas/Types/Config.gen.cs create mode 100644 BililiveRecorder.Web.Schemas/Types/CuttingModeEnum.cs create mode 100644 BililiveRecorder.Web.Schemas/Types/HierarchicalOptionalInputType.cs create mode 100644 BililiveRecorder.Web.Schemas/Types/HierarchicalOptionalType.cs create mode 100644 BililiveRecorder.Web.Schemas/Types/RecordModeEnum.cs create mode 100644 BililiveRecorder.Web.Schemas/Types/RecordingStatsType.cs create mode 100644 BililiveRecorder.Web.Schemas/Types/RoomType.cs create mode 100644 BililiveRecorder.Web/BililiveRecorder.Web.csproj create mode 100644 BililiveRecorder.Web/FakeRecorderForWeb.cs create mode 100644 BililiveRecorder.Web/Program.cs create mode 100644 BililiveRecorder.Web/Properties/launchSettings.json create mode 100644 BililiveRecorder.Web/Startup.cs create mode 100644 BililiveRecorder.Web/appsettings.Development.json create mode 100644 BililiveRecorder.Web/appsettings.json diff --git a/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj b/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj index 3a253d1..669a46f 100644 --- a/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj +++ b/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj @@ -4,6 +4,7 @@ Exe net6.0 BililiveRecorder.Cli.Program + false win-x64;osx-x64;osx.10.11-x64;linux-arm64;linux-arm;linux-x64 publish\any @@ -22,6 +23,7 @@ + @@ -35,6 +37,7 @@ + diff --git a/BililiveRecorder.Cli/Program.cs b/BililiveRecorder.Cli/Program.cs index fb49e87..f27454d 100644 --- a/BililiveRecorder.Cli/Program.cs +++ b/BililiveRecorder.Cli/Program.cs @@ -6,12 +6,16 @@ using System.IO; using System.Linq; using System.Threading; using BililiveRecorder.Cli.Configure; +using System.Threading.Tasks; using BililiveRecorder.Core; using BililiveRecorder.Core.Config; using BililiveRecorder.Core.Config.V2; using BililiveRecorder.DependencyInjection; using BililiveRecorder.ToolBox; +using BililiveRecorder.Web; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Core; using Serilog.Events; @@ -26,12 +30,13 @@ namespace BililiveRecorder.Cli { var cmd_run = new Command("run", "Run BililiveRecorder in standard mode") { + new Option(new []{ "--web-bind", "--bind", "-b" }, () => null, "Bind address for web api"), new Option(new []{ "--loglevel", "--log", "-l" }, () => LogEventLevel.Information, "Minimal log level output to console"), new Option(new []{ "--logfilelevel", "--flog" }, () => LogEventLevel.Debug, "Minimal log level output to file"), new Argument("path"), }; cmd_run.AddAlias("r"); - cmd_run.Handler = CommandHandler.Create(RunConfigMode); + cmd_run.Handler = CommandHandler.Create(RunConfigModeAsync); var cmd_portable = new Command("portable", "Run BililiveRecorder in config-less mode") { @@ -60,9 +65,11 @@ namespace BililiveRecorder.Cli return root.Invoke(args); } - private static int RunConfigMode(string path, LogEventLevel logLevel, LogEventLevel logFileLevel) + private static async Task RunConfigModeAsync(RunModeArguments args) { - using var logger = BuildLogger(logLevel, logFileLevel); + var path = args.Path; + + using var logger = BuildLogger(args.LogLevel, args.LogFileLevel); Log.Logger = logger; path = Path.GetFullPath(path); @@ -76,22 +83,64 @@ namespace BililiveRecorder.Cli config.Global.WorkDirectory = path; var serviceProvider = BuildServiceProvider(config, logger); + IRecorder recorderAccessProxy(IServiceProvider x) => serviceProvider.GetRequiredService(); + + // recorder setup done + // check if web service required + IHost? host = null; + if (string.IsNullOrWhiteSpace(args.WebBind)) + { + logger.Information("Web API not enabled"); + } + else + { + logger.Information("Creating web server on {BindAddress}", args.WebBind); + + host = new HostBuilder() + .ConfigureServices(services => + { + services.AddSingleton(recorderAccessProxy); + }) + .ConfigureWebHost(webBuilder => + { + webBuilder + .UseUrls(urls: args.WebBind) + .UseKestrel() + .UseSerilog(logger: logger) + .UseStartup(); + }) + .Build(); + } + + // make sure recorder is created, also used in cleanup ( .Dispose() ) var recorder = serviceProvider.GetRequiredService(); ConsoleCancelEventHandler p = null!; - var semaphore = new SemaphoreSlim(0, 1); + var cts = new CancellationTokenSource(); p = (sender, e) => { + logger.Information("Ctrl+C pressed. Exiting"); Console.CancelKeyPress -= p; e.Cancel = true; - recorder.Dispose(); - semaphore.Release(); + cts.Cancel(); }; Console.CancelKeyPress += p; - semaphore.Wait(); - Thread.Sleep(1000 * 2); - Console.ReadLine(); + var token = cts.Token; + if (host is not null) + { + var hostStartTask = host.RunAsync(token); + await Task.WhenAny(Task.Delay(-1, token), hostStartTask).ConfigureAwait(false); + await host.StopAsync().ConfigureAwait(false); + } + else + { + await Task.Delay(-1, token).ConfigureAwait(false); + } + + recorder.Dispose(); + + await Task.Delay(1000 * 3).ConfigureAwait(false); return 0; } @@ -140,6 +189,7 @@ namespace BililiveRecorder.Cli var semaphore = new SemaphoreSlim(0, 1); p = (sender, e) => { + logger.Information("Ctrl+C pressed. Exiting"); Console.CancelKeyPress -= p; e.Cancel = true; recorder.Dispose(); @@ -179,6 +229,17 @@ namespace BililiveRecorder.Cli .WriteTo.File(new CompactJsonFormatter(), "./logs/bilirec.txt", restrictedToMinimumLevel: logFileLevel, shared: true, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true) .CreateLogger(); + 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; + } + public class PortableModeArguments { public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information; diff --git a/BililiveRecorder.Core/IRecorder.cs b/BililiveRecorder.Core/IRecorder.cs index b451424..b1b2c45 100644 --- a/BililiveRecorder.Core/IRecorder.cs +++ b/BililiveRecorder.Core/IRecorder.cs @@ -18,8 +18,8 @@ namespace BililiveRecorder.Core event EventHandler>? NetworkingStats; event EventHandler>? RecordingStats; - void AddRoom(int roomid); - void AddRoom(int roomid, bool enabled); + IRoom AddRoom(int roomid); + IRoom AddRoom(int roomid, bool enabled); void RemoveRoom(IRoom room); void SaveConfig(); diff --git a/BililiveRecorder.Core/Recorder.cs b/BililiveRecorder.Core/Recorder.cs index 0592976..22abf3a 100644 --- a/BililiveRecorder.Core/Recorder.cs +++ b/BililiveRecorder.Core/Recorder.cs @@ -58,20 +58,21 @@ namespace BililiveRecorder.Core public ReadOnlyObservableCollection Rooms { get; } - public void AddRoom(int roomid) => this.AddRoom(roomid, true); + public IRoom AddRoom(int roomid) => this.AddRoom(roomid, true); - public void AddRoom(int roomid, bool enabled) + public IRoom AddRoom(int roomid, bool enabled) { lock (this.lockObject) { this.logger.Debug("AddRoom {RoomId}, AutoRecord: {AutoRecord}", roomid, enabled); var roomConfig = new RoomConfig { RoomId = roomid, AutoRecord = enabled }; - this.AddRoom(roomConfig, 0); + var room = this.AddRoom(roomConfig, 0); this.SaveConfig(); + return room; } } - private void AddRoom(RoomConfig roomConfig, int initDelayFactor) + private IRoom AddRoom(RoomConfig roomConfig, int initDelayFactor) { roomConfig.SetParent(this.Config.Global); var room = this.roomFactory.CreateRoom(roomConfig, initDelayFactor); @@ -85,6 +86,7 @@ namespace BililiveRecorder.Core room.PropertyChanged += this.Room_PropertyChanged; this.roomCollection.Add(room); + return room; } public void RemoveRoom(IRoom room) diff --git a/BililiveRecorder.Web.Schemas/BililiveRecorder.Web.Schemas.csproj b/BililiveRecorder.Web.Schemas/BililiveRecorder.Web.Schemas.csproj new file mode 100644 index 0000000..c6d2297 --- /dev/null +++ b/BililiveRecorder.Web.Schemas/BililiveRecorder.Web.Schemas.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + diff --git a/BililiveRecorder.Web.Schemas/RecorderMutation.cs b/BililiveRecorder.Web.Schemas/RecorderMutation.cs new file mode 100644 index 0000000..1815cbf --- /dev/null +++ b/BililiveRecorder.Web.Schemas/RecorderMutation.cs @@ -0,0 +1,229 @@ +using System; +using System.Linq; +using BililiveRecorder.Core; +using BililiveRecorder.Web.Schemas.Types; +using GraphQL; +using GraphQL.Types; + +namespace BililiveRecorder.Web.Schemas +{ + internal class RecorderMutation : ObjectGraphType + { + private readonly IRecorder recorder; + + public RecorderMutation(IRecorder recorder) + { + this.recorder = recorder ?? throw new ArgumentNullException(nameof(recorder)); + + this.SetupFields(); + } + + private void SetupFields() + { + this.Field("addRoom", + arguments: new QueryArguments( + new QueryArgument { Name = "roomId" }, + new QueryArgument { Name = "autoRecord" } + ), + resolve: context => + { + var roomid = context.GetArgument("roomId"); + var enabled = context.GetArgument("autoRecord"); + + var room = this.recorder.Rooms.FirstOrDefault(x => x.RoomConfig.RoomId == roomid || x.ShortId == roomid); + + if (room is not null) + { + context.Errors.Add(new ExecutionError("Room already exist.") + { + Code = "BREC_ROOM_DUPLICATE" + }); + return null; + } + else + { + return this.recorder.AddRoom(roomid, enabled); + } + }); + + this.Field("removeRoom", + arguments: new QueryArguments( + new QueryArgument { Name = "objectId" }, + new QueryArgument { Name = "roomId" } + ), + resolve: context => + { + var objectId = context.GetArgument("objectId"); + var roomid = context.GetArgument("roomId"); + + var room = objectId != default + ? this.recorder.Rooms.FirstOrDefault(x => x.ObjectId == objectId) + : this.recorder.Rooms.FirstOrDefault(x => x.RoomConfig.RoomId == roomid || x.ShortId == roomid); + + if (room is not null) + this.recorder.RemoveRoom(room); + else + context.Errors.Add(new ExecutionError("Room not found") + { + Code = "BREC_ROOM_NOT_FOUND" + }); + + return room; + }); + + this.FieldAsync("refreshRoomInfo", + arguments: new QueryArguments( + new QueryArgument { Name = "objectId" }, + new QueryArgument { Name = "roomId" } + ), + resolve: async context => + { + var objectId = context.GetArgument("objectId"); + var roomid = context.GetArgument("roomId"); + + var room = objectId != default + ? this.recorder.Rooms.FirstOrDefault(x => x.ObjectId == objectId) + : this.recorder.Rooms.FirstOrDefault(x => x.RoomConfig.RoomId == roomid || x.ShortId == roomid); + + if (room is not null) + await room.RefreshRoomInfoAsync().ConfigureAwait(false); + else + context.Errors.Add(new ExecutionError("Room not found") + { + Code = "BREC_ROOM_NOT_FOUND" + }); + + return room; + }); + + this.Field("startRecording", + arguments: new QueryArguments( + new QueryArgument { Name = "objectId" }, + new QueryArgument { Name = "roomId" } + ), + resolve: context => + { + var objectId = context.GetArgument("objectId"); + var roomid = context.GetArgument("roomId"); + + var room = objectId != default + ? this.recorder.Rooms.FirstOrDefault(x => x.ObjectId == objectId) + : this.recorder.Rooms.FirstOrDefault(x => x.RoomConfig.RoomId == roomid || x.ShortId == roomid); + + if (room is not null) + room.StartRecord(); + else + context.Errors.Add(new ExecutionError("Room not found") + { + Code = "BREC_ROOM_NOT_FOUND" + }); + + return room; + }); + + this.Field("stopRecording", + arguments: new QueryArguments( + new QueryArgument { Name = "objectId" }, + new QueryArgument { Name = "roomId" } + ), + resolve: context => + { + var objectId = context.GetArgument("objectId"); + var roomid = context.GetArgument("roomId"); + + var room = objectId != default + ? this.recorder.Rooms.FirstOrDefault(x => x.ObjectId == objectId) + : this.recorder.Rooms.FirstOrDefault(x => x.RoomConfig.RoomId == roomid || x.ShortId == roomid); + + if (room is not null) + room.StopRecord(); + else + context.Errors.Add(new ExecutionError("Room not found") + { + Code = "BREC_ROOM_NOT_FOUND" + }); + + return room; + }); + + this.Field("splitRecordingOutput", + arguments: new QueryArguments( + new QueryArgument { Name = "objectId" }, + new QueryArgument { Name = "roomId" } + ), + resolve: context => + { + var objectId = context.GetArgument("objectId"); + var roomid = context.GetArgument("roomId"); + + var room = objectId != default + ? this.recorder.Rooms.FirstOrDefault(x => x.ObjectId == objectId) + : this.recorder.Rooms.FirstOrDefault(x => x.RoomConfig.RoomId == roomid || x.ShortId == roomid); + + if (room is not null) + room.SplitOutput(); + else + context.Errors.Add(new ExecutionError("Room not found") + { + Code = "BREC_ROOM_NOT_FOUND" + }); + + return room; + }); + + this.Field("setRoomConfig", + arguments: new QueryArguments( + new QueryArgument { Name = "objectId" }, + new QueryArgument { Name = "roomId" }, + new QueryArgument { Name = "config" } + ), + resolve: context => + { + var objectId = context.GetArgument("objectId"); + var roomid = context.GetArgument("roomId"); + var config = context.GetArgument("config"); + + if (config is null) + { + context.Errors.Add(new ExecutionError("config can't be null")); + return null; + } + + var room = objectId != default + ? this.recorder.Rooms.FirstOrDefault(x => x.ObjectId == objectId) + : this.recorder.Rooms.FirstOrDefault(x => x.RoomConfig.RoomId == roomid || x.ShortId == roomid); + + if (room is null) + context.Errors.Add(new ExecutionError("Room not found") + { + Code = "BREC_ROOM_NOT_FOUND" + }); + else + config.ApplyTo(room.RoomConfig); + + return room; + }); + + this.Field("setConfig", + arguments: new QueryArguments( + new QueryArgument { Name = "config" } + ), + resolve: context => + { + var config = context.GetArgument("config"); + + if (config is null) + { + context.Errors.Add(new ExecutionError("config can't be null")); + return null; + } + + var recconfig = this.recorder.Config.Global; + + config.ApplyTo(recconfig); + + return recconfig; + }); + } + } +} diff --git a/BililiveRecorder.Web.Schemas/RecorderQuery.cs b/BililiveRecorder.Web.Schemas/RecorderQuery.cs new file mode 100644 index 0000000..1e26d54 --- /dev/null +++ b/BililiveRecorder.Web.Schemas/RecorderQuery.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using BililiveRecorder.Core; +using BililiveRecorder.Core.Config.V2; +using BililiveRecorder.Web.Schemas.Types; +using GraphQL; +using GraphQL.Types; + +namespace BililiveRecorder.Web.Schemas +{ + internal class RecorderQuery : ObjectGraphType + { + private readonly IRecorder recorder; + + public RecorderQuery(IRecorder recorder) + { + this.recorder = recorder ?? throw new ArgumentNullException(nameof(recorder)); + + this.SetupFields(); + } + + private void SetupFields() + { + this.Field("config", resolve: context => this.recorder.Config.Global); + + this.Field("defaultConfig", resolve: context => DefaultConfig.Instance); + + this.Field>("rooms", resolve: context => this.recorder.Rooms); + + this.Field("room", + arguments: new QueryArguments( + new QueryArgument { Name = "objectId" }, + new QueryArgument { Name = "roomId" } + ), + resolve: context => + { + var objectId = context.GetArgument("objectId"); + var roomid = context.GetArgument("roomId"); + + var room = objectId != default + ? this.recorder.Rooms.FirstOrDefault(x => x.ObjectId == objectId) + : this.recorder.Rooms.FirstOrDefault(x => x.RoomConfig.RoomId == roomid || x.ShortId == roomid); + + return room; + } + ); + } + } +} diff --git a/BililiveRecorder.Web.Schemas/RecorderSchema.cs b/BililiveRecorder.Web.Schemas/RecorderSchema.cs new file mode 100644 index 0000000..bcd632a --- /dev/null +++ b/BililiveRecorder.Web.Schemas/RecorderSchema.cs @@ -0,0 +1,16 @@ +using System; +using BililiveRecorder.Core; +using GraphQL.Types; + +namespace BililiveRecorder.Web.Schemas +{ + public class RecorderSchema : Schema + { + public RecorderSchema(IServiceProvider services, IRecorder recorder) : base(services) + { + this.Query = new RecorderQuery(recorder); + this.Mutation = new RecorderMutation(recorder); + //this.Subscription = new RecorderSubscription(); + } + } +} diff --git a/BililiveRecorder.Web.Schemas/RecorderSubscription.cs b/BililiveRecorder.Web.Schemas/RecorderSubscription.cs new file mode 100644 index 0000000..883fa50 --- /dev/null +++ b/BililiveRecorder.Web.Schemas/RecorderSubscription.cs @@ -0,0 +1,8 @@ +using GraphQL.Types; + +namespace BililiveRecorder.Web.Schemas +{ + internal class RecorderSubscription : ObjectGraphType + { + } +} diff --git a/BililiveRecorder.Web.Schemas/Types/Config.gen.cs b/BililiveRecorder.Web.Schemas/Types/Config.gen.cs new file mode 100644 index 0000000..15382b3 --- /dev/null +++ b/BililiveRecorder.Web.Schemas/Types/Config.gen.cs @@ -0,0 +1,214 @@ +// ****************************** +// GENERATED CODE, DO NOT EDIT MANUALLY. +// SEE /config_gen/README.md +// ****************************** + +using BililiveRecorder.Core.Config.V2; +using GraphQL.Types; +using HierarchicalPropertyDefault; +#nullable enable +namespace BililiveRecorder.Web.Schemas.Types +{ + internal class RoomConfigType : ObjectGraphType + { + public RoomConfigType() + { + this.Field(x => x.RoomId); + this.Field(x => x.AutoRecord); + this.Field(x => x.OptionalRecordMode, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalCuttingMode, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalCuttingNumber, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordDanmaku, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordDanmakuRaw, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordDanmakuSuperChat, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordDanmakuGift, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordDanmakuGuard, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordingQuality, type: typeof(HierarchicalOptionalType)); + } + } + + internal class GlobalConfigType : ObjectGraphType + { + public GlobalConfigType() + { + this.Field(x => x.OptionalRecordMode, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalCuttingMode, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalCuttingNumber, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordDanmaku, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordDanmakuRaw, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordDanmakuSuperChat, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordDanmakuGift, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordDanmakuGuard, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordingQuality, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordFilenameFormat, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalWebHookUrls, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalWebHookUrlsV2, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalWpfShowTitleAndArea, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalCookie, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalLiveApiHost, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalTimingCheckInterval, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalTimingStreamRetry, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalTimingStreamRetryNoQn, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalTimingStreamConnect, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalTimingDanmakuRetry, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalTimingWatchdogTimeout, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalRecordDanmakuFlushInterval, type: typeof(HierarchicalOptionalType)); + } + } + + internal class DefaultConfigType : ObjectGraphType + { + public DefaultConfigType() + { + this.Field(x => x.RecordMode); + this.Field(x => x.CuttingMode); + this.Field(x => x.CuttingNumber); + this.Field(x => x.RecordDanmaku); + this.Field(x => x.RecordDanmakuRaw); + this.Field(x => x.RecordDanmakuSuperChat); + this.Field(x => x.RecordDanmakuGift); + this.Field(x => x.RecordDanmakuGuard); + this.Field(x => x.RecordingQuality); + this.Field(x => x.RecordFilenameFormat); + this.Field(x => x.WebHookUrls); + this.Field(x => x.WebHookUrlsV2); + this.Field(x => x.WpfShowTitleAndArea); + this.Field(x => x.Cookie); + this.Field(x => x.LiveApiHost); + this.Field(x => x.TimingCheckInterval); + this.Field(x => x.TimingStreamRetry); + this.Field(x => x.TimingStreamRetryNoQn); + this.Field(x => x.TimingStreamConnect); + this.Field(x => x.TimingDanmakuRetry); + this.Field(x => x.TimingWatchdogTimeout); + this.Field(x => x.RecordDanmakuFlushInterval); + } + } + + internal class SetRoomConfig + { + public bool? AutoRecord { get; set; } + public Optional? OptionalRecordMode { get; set; } + public Optional? OptionalCuttingMode { get; set; } + public Optional? OptionalCuttingNumber { get; set; } + public Optional? OptionalRecordDanmaku { get; set; } + public Optional? OptionalRecordDanmakuRaw { get; set; } + public Optional? OptionalRecordDanmakuSuperChat { get; set; } + public Optional? OptionalRecordDanmakuGift { get; set; } + public Optional? OptionalRecordDanmakuGuard { get; set; } + public Optional? OptionalRecordingQuality { get; set; } + + public void ApplyTo(RoomConfig config) + { + if (this.AutoRecord.HasValue) config.AutoRecord = this.AutoRecord.Value; + if (this.OptionalRecordMode.HasValue) config.OptionalRecordMode = this.OptionalRecordMode.Value; + if (this.OptionalCuttingMode.HasValue) config.OptionalCuttingMode = this.OptionalCuttingMode.Value; + if (this.OptionalCuttingNumber.HasValue) config.OptionalCuttingNumber = this.OptionalCuttingNumber.Value; + if (this.OptionalRecordDanmaku.HasValue) config.OptionalRecordDanmaku = this.OptionalRecordDanmaku.Value; + if (this.OptionalRecordDanmakuRaw.HasValue) config.OptionalRecordDanmakuRaw = this.OptionalRecordDanmakuRaw.Value; + if (this.OptionalRecordDanmakuSuperChat.HasValue) config.OptionalRecordDanmakuSuperChat = this.OptionalRecordDanmakuSuperChat.Value; + if (this.OptionalRecordDanmakuGift.HasValue) config.OptionalRecordDanmakuGift = this.OptionalRecordDanmakuGift.Value; + if (this.OptionalRecordDanmakuGuard.HasValue) config.OptionalRecordDanmakuGuard = this.OptionalRecordDanmakuGuard.Value; + if (this.OptionalRecordingQuality.HasValue) config.OptionalRecordingQuality = this.OptionalRecordingQuality.Value; + } + } + + internal class SetRoomConfigType : InputObjectGraphType + { + public SetRoomConfigType() + { + this.Field(x => x.AutoRecord, nullable: true); + this.Field(x => x.OptionalRecordMode, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalCuttingMode, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalCuttingNumber, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordDanmaku, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordDanmakuRaw, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordDanmakuSuperChat, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordDanmakuGift, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordDanmakuGuard, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordingQuality, nullable: true, type: typeof(HierarchicalOptionalInputType)); + } + } + + internal class SetGlobalConfig + { + public Optional? OptionalRecordMode { get; set; } + public Optional? OptionalCuttingMode { get; set; } + public Optional? OptionalCuttingNumber { get; set; } + public Optional? OptionalRecordDanmaku { get; set; } + public Optional? OptionalRecordDanmakuRaw { get; set; } + public Optional? OptionalRecordDanmakuSuperChat { get; set; } + public Optional? OptionalRecordDanmakuGift { get; set; } + public Optional? OptionalRecordDanmakuGuard { get; set; } + public Optional? OptionalRecordingQuality { get; set; } + public Optional? OptionalRecordFilenameFormat { get; set; } + public Optional? OptionalWebHookUrls { get; set; } + public Optional? OptionalWebHookUrlsV2 { get; set; } + public Optional? OptionalWpfShowTitleAndArea { get; set; } + public Optional? OptionalCookie { get; set; } + public Optional? OptionalLiveApiHost { get; set; } + public Optional? OptionalTimingCheckInterval { get; set; } + public Optional? OptionalTimingStreamRetry { get; set; } + public Optional? OptionalTimingStreamRetryNoQn { get; set; } + public Optional? OptionalTimingStreamConnect { get; set; } + public Optional? OptionalTimingDanmakuRetry { get; set; } + public Optional? OptionalTimingWatchdogTimeout { get; set; } + public Optional? OptionalRecordDanmakuFlushInterval { get; set; } + + public void ApplyTo(GlobalConfig config) + { + if (this.OptionalRecordMode.HasValue) config.OptionalRecordMode = this.OptionalRecordMode.Value; + if (this.OptionalCuttingMode.HasValue) config.OptionalCuttingMode = this.OptionalCuttingMode.Value; + if (this.OptionalCuttingNumber.HasValue) config.OptionalCuttingNumber = this.OptionalCuttingNumber.Value; + if (this.OptionalRecordDanmaku.HasValue) config.OptionalRecordDanmaku = this.OptionalRecordDanmaku.Value; + if (this.OptionalRecordDanmakuRaw.HasValue) config.OptionalRecordDanmakuRaw = this.OptionalRecordDanmakuRaw.Value; + if (this.OptionalRecordDanmakuSuperChat.HasValue) config.OptionalRecordDanmakuSuperChat = this.OptionalRecordDanmakuSuperChat.Value; + if (this.OptionalRecordDanmakuGift.HasValue) config.OptionalRecordDanmakuGift = this.OptionalRecordDanmakuGift.Value; + if (this.OptionalRecordDanmakuGuard.HasValue) config.OptionalRecordDanmakuGuard = this.OptionalRecordDanmakuGuard.Value; + if (this.OptionalRecordingQuality.HasValue) config.OptionalRecordingQuality = this.OptionalRecordingQuality.Value; + if (this.OptionalRecordFilenameFormat.HasValue) config.OptionalRecordFilenameFormat = this.OptionalRecordFilenameFormat.Value; + if (this.OptionalWebHookUrls.HasValue) config.OptionalWebHookUrls = this.OptionalWebHookUrls.Value; + if (this.OptionalWebHookUrlsV2.HasValue) config.OptionalWebHookUrlsV2 = this.OptionalWebHookUrlsV2.Value; + if (this.OptionalWpfShowTitleAndArea.HasValue) config.OptionalWpfShowTitleAndArea = this.OptionalWpfShowTitleAndArea.Value; + if (this.OptionalCookie.HasValue) config.OptionalCookie = this.OptionalCookie.Value; + if (this.OptionalLiveApiHost.HasValue) config.OptionalLiveApiHost = this.OptionalLiveApiHost.Value; + if (this.OptionalTimingCheckInterval.HasValue) config.OptionalTimingCheckInterval = this.OptionalTimingCheckInterval.Value; + if (this.OptionalTimingStreamRetry.HasValue) config.OptionalTimingStreamRetry = this.OptionalTimingStreamRetry.Value; + if (this.OptionalTimingStreamRetryNoQn.HasValue) config.OptionalTimingStreamRetryNoQn = this.OptionalTimingStreamRetryNoQn.Value; + if (this.OptionalTimingStreamConnect.HasValue) config.OptionalTimingStreamConnect = this.OptionalTimingStreamConnect.Value; + if (this.OptionalTimingDanmakuRetry.HasValue) config.OptionalTimingDanmakuRetry = this.OptionalTimingDanmakuRetry.Value; + if (this.OptionalTimingWatchdogTimeout.HasValue) config.OptionalTimingWatchdogTimeout = this.OptionalTimingWatchdogTimeout.Value; + if (this.OptionalRecordDanmakuFlushInterval.HasValue) config.OptionalRecordDanmakuFlushInterval = this.OptionalRecordDanmakuFlushInterval.Value; + } + } + + internal class SetGlobalConfigType : InputObjectGraphType + { + public SetGlobalConfigType() + { + this.Field(x => x.OptionalRecordMode, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalCuttingMode, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalCuttingNumber, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordDanmaku, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordDanmakuRaw, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordDanmakuSuperChat, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordDanmakuGift, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordDanmakuGuard, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordingQuality, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordFilenameFormat, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalWebHookUrls, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalWebHookUrlsV2, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalWpfShowTitleAndArea, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalCookie, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalLiveApiHost, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalTimingCheckInterval, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalTimingStreamRetry, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalTimingStreamRetryNoQn, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalTimingStreamConnect, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalTimingDanmakuRetry, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalTimingWatchdogTimeout, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalRecordDanmakuFlushInterval, nullable: true, type: typeof(HierarchicalOptionalInputType)); + } + } + +} diff --git a/BililiveRecorder.Web.Schemas/Types/CuttingModeEnum.cs b/BililiveRecorder.Web.Schemas/Types/CuttingModeEnum.cs new file mode 100644 index 0000000..968f01d --- /dev/null +++ b/BililiveRecorder.Web.Schemas/Types/CuttingModeEnum.cs @@ -0,0 +1,9 @@ +using BililiveRecorder.Core.Config.V2; +using GraphQL.Types; + +namespace BililiveRecorder.Web.Schemas.Types +{ + public class CuttingModeEnum : EnumerationGraphType + { + } +} diff --git a/BililiveRecorder.Web.Schemas/Types/HierarchicalOptionalInputType.cs b/BililiveRecorder.Web.Schemas/Types/HierarchicalOptionalInputType.cs new file mode 100644 index 0000000..a2a5c71 --- /dev/null +++ b/BililiveRecorder.Web.Schemas/Types/HierarchicalOptionalInputType.cs @@ -0,0 +1,19 @@ +using GraphQL.Types; +using HierarchicalPropertyDefault; + +namespace BililiveRecorder.Web.Schemas.Types +{ + public class HierarchicalOptionalInputType : InputObjectGraphType> + { + public HierarchicalOptionalInputType() + { + this.Name = "HierarchicalOptionalInput_" + typeof(TValue).Name + "_Type"; + + this.Field(x => x.HasValue) + .Description("Use 'value' when 'hasValue' is true, or use the value from parent object when 'hasValue' is false."); + + this.Field(x => x.Value, nullable: typeof(TValue) == typeof(string)) + .Description("NOTE: The value of this field is ignored when 'hasValue' is false."); + } + } +} diff --git a/BililiveRecorder.Web.Schemas/Types/HierarchicalOptionalType.cs b/BililiveRecorder.Web.Schemas/Types/HierarchicalOptionalType.cs new file mode 100644 index 0000000..b20aa8c --- /dev/null +++ b/BililiveRecorder.Web.Schemas/Types/HierarchicalOptionalType.cs @@ -0,0 +1,19 @@ +using GraphQL.Types; +using HierarchicalPropertyDefault; + +namespace BililiveRecorder.Web.Schemas.Types +{ + public class HierarchicalOptionalType : ObjectGraphType> + { + public HierarchicalOptionalType() + { + this.Name = "HierarchicalOptional_" + typeof(TValue).Name + "_Type"; + + this.Field(x => x.HasValue) + .Description("Use 'value' when 'hasValue' is true, or use the value from parent object when 'hasValue' is false."); + + this.Field(x => x.Value, nullable: typeof(TValue) == typeof(string)) + .Description("NOTE: The value of this field is ignored when 'hasValue' is false."); + } + } +} diff --git a/BililiveRecorder.Web.Schemas/Types/RecordModeEnum.cs b/BililiveRecorder.Web.Schemas/Types/RecordModeEnum.cs new file mode 100644 index 0000000..eb0fc9e --- /dev/null +++ b/BililiveRecorder.Web.Schemas/Types/RecordModeEnum.cs @@ -0,0 +1,9 @@ +using BililiveRecorder.Core.Config.V2; +using GraphQL.Types; + +namespace BililiveRecorder.Web.Schemas.Types +{ + public class RecordModeEnum : EnumerationGraphType + { + } +} diff --git a/BililiveRecorder.Web.Schemas/Types/RecordingStatsType.cs b/BililiveRecorder.Web.Schemas/Types/RecordingStatsType.cs new file mode 100644 index 0000000..97c00f0 --- /dev/null +++ b/BililiveRecorder.Web.Schemas/Types/RecordingStatsType.cs @@ -0,0 +1,19 @@ +using BililiveRecorder.Core; +using GraphQL.Types; + +namespace BililiveRecorder.Web.Schemas.Types +{ + public class RecordingStatsType : ObjectGraphType + { + public RecordingStatsType() + { + this.Field(x => x.NetworkMbps); + this.Field(x => x.SessionDuration); + this.Field(x => x.FileMaxTimestamp); + this.Field(x => x.SessionMaxTimestamp); + this.Field(x => x.DurationRatio); + this.Field(x => x.TotalInputBytes); + this.Field(x => x.TotalOutputBytes); + } + } +} diff --git a/BililiveRecorder.Web.Schemas/Types/RoomType.cs b/BililiveRecorder.Web.Schemas/Types/RoomType.cs new file mode 100644 index 0000000..1f41285 --- /dev/null +++ b/BililiveRecorder.Web.Schemas/Types/RoomType.cs @@ -0,0 +1,24 @@ +using BililiveRecorder.Core; +using GraphQL.Types; + +namespace BililiveRecorder.Web.Schemas.Types +{ + internal class RoomType : ObjectGraphType + { + public RoomType() + { + this.Field(x => x.ObjectId); + this.Field(x => x.RoomConfig, type: typeof(RoomConfigType)); + this.Field(x => x.ShortId); + this.Field(x => x.Name); + this.Field(x => x.Title); + this.Field(x => x.AreaNameParent); + this.Field(x => x.AreaNameChild); + this.Field(x => x.Recording); + this.Field(x => x.Streaming); + this.Field(x => x.AutoRecordForThisSession); + this.Field(x => x.DanmakuConnected); + this.Field(x => x.Stats, type: typeof(RecordingStatsType)); + } + } +} diff --git a/BililiveRecorder.Web/BililiveRecorder.Web.csproj b/BililiveRecorder.Web/BililiveRecorder.Web.csproj new file mode 100644 index 0000000..d2f65b6 --- /dev/null +++ b/BililiveRecorder.Web/BililiveRecorder.Web.csproj @@ -0,0 +1,15 @@ + + + + net5.0 + + + + + + + + + + + diff --git a/BililiveRecorder.Web/FakeRecorderForWeb.cs b/BililiveRecorder.Web/FakeRecorderForWeb.cs new file mode 100644 index 0000000..13e665b --- /dev/null +++ b/BililiveRecorder.Web/FakeRecorderForWeb.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using BililiveRecorder.Core; +using BililiveRecorder.Core.Config.V2; +using BililiveRecorder.Core.Event; + +namespace BililiveRecorder.Web +{ + internal class FakeRecorderForWeb : IRecorder + { + private bool disposedValue; + + public ConfigV2 Config { get; } = new ConfigV2(); + + public ReadOnlyObservableCollection Rooms { get; } = new ReadOnlyObservableCollection(new ObservableCollection()); + + public event EventHandler>? RecordSessionStarted; + public event EventHandler>? RecordSessionEnded; + public event EventHandler>? RecordFileOpening; + public event EventHandler>? RecordFileClosed; + public event EventHandler>? NetworkingStats; + public event EventHandler>? RecordingStats; + public event PropertyChangedEventHandler? PropertyChanged; + + public IRoom AddRoom(int roomid) => null!; + + public IRoom AddRoom(int roomid, bool enabled) => null!; + + public void RemoveRoom(IRoom room) + { } + + public void SaveConfig() + { } + + protected virtual void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + this.disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~FakeRecorderForWeb() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/BililiveRecorder.Web/Program.cs b/BililiveRecorder.Web/Program.cs new file mode 100644 index 0000000..bd508c7 --- /dev/null +++ b/BililiveRecorder.Web/Program.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BililiveRecorder.Web +{ + public class Program + { + public static void Main(string[] args) => CreateHostBuilder(args).Build().Run(); + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/BililiveRecorder.Web/Properties/launchSettings.json b/BililiveRecorder.Web/Properties/launchSettings.json new file mode 100644 index 0000000..7da2471 --- /dev/null +++ b/BililiveRecorder.Web/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:9644", + "sslPort": 44313 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "BililiveRecorder.Web": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/BililiveRecorder.Web/Startup.cs b/BililiveRecorder.Web/Startup.cs new file mode 100644 index 0000000..9132ea5 --- /dev/null +++ b/BililiveRecorder.Web/Startup.cs @@ -0,0 +1,82 @@ +using System.Diagnostics; +using BililiveRecorder.Core; +using BililiveRecorder.Web.Schemas; +using GraphQL; +using GraphQL.Server; +using GraphQL.Types; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace BililiveRecorder.Web +{ + public class Startup + { + // TODO 减少引用的依赖数量 + + public Startup(IConfiguration configuration, IWebHostEnvironment environment) + { + this.Configuration = configuration; + this.Environment = environment; + } + + public IConfiguration Configuration { get; } + + public IWebHostEnvironment Environment { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.Configure(o => o.AllowSynchronousIO = true); + + services.TryAddSingleton(new FakeRecorderForWeb()); + + services + .AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader())) + .AddSingleton() + .AddSingleton(typeof(EnumerationGraphType<>)) + .AddSingleton() + .AddGraphQL((options, provider) => + { + options.EnableMetrics = this.Environment.IsDevelopment() || Debugger.IsAttached; + var logger = provider.GetRequiredService>(); + options.UnhandledExceptionDelegate = ctx => logger.LogWarning(ctx.OriginalException, "Unhandled GraphQL Exception"); + }) + //.AddSystemTextJson() + .AddNewtonsoftJson() + .AddWebSockets() + .AddGraphTypes(typeof(RecorderSchema)) + ; + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) => app + .UseCors() + .UseWebSockets() + .UseGraphQLWebSockets() + .UseGraphQL() + .UseGraphQLPlayground() + .UseGraphQLGraphiQL() + .UseGraphQLAltair() + .UseGraphQLVoyager() + .Use(next => async context => + { + if (context.Request.Path == "/") + { + await context.Response.WriteAsync(@"

to be done

PlaygroundGraphiQLAltairVoyager").ConfigureAwait(false); + } + else + { + context.Response.Redirect("/"); + } + }) + ; + } +} diff --git a/BililiveRecorder.Web/appsettings.Development.json b/BililiveRecorder.Web/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/BililiveRecorder.Web/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/BililiveRecorder.Web/appsettings.json b/BililiveRecorder.Web/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/BililiveRecorder.Web/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/BililiveRecorder.sln b/BililiveRecorder.sln index 02ab07d..bca5bc6 100644 --- a/BililiveRecorder.sln +++ b/BililiveRecorder.sln @@ -29,6 +29,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.ToolBox", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Flv.Tests", "test\BililiveRecorder.Flv.Tests\BililiveRecorder.Flv.Tests.csproj", "{32E554B1-0ECC-4145-85B8-3FC128FEBEA1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.Web", "BililiveRecorder.Web\BililiveRecorder.Web.csproj", "{263EC584-AFD5-45C9-8347-127016B3B8F5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.Web.Schemas", "BililiveRecorder.Web.Schemas\BililiveRecorder.Web.Schemas.csproj", "{4E72646D-8E25-49E5-B72C-E9749141DBF4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +67,14 @@ Global {32E554B1-0ECC-4145-85B8-3FC128FEBEA1}.Debug|Any CPU.Build.0 = Debug|Any CPU {32E554B1-0ECC-4145-85B8-3FC128FEBEA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {32E554B1-0ECC-4145-85B8-3FC128FEBEA1}.Release|Any CPU.Build.0 = Release|Any CPU + {263EC584-AFD5-45C9-8347-127016B3B8F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {263EC584-AFD5-45C9-8347-127016B3B8F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {263EC584-AFD5-45C9-8347-127016B3B8F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {263EC584-AFD5-45C9-8347-127016B3B8F5}.Release|Any CPU.Build.0 = Release|Any CPU + {4E72646D-8E25-49E5-B72C-E9749141DBF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E72646D-8E25-49E5-B72C-E9749141DBF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E72646D-8E25-49E5-B72C-E9749141DBF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E72646D-8E25-49E5-B72C-E9749141DBF4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -75,12 +87,14 @@ Global {521EC763-5694-45A8-B87F-6E6B7F2A3BD4} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1} {4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200} = {2D44A59D-E437-4FEE-8A2E-3FF00D53A64D} {32E554B1-0ECC-4145-85B8-3FC128FEBEA1} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1} + {263EC584-AFD5-45C9-8347-127016B3B8F5} = {2D44A59D-E437-4FEE-8A2E-3FF00D53A64D} + {4E72646D-8E25-49E5-B72C-E9749141DBF4} = {2D44A59D-E437-4FEE-8A2E-3FF00D53A64D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - RESX_SortFileContentOnSave = True - SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170} - RESX_NeutralResourcesLanguage = zh-Hans - RESX_SaveFilesImmediatelyUponChange = True RESX_ShowErrorsInErrorList = False + RESX_SaveFilesImmediatelyUponChange = True + RESX_NeutralResourcesLanguage = zh-Hans + SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170} + RESX_SortFileContentOnSave = True EndGlobalSection EndGlobal diff --git a/config_gen/data.ts b/config_gen/data.ts index be4554c..1c0da98 100644 --- a/config_gen/data.ts +++ b/config_gen/data.ts @@ -7,7 +7,7 @@ export const data: Array = [ type: "int", configType: "roomOnly", defaultValue: "default", - // web_readonly: true, + webReadonly: true, markdown: "" }, { diff --git a/config_gen/generators/code.ts b/config_gen/generators/code.ts index e0a1d24..51fc0d7 100644 --- a/config_gen/generators/code.ts +++ b/config_gen/generators/code.ts @@ -33,7 +33,7 @@ const map: SectionInfoMap = { header: true, build: builderCli }, - web_is_disabled_for_now: { + web: { path: './BililiveRecorder.Web.Schemas/Types/Config.gen.cs', format: true, header: true, diff --git a/config_gen/generators/codeWeb.ts b/config_gen/generators/codeWeb.ts index 6400d23..d0a0bde 100644 --- a/config_gen/generators/codeWeb.ts +++ b/config_gen/generators/codeWeb.ts @@ -1,5 +1,125 @@ import { ConfigEntry, ConfigEntryType } from "../types" +import { trimEnd } from "../utils"; export default function (data: ConfigEntry[]): string { - return "" + let result = `using BililiveRecorder.Core.Config.V2; +using GraphQL.Types; +using HierarchicalPropertyDefault; +#nullable enable +namespace BililiveRecorder.Web.Schemas.Types +{ +`; + function write_query_graphType_property(r: ConfigEntry) { + if (r.configType == "roomOnly") { + result += `this.Field(x => x.${r.name});\n`; + } else { + result += `this.Field(x => x.Optional${r.name}, type: typeof(HierarchicalOptionalType<${trimEnd(r.type, '?')}>));\n`; + } + } + + function write_mutation_graphType_property(r: ConfigEntry) { + if (r.configType == "roomOnly") { + result += `this.Field(x => x.${r.name}, nullable: true);\n`; + } else { + result += `this.Field(x => x.Optional${r.name}, nullable: true, type: typeof(HierarchicalOptionalInputType<${trimEnd(r.type, '?')}>));\n`; + } + } + + function write_mutation_dataType_property(r: ConfigEntry) { + if (r.configType == "roomOnly") { + result += `public ${r.type}? ${r.name} { get; set; }\n`; + } else { + result += `public Optional<${r.type}>? Optional${r.name} { get; set; }\n`; + } + } + + function write_mutation_apply_method(r: ConfigEntry) { + if (r.configType == "roomOnly") { + result += `if (this.${r.name}.HasValue) config.${r.name} = this.${r.name}.Value;\n`; + } else { + result += `if (this.Optional${r.name}.HasValue) config.Optional${r.name} = this.Optional${r.name}.Value;\n`; + } + } + + { // ====== RoomConfigType ====== + result += "internal class RoomConfigType : ObjectGraphType\n{\n"; + result += "public RoomConfigType()\n{\n" + + data.filter(r => r.configType != "globalOnly").forEach(r => write_query_graphType_property(r)); + + result += "}\n}\n\n"; + } + + { // ====== GlobalConfigType ====== + result += "internal class GlobalConfigType : ObjectGraphType\n{\n" + result += "public GlobalConfigType()\n{\n"; + + data.filter(r => r.configType != "roomOnly") + .forEach(r => write_query_graphType_property(r)); + + result += "}\n}\n\n"; + } + + { // ====== DefaultConfigType ====== + result += "internal class DefaultConfigType : ObjectGraphType\n{\n" + result += "public DefaultConfigType()\n{\n"; + + data.filter(r => r.configType != "roomOnly") + .forEach(r => { + result += `this.Field(x => x.${r.name});\n`; + }); + + result += "}\n}\n\n"; + } + + { // ====== SetRoomConfig ====== + result += "internal class SetRoomConfig\n{\n" + + data.filter(x => x.configType != "globalOnly" && !x.webReadonly) + .forEach(r => write_mutation_dataType_property(r)); + + result += "\npublic void ApplyTo(RoomConfig config)\n{\n"; + + data.filter(x => x.configType != "globalOnly" && !x.webReadonly) + .forEach(r => write_mutation_apply_method(r)); + + result += "}\n}\n\n"; + } + + { // ====== SetRoomConfigType ====== + result += "internal class SetRoomConfigType : InputObjectGraphType\n{\n" + result += "public SetRoomConfigType()\n{\n"; + + data.filter(x => x.configType != "globalOnly" && !x.webReadonly) + .forEach(r => write_mutation_graphType_property(r)); + + result += "}\n}\n\n"; + } + + { // ====== SetGlobalConfig ====== + result += "internal class SetGlobalConfig\n{\n" + + data.filter(r => r.configType != "roomOnly" && !r.webReadonly) + .forEach(r => write_mutation_dataType_property(r)); + + result += "\npublic void ApplyTo(GlobalConfig config)\n{\n"; + + data.filter(r => r.configType != "roomOnly" && !r.webReadonly) + .forEach(r => write_mutation_apply_method(r)); + + result += "}\n}\n\n"; + } + + { // ====== SetGlobalConfigType ====== + result += "internal class SetGlobalConfigType : InputObjectGraphType\n{\n" + result += "public SetGlobalConfigType()\n{\n"; + + data.filter(r => r.configType != "roomOnly" && !r.webReadonly) + .forEach(r => write_mutation_graphType_property(r)); + + result += "}\n}\n\n"; + } + + result += `}\n`; + return result; } diff --git a/config_gen/generators/index.ts b/config_gen/generators/index.ts index 11136b7..1e76b2c 100644 --- a/config_gen/generators/index.ts +++ b/config_gen/generators/index.ts @@ -1,12 +1,2 @@ -import { ConfigEntry } from "../types" export { default as code } from "./code" export { default as doc } from "./doc" - -export const core = function (data: Array) { -} -export const cli = function (data: Array) { -} -export const web = function (data: Array) { -} -export const schema = function (data: Array) { -} \ No newline at end of file diff --git a/config_gen/index.ts b/config_gen/index.ts index 3b25004..aa7ee52 100644 --- a/config_gen/index.ts +++ b/config_gen/index.ts @@ -1,14 +1,5 @@ -import { spawn } from "child_process"; -import { stdout, stderr } from "process"; -import { writeFileSync } from "fs"; -import { resolve, dirname } from "path"; -import { fileURLToPath } from 'url'; - -import { data } from "./data" import * as generators from "./generators" -const baseDirectory = __dirname - const argv = process.argv.slice(2) switch (argv[0]) { diff --git a/config_gen/package-lock.json b/config_gen/package-lock.json index faa3d26..703b3e1 100644 --- a/config_gen/package-lock.json +++ b/config_gen/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "devDependencies": { - "@types/node": "^16.6.1", + "@types/node": "^16.11.26", "ts-node": "^10.2.0", "tslib": "^2.3.1", "typescript": "^4.3.5" @@ -57,9 +57,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "16.6.1", - "resolved": "https://registry.nlark.com/@types/node/download/@types/node-16.6.1.tgz?cache=0&sync_timestamp=1628800447602&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-16.6.1.tgz", - "integrity": "sha1-ruYse5ZvVfxmx7bfodWNsqYW2mE=", + "version": "16.11.26", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-16.11.26.tgz", + "integrity": "sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==", "dev": true }, "node_modules/acorn": { @@ -224,9 +224,9 @@ "dev": true }, "@types/node": { - "version": "16.6.1", - "resolved": "https://registry.nlark.com/@types/node/download/@types/node-16.6.1.tgz?cache=0&sync_timestamp=1628800447602&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-16.6.1.tgz", - "integrity": "sha1-ruYse5ZvVfxmx7bfodWNsqYW2mE=", + "version": "16.11.26", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-16.11.26.tgz", + "integrity": "sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==", "dev": true }, "acorn": { diff --git a/config_gen/package.json b/config_gen/package.json index 9ed026f..16c874f 100644 --- a/config_gen/package.json +++ b/config_gen/package.json @@ -3,10 +3,9 @@ "build": "ts-node index.ts" }, "devDependencies": { - "@types/node": "^16.6.1", + "@types/node": "^16.11.26", "ts-node": "^10.2.0", "tslib": "^2.3.1", "typescript": "^4.3.5" - }, - "dependencies": {} + } } diff --git a/config_gen/types.ts b/config_gen/types.ts index 018bd5f..25dbf19 100644 --- a/config_gen/types.ts +++ b/config_gen/types.ts @@ -16,6 +16,8 @@ export interface ConfigEntry { readonly type: string, /** 设置类型 */ readonly configType: ConfigEntryType + /** Web API 只读属性 */ + readonly webReadonly?: boolean, /** 是否为高级设置(隐藏设置),默认为 false */ readonly advancedConfig?: boolean, /** 默认值 */