Web: Implemented web resources embedding

This commit is contained in:
genteure 2022-04-05 13:42:14 +08:00
parent e7916fbb16
commit 070d007924
6 changed files with 255 additions and 42 deletions

View File

@ -1,11 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<NoWarn>1701;1702;1591</NoWarn>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="11.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
@ -19,11 +20,34 @@
<PackageReference Include="GraphQL.Server.Ui.Voyager" Version="5.2.1" />
<PackageReference Include="GraphQL.SystemReactive" Version="4.8.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.3" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BililiveRecorder.Core\BililiveRecorder.Core.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="embeded\**\*">
<TargetPath>%(RecursiveDir)%(Filename)%(Extension)</TargetPath>
</EmbeddedResource>
</ItemGroup>
<Target Name="_RemoveManifestItSelfFromTheManifest" AfterTargets="PrepareResourceNames" BeforeTargets="_GenerateEmbeddedFilesManifest">
<!--
保留以后万一调试用。
<Message Importance="high" Text="AAAAAAAAAAAAAAAAAA: %(EmbeddedResource.ManifestResourceName)&#xD;&#xA; LogicalName: %(EmbeddedResource.LogicalName)&#xD;&#xA; ExcludeFromManifest: %(EmbeddedResource.AAAAAAAAExcludeFromManifest)" />
-->
<ItemGroup>
<EmbeddedResource>
<ExcludeFromManifest Condition="'%(LogicalName)' == '$(EmbeddedFilesManifestFileName)'">true</ExcludeFromManifest>
</EmbeddedResource>
</ItemGroup>
<!--
<Message Importance="high" Text="DEBUG: @(EmbeddedResource->WithMetadataValue('LogicalName',$(EmbeddedFilesManifestFileName)))"/>
<Message Importance="high" Text="BBBBBBBBBBBBBBBBBB: %(EmbeddedResource.ManifestResourceName)&#xD;&#xA; LogicalName: %(EmbeddedResource.LogicalName)&#xD;&#xA; ExcludeFromManifest: %(EmbeddedResource.AAAAAAAAExcludeFromManifest)" />
-->
</Target>
</Project>

View File

@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using BililiveRecorder.Core;
using BililiveRecorder.Web.Graphql;
using BililiveRecorder.Web.Models.Rest;
@ -12,9 +13,12 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.AspNetCore.StaticFiles.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
@ -39,15 +43,18 @@ namespace BililiveRecorder.Web
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
// GraphQL API 使用 Newtonsoft.Json 会同步调用 System.IO.StreamReader.Peek
// 来源是 GraphQL.Server.Transports.AspNetCore.NewtonsoftJson.GraphQLRequestDeserializer.DeserializeFromJsonBodyAsync
// System.Text.Json 不使用构造函数传属性,所以无法创建 Optional<T> (需要加 attribute回头改
// TODO 改 Optional<T>
services.Configure<KestrelServerOptions>(o => o.AllowSynchronousIO = true);
// 如果 IRecorder 没有被注册过才会添加,模拟调试用
// 实际运行时在 BililiveRecorder.Web.Program 里会加上真的 IRecorder
services.TryAddSingleton<IRecorder>(new FakeRecorderForWeb());
services
.AddAutoMapper(c =>
{
c.AddProfile<DataMappingProfile>();
})
.AddAutoMapper(c => c.AddProfile<DataMappingProfile>())
.AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()))
;
@ -65,7 +72,7 @@ namespace BililiveRecorder.Web
{
options.EnableMetrics = this.Environment.IsDevelopment() || Debugger.IsAttached;
var logger = options.RequestServices?.GetRequiredService<ILogger<Startup>>();
options.UnhandledExceptionDelegate = ctx => logger?.LogError(ctx.OriginalException, "Graphql Error: {Error}");
options.UnhandledExceptionDelegate = ctx => logger?.LogError(ctx.OriginalException, "Graphql Error");
})
;
@ -99,44 +106,101 @@ namespace BililiveRecorder.Web
}
// 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()
.UseRouting()
.UseEndpoints(endpoints =>
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
const string PAGE404 = "/404.html";
app.UseCors().UseWebSockets();
app.Use(static next => async context =>
{
endpoints.MapControllers();
// 404 页面处理
await next(context);
endpoints.MapGraphQL<RecorderSchema>();
endpoints.MapGraphQLWebSockets<RecorderSchema>();
endpoints.MapSwagger();
endpoints.MapGraphQLPlayground();
endpoints.MapGraphQLGraphiQL();
endpoints.MapGraphQLAltair();
endpoints.MapGraphQLVoyager();
endpoints.MapGet("/", async context =>
if (!context.Response.HasStarted
&& !context.Response.ContentLength.HasValue
&& string.IsNullOrEmpty(context.Response.ContentType)
&& context.Response.StatusCode == StatusCodes.Status404NotFound)
{
context.Response.ContentType = "text/html";
await context.Response.WriteAsync(ConstStrings.HOME_PAGE_HTML, encoding: System.Text.Encoding.UTF8).ConfigureAwait(false);
});
var originalPath = context.Request.Path;
var originalQueryString = context.Request.QueryString;
try
{
context.Request.Path = PAGE404;
context.Request.QueryString = new QueryString();
await next(context);
}
finally
{
context.Request.Path = originalPath;
context.Request.QueryString = originalQueryString;
}
}
});
endpoints.MapGet("favicon.ico", async context =>
app.UseRouting();
static void OnPrepareResponse(StaticFileResponseContext context)
{
if (context.Context.Request.Path == PAGE404)
{
context.Response.StatusCode = 404;
await context.Response.WriteAsync(string.Empty);
context.Context.Response.StatusCode = StatusCodes.Status404NotFound;
context.Context.Response.Headers.CacheControl = "no-cache";
}
else
{
context.Context.Response.Headers.CacheControl = "max-age=86400, must-revalidate";
}
}
var ctp = new FileExtensionContentTypeProvider();
ctp.Mappings[".htm"] = "text/html; charset=utf-8";
ctp.Mappings[".html"] = "text/html; charset=utf-8";
ctp.Mappings[".css"] = "text/css; charset=utf-8";
ctp.Mappings[".js"] = "text/javascript; charset=utf-8";
ctp.Mappings[".mjs"] = "text/javascript; charset=utf-8";
ctp.Mappings[".json"] = "application/json; charset=utf-8";
var shared = new SharedOptions()
{
// 在运行的 exe 旁边新建一个 wwwroot 文件夹,会优先使用里面的内容,然后 fallback 到打包的资源文件
FileProvider = new CompositeFileProvider(env.WebRootFileProvider, new ManifestEmbeddedFileProvider(typeof(Startup).Assembly)),
RedirectToAppendTrailingSlash = true,
};
app
.UseDefaultFiles(new DefaultFilesOptions(shared))
.UseStaticFiles(new StaticFileOptions(shared)
{
ContentTypeProvider = ctp,
OnPrepareResponse = OnPrepareResponse
})
//.UseDirectoryBrowser(new DirectoryBrowserOptions(shared))
;
app
.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapGraphQL<RecorderSchema>();
endpoints.MapGraphQLWebSockets<RecorderSchema>();
endpoints.MapSwagger();
endpoints.MapGraphQLPlayground();
endpoints.MapGraphQLGraphiQL();
endpoints.MapGraphQLAltair();
endpoints.MapGraphQLVoyager();
})
.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("brec/swagger.json", "录播姬 REST API");
})
.Use(static next => static context =>
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
return Task.CompletedTask;
});
})
.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("brec/swagger.json", "录播姬 REST API");
})
.Use(next => async context =>
{
context.Response.Redirect("/");
await context.Response.WriteAsync(string.Empty);
})
;
}
}
}

View File

@ -0,0 +1,2 @@
<title>录播姬 404</title>
<h1 style="color: red;">HTTP 404</h1>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,8 @@
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024" xml:space="preserve">
<style>.st1{fill:#1c9bcd}</style>
<path d="M293.4 951c-28.4 0-44.3-13.2-52.7-24.2-5.9-7.8-9.3-16.1-11.5-23.1-24.9-.2-51.1-.7-51.5-.7h-1.9l-1.8-.4c-2.9-.6-29.6-6.6-58.2-29.6-26.4-21.2-59.3-61.4-66.4-132.9l-.1-1.1v-1.1c1.6-120.4.2-357.6.2-370.7-.3-5.5-1.3-35.6 11.2-69.3 12.7-34.4 42.1-79.3 109.8-98.4l2.6-.7 155.2-.4-48.8-47.4-.7-.9c-10.2-12.6-15.5-39.3 11.7-61.6 11.8-9.7 20.1-14.1 28.4-15.2 12.7-1.7 20.8 5.2 24.3 8.1.5.4 1.1.9 1.3 1.1 5.7 3.8 46.1 42.8 120.2 115.6l97.5.2C671.1 91 677.1 87 679.3 85.4c.2-.2.9-.7 1.3-1.1 3.5-3 11.5-9.9 24.3-8.1 8.3 1.1 16.6 5.6 28.4 15.2 27.2 22.3 21.9 49 11.7 61.6l-.7.9-45.9 44.5c83.9.3 144.7.3 145.4.3h2.2l2.1.5c1.3.3 31.8 7.2 62.4 31.1 28.3 22.2 62.5 63.8 64.2 135.7v.8c-1.5 122.5 0 369.6 0 372.1v1c-.1 1.4-1.8 34.4-17.6 70.1-21.6 49.1-59 79.9-108.1 89l-2.1.4-2.2-.1c-19.3-.6-32.8-.7-41.8-.6-1.3 4.6-3.1 9.7-5.6 14.9-11.4 23.7-31.8 37-57.3 37.5-34.3.5-58.9-17.3-68.4-48.7-60.7.2-257.2 1.1-314 1.4-1.5 4.3-3.5 8.9-6.2 13.6-12.3 21.6-33 33.6-58 33.6zm408.5-832.6c-.1.1-.2.1-.2.2l.2-.2zm-380.1-2.9c.1.1.2.1.3.2-.1-.1-.2-.2-.3-.2z" style="fill:#f7f8f8"/>
<path class="st1" d="M175.9 218.7C60.6 251.2 69.5 366.5 69.5 366.5s1.5 246.8-.2 371.7C81.7 863 178.1 883 178.1 883s38.4.7 66.5.7c3 8.1 5.2 47.3 48.8 47.3 43.6 0 48.8-47.3 48.8-47.3s319.2-1.5 345.8-1.5c1.5 13.3 8.1 49.5 51.7 48.8 43.6-.7 46.6-51.7 46.6-51.7s14.8-1.5 59.1 0c103.4-19.2 109.4-140.4 109.4-140.4s-1.5-248.3 0-372.4C951.8 242.4 844 218.7 844 218.7s-85 .1-194.6-.5l80.2-77.8s12.8-15.8-8.9-33.5C699 89.2 697.9 97 690.6 102c-6.6 4.5-103.1 99.5-120.2 116.3l-113.8-.2S340.7 104 333.4 99c-7.3-4.9-8.5-12.8-30.1 4.9-21.7 17.7-8.9 33.5-8.9 33.5l83.2 80.6-201.7.7zm689.8 542.8c0 19.6-15.5 35.5-34.6 35.5H201.8c-19.1 0-34.6-15.9-34.6-35.5V344.8c0-19.6 15.5-35.5 34.6-35.5H831c19.1 0 34.6 15.9 34.6 35.5v416.7z"/>
<path class="st1" d="M512 340c-116 0-210 94-210 210s94 210 210 210 210-94 210-210-94-210-210-210zm0 375c-91.1 0-165-73.9-165-165s73.9-165 165-165 165 73.9 165 165-73.9 165-165 165z"/>
<path class="st1" d="M512 490c-33.1 0-60 26.9-60 60 0 12.4-10.1 22.5-22.5 22.5S407 562.4 407 550c0-58 47-105 105-105 12.4 0 22.5 10.1 22.5 22.5S524.4 490 512 490z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,115 @@
<meta charset=utf-8><meta content="width=device-width,initial-scale=1"name=viewport><title>录播姬</title><style>a{margin:5px}textarea{width:300px;height:200px}</style><h1>录播姬,以后再写管理界面</h1><p><span style=color:red;font-weight:700>API 界面</span> <a href=/ui/graphiql>GraphiQL</a> <a href=/ui/playground>Playground</a> <a href=/ui/altair>Altair</a> <a href=/ui/voyager>Voyager</a><p>说明:录播姬 API 里的 objectId 在重启后会重新生成,是不保存到配置文件里的<div><h2>API 用法示例</h2><p>graphql 的<b>语法</b>请查看官方文档 <a href=https://graphql.org/learn/ >https://graphql.org/learn/</a> ,或中文翻译镜像 <a href=https://graphql.cn/learn/ >https://graphql.cn/learn/</a><div><h3>列出所有直播间和基本信息</h3><textarea>
query {
rooms {
objectId
roomConfig {
roomId
autoRecord
}
shortId
name
streaming
title
areaNameParent
areaNameChild
}
}</textarea></div><div><h3>列出所有直播间的所有信息截至编写此页面时具体最新属性见API界面的文档</h3><textarea>
query {
rooms {
objectId
shortId
recording
roomConfig {
roomId
autoRecord
optionalRecordingQuality {
value
hasValue
}
optionalRecordMode {
value
hasValue
}
optionalRecordDanmakuSuperChat {
value
hasValue
}
optionalRecordDanmakuRaw {
value
hasValue
}
optionalRecordDanmakuGuard {
value
hasValue
}
optionalRecordDanmakuGift {
value
hasValue
}
optionalRecordDanmaku {
value
hasValue
}
optionalCuttingNumber {
value
hasValue
}
optionalCuttingMode {
value
hasValue
}
}
stats {
durationRatio
fileMaxTimestamp
networkMbps
sessionDuration
totalInputBytes
sessionMaxTimestamp
totalOutputBytes
}
streaming
title
name
danmakuConnected
autoRecordForThisSession
areaNameParent
areaNameChild
}
}
</textarea></div><div><h3>添加一个房间</h3><textarea>
mutation {
addRoom(roomId: 3, autoRecord: false) {
objectId
shortId
roomConfig {
roomId
autoRecord
}
}
}
</textarea></div><div><h3>添加一个房间,用变量传参版</h3><textarea>
mutation AddRoom($roomid: Int, $autoRecord: Boolean) {
addRoom(roomId: $roomid, autoRecord: $autoRecord) {
objectId
shortId
roomConfig {
roomId
autoRecord
}
}
}
</textarea> <textarea>{'roomid': 4, 'autoRecord': false}</textarea></div><div><h3>启用自动录制</h3><textarea>
mutation {
setRoomConfig(roomId: 3, config: { autoRecord: true }) {
objectId
shortId
recording
roomConfig {
roomId
autoRecord
}
}
}
</textarea></div><div>开始录制、结束录制、手动切分,都是 mutation在 graphql 文档页面里可以看到</div></div>