Add Web API

This commit is contained in:
Genteure 2021-05-30 19:16:20 +08:00 committed by genteure
parent 30a659f4d2
commit db7f2872f8
32 changed files with 1102 additions and 51 deletions

View File

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<StartupObject>BililiveRecorder.Cli.Program</StartupObject> <StartupObject>BililiveRecorder.Cli.Program</StartupObject>
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
<RuntimeIdentifiers>win-x64;osx-x64;osx.10.11-x64;linux-arm64;linux-arm;linux-x64</RuntimeIdentifiers> <RuntimeIdentifiers>win-x64;osx-x64;osx.10.11-x64;linux-arm64;linux-arm;linux-x64</RuntimeIdentifiers>
<RuntimeIdentifier Condition=" '$(RuntimeIdentifier)' == 'any' "></RuntimeIdentifier> <RuntimeIdentifier Condition=" '$(RuntimeIdentifier)' == 'any' "></RuntimeIdentifier>
<PublishDir Condition=" '$(RuntimeIdentifier)' == '' ">publish\any</PublishDir> <PublishDir Condition=" '$(RuntimeIdentifier)' == '' ">publish\any</PublishDir>
@ -22,6 +23,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Serilog" Version="2.10.0" /> <PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Process" Version="2.0.2" /> <PackageReference Include="Serilog.Enrichers.Process" Version="2.0.2" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.0.0" /> <PackageReference Include="Serilog.Exceptions" Version="8.0.0" />
@ -35,6 +37,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\BililiveRecorder.Core\BililiveRecorder.Core.csproj" /> <ProjectReference Include="..\BililiveRecorder.Core\BililiveRecorder.Core.csproj" />
<ProjectReference Include="..\BililiveRecorder.ToolBox\BililiveRecorder.ToolBox.csproj" /> <ProjectReference Include="..\BililiveRecorder.ToolBox\BililiveRecorder.ToolBox.csproj" />
<ProjectReference Include="..\BililiveRecorder.Web\BililiveRecorder.Web.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -6,12 +6,16 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using BililiveRecorder.Cli.Configure; using BililiveRecorder.Cli.Configure;
using System.Threading.Tasks;
using BililiveRecorder.Core; using BililiveRecorder.Core;
using BililiveRecorder.Core.Config; using BililiveRecorder.Core.Config;
using BililiveRecorder.Core.Config.V2; using BililiveRecorder.Core.Config.V2;
using BililiveRecorder.DependencyInjection; using BililiveRecorder.DependencyInjection;
using BililiveRecorder.ToolBox; using BililiveRecorder.ToolBox;
using BililiveRecorder.Web;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog; using Serilog;
using Serilog.Core; using Serilog.Core;
using Serilog.Events; using Serilog.Events;
@ -26,12 +30,13 @@ namespace BililiveRecorder.Cli
{ {
var cmd_run = new Command("run", "Run BililiveRecorder in standard mode") var cmd_run = new Command("run", "Run BililiveRecorder in standard mode")
{ {
new Option<string?>(new []{ "--web-bind", "--bind", "-b" }, () => null, "Bind address for web api"),
new Option<LogEventLevel>(new []{ "--loglevel", "--log", "-l" }, () => LogEventLevel.Information, "Minimal log level output to console"), 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<LogEventLevel>(new []{ "--logfilelevel", "--flog" }, () => LogEventLevel.Debug, "Minimal log level output to file"),
new Argument<string>("path"), new Argument<string>("path"),
}; };
cmd_run.AddAlias("r"); cmd_run.AddAlias("r");
cmd_run.Handler = CommandHandler.Create<string, LogEventLevel, LogEventLevel>(RunConfigMode); cmd_run.Handler = CommandHandler.Create<RunModeArguments>(RunConfigModeAsync);
var cmd_portable = new Command("portable", "Run BililiveRecorder in config-less mode") var cmd_portable = new Command("portable", "Run BililiveRecorder in config-less mode")
{ {
@ -60,9 +65,11 @@ namespace BililiveRecorder.Cli
return root.Invoke(args); return root.Invoke(args);
} }
private static int RunConfigMode(string path, LogEventLevel logLevel, LogEventLevel logFileLevel) private static async Task<int> RunConfigModeAsync(RunModeArguments args)
{ {
using var logger = BuildLogger(logLevel, logFileLevel); var path = args.Path;
using var logger = BuildLogger(args.LogLevel, args.LogFileLevel);
Log.Logger = logger; Log.Logger = logger;
path = Path.GetFullPath(path); path = Path.GetFullPath(path);
@ -76,22 +83,64 @@ namespace BililiveRecorder.Cli
config.Global.WorkDirectory = path; config.Global.WorkDirectory = path;
var serviceProvider = BuildServiceProvider(config, logger); var serviceProvider = BuildServiceProvider(config, logger);
IRecorder recorderAccessProxy(IServiceProvider x) => serviceProvider.GetRequiredService<IRecorder>();
// 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<Startup>();
})
.Build();
}
// make sure recorder is created, also used in cleanup ( .Dispose() )
var recorder = serviceProvider.GetRequiredService<IRecorder>(); var recorder = serviceProvider.GetRequiredService<IRecorder>();
ConsoleCancelEventHandler p = null!; ConsoleCancelEventHandler p = null!;
var semaphore = new SemaphoreSlim(0, 1); var cts = new CancellationTokenSource();
p = (sender, e) => p = (sender, e) =>
{ {
logger.Information("Ctrl+C pressed. Exiting");
Console.CancelKeyPress -= p; Console.CancelKeyPress -= p;
e.Cancel = true; e.Cancel = true;
recorder.Dispose(); cts.Cancel();
semaphore.Release();
}; };
Console.CancelKeyPress += p; Console.CancelKeyPress += p;
semaphore.Wait(); var token = cts.Token;
Thread.Sleep(1000 * 2); if (host is not null)
Console.ReadLine(); {
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; return 0;
} }
@ -140,6 +189,7 @@ namespace BililiveRecorder.Cli
var semaphore = new SemaphoreSlim(0, 1); var semaphore = new SemaphoreSlim(0, 1);
p = (sender, e) => p = (sender, e) =>
{ {
logger.Information("Ctrl+C pressed. Exiting");
Console.CancelKeyPress -= p; Console.CancelKeyPress -= p;
e.Cancel = true; e.Cancel = true;
recorder.Dispose(); 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) .WriteTo.File(new CompactJsonFormatter(), "./logs/bilirec.txt", restrictedToMinimumLevel: logFileLevel, shared: true, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true)
.CreateLogger(); .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 class PortableModeArguments
{ {
public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information; public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information;

View File

@ -18,8 +18,8 @@ namespace BililiveRecorder.Core
event EventHandler<AggregatedRoomEventArgs<NetworkingStatsEventArgs>>? NetworkingStats; event EventHandler<AggregatedRoomEventArgs<NetworkingStatsEventArgs>>? NetworkingStats;
event EventHandler<AggregatedRoomEventArgs<RecordingStatsEventArgs>>? RecordingStats; event EventHandler<AggregatedRoomEventArgs<RecordingStatsEventArgs>>? RecordingStats;
void AddRoom(int roomid); IRoom AddRoom(int roomid);
void AddRoom(int roomid, bool enabled); IRoom AddRoom(int roomid, bool enabled);
void RemoveRoom(IRoom room); void RemoveRoom(IRoom room);
void SaveConfig(); void SaveConfig();

View File

@ -58,20 +58,21 @@ namespace BililiveRecorder.Core
public ReadOnlyObservableCollection<IRoom> Rooms { get; } public ReadOnlyObservableCollection<IRoom> 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) lock (this.lockObject)
{ {
this.logger.Debug("AddRoom {RoomId}, AutoRecord: {AutoRecord}", roomid, enabled); this.logger.Debug("AddRoom {RoomId}, AutoRecord: {AutoRecord}", roomid, enabled);
var roomConfig = new RoomConfig { RoomId = roomid, AutoRecord = enabled }; var roomConfig = new RoomConfig { RoomId = roomid, AutoRecord = enabled };
this.AddRoom(roomConfig, 0); var room = this.AddRoom(roomConfig, 0);
this.SaveConfig(); this.SaveConfig();
return room;
} }
} }
private void AddRoom(RoomConfig roomConfig, int initDelayFactor) private IRoom AddRoom(RoomConfig roomConfig, int initDelayFactor)
{ {
roomConfig.SetParent(this.Config.Global); roomConfig.SetParent(this.Config.Global);
var room = this.roomFactory.CreateRoom(roomConfig, initDelayFactor); var room = this.roomFactory.CreateRoom(roomConfig, initDelayFactor);
@ -85,6 +86,7 @@ namespace BililiveRecorder.Core
room.PropertyChanged += this.Room_PropertyChanged; room.PropertyChanged += this.Room_PropertyChanged;
this.roomCollection.Add(room); this.roomCollection.Add(room);
return room;
} }
public void RemoveRoom(IRoom room) public void RemoveRoom(IRoom room)

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GraphQL.Server.Core" Version="5.0.2" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore.SystemTextJson" Version="5.0.2" />
<PackageReference Include="GraphQL.Server.Transports.Subscriptions.WebSockets" Version="5.0.2" />
<PackageReference Include="GraphQL.Server.Ui.Altair" Version="5.0.2" />
<PackageReference Include="GraphQL.Server.Ui.GraphiQL" Version="5.0.2" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="5.0.2" />
<PackageReference Include="GraphQL.Server.Ui.Voyager" Version="5.0.2" />
<PackageReference Include="GraphQL.SystemReactive" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BililiveRecorder.Core\BililiveRecorder.Core.csproj" />
</ItemGroup>
</Project>

View File

@ -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<RoomType>("addRoom",
arguments: new QueryArguments(
new QueryArgument<IntGraphType> { Name = "roomId" },
new QueryArgument<BooleanGraphType> { Name = "autoRecord" }
),
resolve: context =>
{
var roomid = context.GetArgument<int>("roomId");
var enabled = context.GetArgument<bool>("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<RoomType>("removeRoom",
arguments: new QueryArguments(
new QueryArgument<GuidGraphType> { Name = "objectId" },
new QueryArgument<IntGraphType> { Name = "roomId" }
),
resolve: context =>
{
var objectId = context.GetArgument<Guid>("objectId");
var roomid = context.GetArgument<int>("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<RoomType>("refreshRoomInfo",
arguments: new QueryArguments(
new QueryArgument<GuidGraphType> { Name = "objectId" },
new QueryArgument<IntGraphType> { Name = "roomId" }
),
resolve: async context =>
{
var objectId = context.GetArgument<Guid>("objectId");
var roomid = context.GetArgument<int>("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<RoomType>("startRecording",
arguments: new QueryArguments(
new QueryArgument<GuidGraphType> { Name = "objectId" },
new QueryArgument<IntGraphType> { Name = "roomId" }
),
resolve: context =>
{
var objectId = context.GetArgument<Guid>("objectId");
var roomid = context.GetArgument<int>("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<RoomType>("stopRecording",
arguments: new QueryArguments(
new QueryArgument<GuidGraphType> { Name = "objectId" },
new QueryArgument<IntGraphType> { Name = "roomId" }
),
resolve: context =>
{
var objectId = context.GetArgument<Guid>("objectId");
var roomid = context.GetArgument<int>("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<RoomType>("splitRecordingOutput",
arguments: new QueryArguments(
new QueryArgument<GuidGraphType> { Name = "objectId" },
new QueryArgument<IntGraphType> { Name = "roomId" }
),
resolve: context =>
{
var objectId = context.GetArgument<Guid>("objectId");
var roomid = context.GetArgument<int>("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<RoomType>("setRoomConfig",
arguments: new QueryArguments(
new QueryArgument<GuidGraphType> { Name = "objectId" },
new QueryArgument<IntGraphType> { Name = "roomId" },
new QueryArgument<SetRoomConfigType> { Name = "config" }
),
resolve: context =>
{
var objectId = context.GetArgument<Guid>("objectId");
var roomid = context.GetArgument<int>("roomId");
var config = context.GetArgument<SetRoomConfig>("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<GlobalConfigType>("setConfig",
arguments: new QueryArguments(
new QueryArgument<SetGlobalConfigType> { Name = "config" }
),
resolve: context =>
{
var config = context.GetArgument<SetGlobalConfig>("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;
});
}
}
}

View File

@ -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<GlobalConfigType>("config", resolve: context => this.recorder.Config.Global);
this.Field<DefaultConfigType>("defaultConfig", resolve: context => DefaultConfig.Instance);
this.Field<ListGraphType<RoomType>>("rooms", resolve: context => this.recorder.Rooms);
this.Field<RoomType>("room",
arguments: new QueryArguments(
new QueryArgument<IdGraphType> { Name = "objectId" },
new QueryArgument<IntGraphType> { Name = "roomId" }
),
resolve: context =>
{
var objectId = context.GetArgument<Guid>("objectId");
var roomid = context.GetArgument<int>("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;
}
);
}
}
}

View File

@ -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();
}
}
}

View File

@ -0,0 +1,8 @@
using GraphQL.Types;
namespace BililiveRecorder.Web.Schemas
{
internal class RecorderSubscription : ObjectGraphType
{
}
}

View File

@ -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<RoomConfig>
{
public RoomConfigType()
{
this.Field(x => x.RoomId);
this.Field(x => x.AutoRecord);
this.Field(x => x.OptionalRecordMode, type: typeof(HierarchicalOptionalType<RecordMode>));
this.Field(x => x.OptionalCuttingMode, type: typeof(HierarchicalOptionalType<CuttingMode>));
this.Field(x => x.OptionalCuttingNumber, type: typeof(HierarchicalOptionalType<uint>));
this.Field(x => x.OptionalRecordDanmaku, type: typeof(HierarchicalOptionalType<bool>));
this.Field(x => x.OptionalRecordDanmakuRaw, type: typeof(HierarchicalOptionalType<bool>));
this.Field(x => x.OptionalRecordDanmakuSuperChat, type: typeof(HierarchicalOptionalType<bool>));
this.Field(x => x.OptionalRecordDanmakuGift, type: typeof(HierarchicalOptionalType<bool>));
this.Field(x => x.OptionalRecordDanmakuGuard, type: typeof(HierarchicalOptionalType<bool>));
this.Field(x => x.OptionalRecordingQuality, type: typeof(HierarchicalOptionalType<string>));
}
}
internal class GlobalConfigType : ObjectGraphType<GlobalConfig>
{
public GlobalConfigType()
{
this.Field(x => x.OptionalRecordMode, type: typeof(HierarchicalOptionalType<RecordMode>));
this.Field(x => x.OptionalCuttingMode, type: typeof(HierarchicalOptionalType<CuttingMode>));
this.Field(x => x.OptionalCuttingNumber, type: typeof(HierarchicalOptionalType<uint>));
this.Field(x => x.OptionalRecordDanmaku, type: typeof(HierarchicalOptionalType<bool>));
this.Field(x => x.OptionalRecordDanmakuRaw, type: typeof(HierarchicalOptionalType<bool>));
this.Field(x => x.OptionalRecordDanmakuSuperChat, type: typeof(HierarchicalOptionalType<bool>));
this.Field(x => x.OptionalRecordDanmakuGift, type: typeof(HierarchicalOptionalType<bool>));
this.Field(x => x.OptionalRecordDanmakuGuard, type: typeof(HierarchicalOptionalType<bool>));
this.Field(x => x.OptionalRecordingQuality, type: typeof(HierarchicalOptionalType<string>));
this.Field(x => x.OptionalRecordFilenameFormat, type: typeof(HierarchicalOptionalType<string>));
this.Field(x => x.OptionalWebHookUrls, type: typeof(HierarchicalOptionalType<string>));
this.Field(x => x.OptionalWebHookUrlsV2, type: typeof(HierarchicalOptionalType<string>));
this.Field(x => x.OptionalWpfShowTitleAndArea, type: typeof(HierarchicalOptionalType<bool>));
this.Field(x => x.OptionalCookie, type: typeof(HierarchicalOptionalType<string>));
this.Field(x => x.OptionalLiveApiHost, type: typeof(HierarchicalOptionalType<string>));
this.Field(x => x.OptionalTimingCheckInterval, type: typeof(HierarchicalOptionalType<uint>));
this.Field(x => x.OptionalTimingStreamRetry, type: typeof(HierarchicalOptionalType<uint>));
this.Field(x => x.OptionalTimingStreamRetryNoQn, type: typeof(HierarchicalOptionalType<uint>));
this.Field(x => x.OptionalTimingStreamConnect, type: typeof(HierarchicalOptionalType<uint>));
this.Field(x => x.OptionalTimingDanmakuRetry, type: typeof(HierarchicalOptionalType<uint>));
this.Field(x => x.OptionalTimingWatchdogTimeout, type: typeof(HierarchicalOptionalType<uint>));
this.Field(x => x.OptionalRecordDanmakuFlushInterval, type: typeof(HierarchicalOptionalType<uint>));
}
}
internal class DefaultConfigType : ObjectGraphType<DefaultConfig>
{
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<RecordMode>? OptionalRecordMode { get; set; }
public Optional<CuttingMode>? OptionalCuttingMode { get; set; }
public Optional<uint>? OptionalCuttingNumber { get; set; }
public Optional<bool>? OptionalRecordDanmaku { get; set; }
public Optional<bool>? OptionalRecordDanmakuRaw { get; set; }
public Optional<bool>? OptionalRecordDanmakuSuperChat { get; set; }
public Optional<bool>? OptionalRecordDanmakuGift { get; set; }
public Optional<bool>? OptionalRecordDanmakuGuard { get; set; }
public Optional<string?>? 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<SetRoomConfig>
{
public SetRoomConfigType()
{
this.Field(x => x.AutoRecord, nullable: true);
this.Field(x => x.OptionalRecordMode, nullable: true, type: typeof(HierarchicalOptionalInputType<RecordMode>));
this.Field(x => x.OptionalCuttingMode, nullable: true, type: typeof(HierarchicalOptionalInputType<CuttingMode>));
this.Field(x => x.OptionalCuttingNumber, nullable: true, type: typeof(HierarchicalOptionalInputType<uint>));
this.Field(x => x.OptionalRecordDanmaku, nullable: true, type: typeof(HierarchicalOptionalInputType<bool>));
this.Field(x => x.OptionalRecordDanmakuRaw, nullable: true, type: typeof(HierarchicalOptionalInputType<bool>));
this.Field(x => x.OptionalRecordDanmakuSuperChat, nullable: true, type: typeof(HierarchicalOptionalInputType<bool>));
this.Field(x => x.OptionalRecordDanmakuGift, nullable: true, type: typeof(HierarchicalOptionalInputType<bool>));
this.Field(x => x.OptionalRecordDanmakuGuard, nullable: true, type: typeof(HierarchicalOptionalInputType<bool>));
this.Field(x => x.OptionalRecordingQuality, nullable: true, type: typeof(HierarchicalOptionalInputType<string>));
}
}
internal class SetGlobalConfig
{
public Optional<RecordMode>? OptionalRecordMode { get; set; }
public Optional<CuttingMode>? OptionalCuttingMode { get; set; }
public Optional<uint>? OptionalCuttingNumber { get; set; }
public Optional<bool>? OptionalRecordDanmaku { get; set; }
public Optional<bool>? OptionalRecordDanmakuRaw { get; set; }
public Optional<bool>? OptionalRecordDanmakuSuperChat { get; set; }
public Optional<bool>? OptionalRecordDanmakuGift { get; set; }
public Optional<bool>? OptionalRecordDanmakuGuard { get; set; }
public Optional<string?>? OptionalRecordingQuality { get; set; }
public Optional<string?>? OptionalRecordFilenameFormat { get; set; }
public Optional<string?>? OptionalWebHookUrls { get; set; }
public Optional<string?>? OptionalWebHookUrlsV2 { get; set; }
public Optional<bool>? OptionalWpfShowTitleAndArea { get; set; }
public Optional<string?>? OptionalCookie { get; set; }
public Optional<string?>? OptionalLiveApiHost { get; set; }
public Optional<uint>? OptionalTimingCheckInterval { get; set; }
public Optional<uint>? OptionalTimingStreamRetry { get; set; }
public Optional<uint>? OptionalTimingStreamRetryNoQn { get; set; }
public Optional<uint>? OptionalTimingStreamConnect { get; set; }
public Optional<uint>? OptionalTimingDanmakuRetry { get; set; }
public Optional<uint>? OptionalTimingWatchdogTimeout { get; set; }
public Optional<uint>? 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<SetGlobalConfig>
{
public SetGlobalConfigType()
{
this.Field(x => x.OptionalRecordMode, nullable: true, type: typeof(HierarchicalOptionalInputType<RecordMode>));
this.Field(x => x.OptionalCuttingMode, nullable: true, type: typeof(HierarchicalOptionalInputType<CuttingMode>));
this.Field(x => x.OptionalCuttingNumber, nullable: true, type: typeof(HierarchicalOptionalInputType<uint>));
this.Field(x => x.OptionalRecordDanmaku, nullable: true, type: typeof(HierarchicalOptionalInputType<bool>));
this.Field(x => x.OptionalRecordDanmakuRaw, nullable: true, type: typeof(HierarchicalOptionalInputType<bool>));
this.Field(x => x.OptionalRecordDanmakuSuperChat, nullable: true, type: typeof(HierarchicalOptionalInputType<bool>));
this.Field(x => x.OptionalRecordDanmakuGift, nullable: true, type: typeof(HierarchicalOptionalInputType<bool>));
this.Field(x => x.OptionalRecordDanmakuGuard, nullable: true, type: typeof(HierarchicalOptionalInputType<bool>));
this.Field(x => x.OptionalRecordingQuality, nullable: true, type: typeof(HierarchicalOptionalInputType<string>));
this.Field(x => x.OptionalRecordFilenameFormat, nullable: true, type: typeof(HierarchicalOptionalInputType<string>));
this.Field(x => x.OptionalWebHookUrls, nullable: true, type: typeof(HierarchicalOptionalInputType<string>));
this.Field(x => x.OptionalWebHookUrlsV2, nullable: true, type: typeof(HierarchicalOptionalInputType<string>));
this.Field(x => x.OptionalWpfShowTitleAndArea, nullable: true, type: typeof(HierarchicalOptionalInputType<bool>));
this.Field(x => x.OptionalCookie, nullable: true, type: typeof(HierarchicalOptionalInputType<string>));
this.Field(x => x.OptionalLiveApiHost, nullable: true, type: typeof(HierarchicalOptionalInputType<string>));
this.Field(x => x.OptionalTimingCheckInterval, nullable: true, type: typeof(HierarchicalOptionalInputType<uint>));
this.Field(x => x.OptionalTimingStreamRetry, nullable: true, type: typeof(HierarchicalOptionalInputType<uint>));
this.Field(x => x.OptionalTimingStreamRetryNoQn, nullable: true, type: typeof(HierarchicalOptionalInputType<uint>));
this.Field(x => x.OptionalTimingStreamConnect, nullable: true, type: typeof(HierarchicalOptionalInputType<uint>));
this.Field(x => x.OptionalTimingDanmakuRetry, nullable: true, type: typeof(HierarchicalOptionalInputType<uint>));
this.Field(x => x.OptionalTimingWatchdogTimeout, nullable: true, type: typeof(HierarchicalOptionalInputType<uint>));
this.Field(x => x.OptionalRecordDanmakuFlushInterval, nullable: true, type: typeof(HierarchicalOptionalInputType<uint>));
}
}
}

View File

@ -0,0 +1,9 @@
using BililiveRecorder.Core.Config.V2;
using GraphQL.Types;
namespace BililiveRecorder.Web.Schemas.Types
{
public class CuttingModeEnum : EnumerationGraphType<CuttingMode>
{
}
}

View File

@ -0,0 +1,19 @@
using GraphQL.Types;
using HierarchicalPropertyDefault;
namespace BililiveRecorder.Web.Schemas.Types
{
public class HierarchicalOptionalInputType<TValue> : InputObjectGraphType<Optional<TValue>>
{
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.");
}
}
}

View File

@ -0,0 +1,19 @@
using GraphQL.Types;
using HierarchicalPropertyDefault;
namespace BililiveRecorder.Web.Schemas.Types
{
public class HierarchicalOptionalType<TValue> : ObjectGraphType<Optional<TValue>>
{
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.");
}
}
}

View File

@ -0,0 +1,9 @@
using BililiveRecorder.Core.Config.V2;
using GraphQL.Types;
namespace BililiveRecorder.Web.Schemas.Types
{
public class RecordModeEnum : EnumerationGraphType<RecordMode>
{
}
}

View File

@ -0,0 +1,19 @@
using BililiveRecorder.Core;
using GraphQL.Types;
namespace BililiveRecorder.Web.Schemas.Types
{
public class RecordingStatsType : ObjectGraphType<RecordingStats>
{
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);
}
}
}

View File

@ -0,0 +1,24 @@
using BililiveRecorder.Core;
using GraphQL.Types;
namespace BililiveRecorder.Web.Schemas.Types
{
internal class RoomType : ObjectGraphType<IRoom>
{
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));
}
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GraphQL.Server.Transports.AspNetCore.NewtonsoftJson" Version="5.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BililiveRecorder.Web.Schemas\BililiveRecorder.Web.Schemas.csproj" />
</ItemGroup>
</Project>

View File

@ -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<IRoom> Rooms { get; } = new ReadOnlyObservableCollection<IRoom>(new ObservableCollection<IRoom>());
public event EventHandler<AggregatedRoomEventArgs<RecordSessionStartedEventArgs>>? RecordSessionStarted;
public event EventHandler<AggregatedRoomEventArgs<RecordSessionEndedEventArgs>>? RecordSessionEnded;
public event EventHandler<AggregatedRoomEventArgs<RecordFileOpeningEventArgs>>? RecordFileOpening;
public event EventHandler<AggregatedRoomEventArgs<RecordFileClosedEventArgs>>? RecordFileClosed;
public event EventHandler<AggregatedRoomEventArgs<NetworkingStatsEventArgs>>? NetworkingStats;
public event EventHandler<AggregatedRoomEventArgs<RecordingStatsEventArgs>>? 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);
}
}
}

View File

@ -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<Startup>();
});
}
}

View File

@ -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"
}
}
}
}

View File

@ -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<KestrelServerOptions>(o => o.AllowSynchronousIO = true);
services.TryAddSingleton<IRecorder>(new FakeRecorderForWeb());
services
.AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()))
.AddSingleton<RecorderSchema>()
.AddSingleton(typeof(EnumerationGraphType<>))
.AddSingleton<IDocumentExecuter, SubscriptionDocumentExecuter>()
.AddGraphQL((options, provider) =>
{
options.EnableMetrics = this.Environment.IsDevelopment() || Debugger.IsAttached;
var logger = provider.GetRequiredService<ILogger<Startup>>();
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<RecorderSchema>()
.UseGraphQL<RecorderSchema>()
.UseGraphQLPlayground()
.UseGraphQLGraphiQL()
.UseGraphQLAltair()
.UseGraphQLVoyager()
.Use(next => async context =>
{
if (context.Request.Path == "/")
{
await context.Response.WriteAsync(@"<h1>to be done</h1><style>a{margin:5px}</style><a href=""/ui/playground"">Playground</a><a href=""/ui/graphiql"">GraphiQL</a><a href=""/ui/altair"">Altair</a><a href=""/ui/voyager"">Voyager</a>").ConfigureAwait(false);
}
else
{
context.Response.Redirect("/");
}
})
;
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

View File

@ -29,6 +29,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.ToolBox",
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Flv.Tests", "test\BililiveRecorder.Flv.Tests\BililiveRecorder.Flv.Tests.csproj", "{32E554B1-0ECC-4145-85B8-3FC128FEBEA1}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Flv.Tests", "test\BililiveRecorder.Flv.Tests\BililiveRecorder.Flv.Tests.csproj", "{32E554B1-0ECC-4145-85B8-3FC128FEBEA1}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{32E554B1-0ECC-4145-85B8-3FC128FEBEA1}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -75,12 +87,14 @@ Global
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1} {521EC763-5694-45A8-B87F-6E6B7F2A3BD4} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}
{4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200} = {2D44A59D-E437-4FEE-8A2E-3FF00D53A64D} {4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200} = {2D44A59D-E437-4FEE-8A2E-3FF00D53A64D}
{32E554B1-0ECC-4145-85B8-3FC128FEBEA1} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1} {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
RESX_SortFileContentOnSave = True
SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170}
RESX_NeutralResourcesLanguage = zh-Hans
RESX_SaveFilesImmediatelyUponChange = True
RESX_ShowErrorsInErrorList = False RESX_ShowErrorsInErrorList = False
RESX_SaveFilesImmediatelyUponChange = True
RESX_NeutralResourcesLanguage = zh-Hans
SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170}
RESX_SortFileContentOnSave = True
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -7,7 +7,7 @@ export const data: Array<ConfigEntry> = [
type: "int", type: "int",
configType: "roomOnly", configType: "roomOnly",
defaultValue: "default", defaultValue: "default",
// web_readonly: true, webReadonly: true,
markdown: "" markdown: ""
}, },
{ {

View File

@ -33,7 +33,7 @@ const map: SectionInfoMap = {
header: true, header: true,
build: builderCli build: builderCli
}, },
web_is_disabled_for_now: { web: {
path: './BililiveRecorder.Web.Schemas/Types/Config.gen.cs', path: './BililiveRecorder.Web.Schemas/Types/Config.gen.cs',
format: true, format: true,
header: true, header: true,

View File

@ -1,5 +1,125 @@
import { ConfigEntry, ConfigEntryType } from "../types" import { ConfigEntry, ConfigEntryType } from "../types"
import { trimEnd } from "../utils";
export default function (data: ConfigEntry[]): string { 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<RoomConfig>\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<GlobalConfig>\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<DefaultConfig>\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<SetRoomConfig>\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<SetGlobalConfig>\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;
} }

View File

@ -1,12 +1,2 @@
import { ConfigEntry } from "../types"
export { default as code } from "./code" export { default as code } from "./code"
export { default as doc } from "./doc" export { default as doc } from "./doc"
export const core = function (data: Array<ConfigEntry>) {
}
export const cli = function (data: Array<ConfigEntry>) {
}
export const web = function (data: Array<ConfigEntry>) {
}
export const schema = function (data: Array<ConfigEntry>) {
}

View File

@ -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" import * as generators from "./generators"
const baseDirectory = __dirname
const argv = process.argv.slice(2) const argv = process.argv.slice(2)
switch (argv[0]) { switch (argv[0]) {

View File

@ -5,7 +5,7 @@
"packages": { "packages": {
"": { "": {
"devDependencies": { "devDependencies": {
"@types/node": "^16.6.1", "@types/node": "^16.11.26",
"ts-node": "^10.2.0", "ts-node": "^10.2.0",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"typescript": "^4.3.5" "typescript": "^4.3.5"
@ -57,9 +57,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "16.6.1", "version": "16.11.26",
"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", "resolved": "https://registry.npmmirror.com/@types/node/-/node-16.11.26.tgz",
"integrity": "sha1-ruYse5ZvVfxmx7bfodWNsqYW2mE=", "integrity": "sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==",
"dev": true "dev": true
}, },
"node_modules/acorn": { "node_modules/acorn": {
@ -224,9 +224,9 @@
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
"version": "16.6.1", "version": "16.11.26",
"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", "resolved": "https://registry.npmmirror.com/@types/node/-/node-16.11.26.tgz",
"integrity": "sha1-ruYse5ZvVfxmx7bfodWNsqYW2mE=", "integrity": "sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==",
"dev": true "dev": true
}, },
"acorn": { "acorn": {

View File

@ -3,10 +3,9 @@
"build": "ts-node index.ts" "build": "ts-node index.ts"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^16.6.1", "@types/node": "^16.11.26",
"ts-node": "^10.2.0", "ts-node": "^10.2.0",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"typescript": "^4.3.5" "typescript": "^4.3.5"
}, }
"dependencies": {}
} }

View File

@ -16,6 +16,8 @@ export interface ConfigEntry {
readonly type: string, readonly type: string,
/** 设置类型 */ /** 设置类型 */
readonly configType: ConfigEntryType readonly configType: ConfigEntryType
/** Web API 只读属性 */
readonly webReadonly?: boolean,
/** 是否为高级设置(隐藏设置),默认为 false */ /** 是否为高级设置(隐藏设置),默认为 false */
readonly advancedConfig?: boolean, readonly advancedConfig?: boolean,
/** 默认值 */ /** 默认值 */