mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-15 19:22:19 +08:00
Web: Implemented web resources embedding
This commit is contained in:
parent
e7916fbb16
commit
070d007924
|
@ -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)
 LogicalName: %(EmbeddedResource.LogicalName)
 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)
 LogicalName: %(EmbeddedResource.LogicalName)
 ExcludeFromManifest: %(EmbeddedResource.AAAAAAAAExcludeFromManifest)" />
|
||||
-->
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
2
BililiveRecorder.Web/embeded/404.html
Normal file
2
BililiveRecorder.Web/embeded/404.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
<title>录播姬 404</title>
|
||||
<h1 style="color: red;">HTTP 404</h1>
|
BIN
BililiveRecorder.Web/embeded/favicon.ico
Normal file
BIN
BililiveRecorder.Web/embeded/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
8
BililiveRecorder.Web/embeded/favicon.svg
Normal file
8
BililiveRecorder.Web/embeded/favicon.svg
Normal 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 |
115
BililiveRecorder.Web/embeded/index.html
Normal file
115
BililiveRecorder.Web/embeded/index.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user