2022-04-03 13:34:55 +08:00
|
|
|
|
using System;
|
2021-05-30 19:16:20 +08:00
|
|
|
|
using System.Diagnostics;
|
2022-04-03 13:34:55 +08:00
|
|
|
|
using System.IO;
|
2022-04-05 13:42:14 +08:00
|
|
|
|
using System.Threading.Tasks;
|
2022-04-09 16:43:05 +08:00
|
|
|
|
using AutoMapper;
|
2021-05-30 19:16:20 +08:00
|
|
|
|
using BililiveRecorder.Core;
|
2022-04-03 14:47:54 +08:00
|
|
|
|
using BililiveRecorder.Web.Graphql;
|
2022-04-03 15:34:22 +08:00
|
|
|
|
using BililiveRecorder.Web.Models.Rest;
|
2021-05-30 19:16:20 +08:00
|
|
|
|
using GraphQL;
|
2022-04-03 14:52:11 +08:00
|
|
|
|
using GraphQL.Execution;
|
2021-05-30 19:16:20 +08:00
|
|
|
|
using GraphQL.Server;
|
2022-04-03 14:52:11 +08:00
|
|
|
|
using GraphQL.SystemReactive;
|
2021-05-30 19:16:20 +08:00
|
|
|
|
using Microsoft.AspNetCore.Builder;
|
|
|
|
|
using Microsoft.AspNetCore.Hosting;
|
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
2022-04-05 13:42:14 +08:00
|
|
|
|
using Microsoft.AspNetCore.StaticFiles;
|
|
|
|
|
using Microsoft.AspNetCore.StaticFiles.Infrastructure;
|
2021-05-30 19:16:20 +08:00
|
|
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
2022-04-05 13:42:14 +08:00
|
|
|
|
using Microsoft.Extensions.FileProviders;
|
2021-05-30 19:16:20 +08:00
|
|
|
|
using Microsoft.Extensions.Hosting;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
2022-04-03 13:34:55 +08:00
|
|
|
|
using Microsoft.OpenApi.Models;
|
2021-05-30 19:16:20 +08:00
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
{
|
2022-04-05 13:42:14 +08:00
|
|
|
|
// GraphQL API 使用 Newtonsoft.Json 会同步调用 System.IO.StreamReader.Peek
|
|
|
|
|
// 来源是 GraphQL.Server.Transports.AspNetCore.NewtonsoftJson.GraphQLRequestDeserializer.DeserializeFromJsonBodyAsync
|
|
|
|
|
// System.Text.Json 不使用构造函数传属性,所以无法创建 Optional<T> (需要加 attribute,回头改)
|
|
|
|
|
// TODO 改 Optional<T>
|
2021-05-30 19:16:20 +08:00
|
|
|
|
services.Configure<KestrelServerOptions>(o => o.AllowSynchronousIO = true);
|
|
|
|
|
|
2022-04-05 13:42:14 +08:00
|
|
|
|
// 如果 IRecorder 没有被注册过才会添加,模拟调试用
|
|
|
|
|
// 实际运行时在 BililiveRecorder.Web.Program 里会加上真的 IRecorder
|
2021-05-30 19:16:20 +08:00
|
|
|
|
services.TryAddSingleton<IRecorder>(new FakeRecorderForWeb());
|
|
|
|
|
|
2022-04-09 16:43:05 +08:00
|
|
|
|
#if DEBUG
|
|
|
|
|
// TODO 移动到一个单独的测试项目里
|
2022-06-09 15:40:08 +08:00
|
|
|
|
var configuration = new MapperConfiguration(cfg => cfg.AddProfile<DataMappingProfile>());
|
|
|
|
|
configuration.AssertConfigurationIsValid();
|
2022-04-09 16:43:05 +08:00
|
|
|
|
#endif
|
|
|
|
|
|
2021-05-30 19:16:20 +08:00
|
|
|
|
services
|
2022-04-05 13:42:14 +08:00
|
|
|
|
.AddAutoMapper(c => c.AddProfile<DataMappingProfile>())
|
2022-06-09 16:04:29 +08:00
|
|
|
|
.AddCors(o => o.AddDefaultPolicy(policy =>
|
|
|
|
|
{
|
|
|
|
|
policy
|
|
|
|
|
.SetIsOriginAllowed(origin => true)
|
|
|
|
|
.AllowAnyMethod()
|
|
|
|
|
.AllowAnyHeader()
|
|
|
|
|
.AllowCredentials()
|
|
|
|
|
.WithExposedHeaders("Content-Length")
|
|
|
|
|
.SetPreflightMaxAge(TimeSpan.FromMinutes(5))
|
|
|
|
|
;
|
|
|
|
|
}));
|
2022-04-03 14:52:11 +08:00
|
|
|
|
|
2022-08-26 18:56:46 +08:00
|
|
|
|
services
|
|
|
|
|
.AddSingleton(new ManifestEmbeddedFileProvider(typeof(Startup).Assembly))
|
|
|
|
|
.AddSingleton(sp => new CompositeFileProvider(sp.GetRequiredService<IWebHostEnvironment>().WebRootFileProvider, sp.GetRequiredService<ManifestEmbeddedFileProvider>()));
|
2022-06-07 13:44:50 +08:00
|
|
|
|
|
2022-04-03 14:52:11 +08:00
|
|
|
|
// Graphql API
|
|
|
|
|
GraphQL.MicrosoftDI.GraphQLBuilderExtensions.AddGraphQL(services)
|
|
|
|
|
.AddServer(true)
|
|
|
|
|
.AddWebSockets()
|
|
|
|
|
.AddNewtonsoftJson()
|
|
|
|
|
.AddSchema<RecorderSchema>()
|
|
|
|
|
.AddSubscriptionDocumentExecuter()
|
|
|
|
|
.AddDefaultEndpointSelectorPolicy()
|
|
|
|
|
.AddGraphTypes(typeof(RecorderSchema).Assembly)
|
|
|
|
|
.Configure<ErrorInfoProviderOptions>(opt => opt.ExposeExceptionStackTrace = this.Environment.IsDevelopment() || Debugger.IsAttached)
|
|
|
|
|
.ConfigureExecution(options =>
|
2021-05-30 19:16:20 +08:00
|
|
|
|
{
|
|
|
|
|
options.EnableMetrics = this.Environment.IsDevelopment() || Debugger.IsAttached;
|
2022-04-03 14:52:11 +08:00
|
|
|
|
var logger = options.RequestServices?.GetRequiredService<ILogger<Startup>>();
|
2022-04-05 13:42:14 +08:00
|
|
|
|
options.UnhandledExceptionDelegate = ctx => logger?.LogError(ctx.OriginalException, "Graphql Error");
|
2021-05-30 19:16:20 +08:00
|
|
|
|
})
|
|
|
|
|
;
|
2022-04-03 13:34:55 +08:00
|
|
|
|
|
|
|
|
|
// REST API
|
|
|
|
|
services
|
|
|
|
|
.AddSwaggerGen(c =>
|
|
|
|
|
{
|
2022-06-12 14:44:29 +08:00
|
|
|
|
c.UseAllOfForInheritance();
|
|
|
|
|
c.UseOneOfForPolymorphism();
|
|
|
|
|
|
2022-04-03 13:34:55 +08:00
|
|
|
|
c.SwaggerDoc("brec", new OpenApiInfo
|
|
|
|
|
{
|
|
|
|
|
Title = "录播姬 REST API",
|
2022-06-07 18:35:46 +08:00
|
|
|
|
Description = "录播姬网站 [rec.danmuji.org](https://rec.danmuji.org/) \n录播姬 GitHub [BililiveRecorder/BililiveRecorder](https://github.com/BililiveRecorder/BililiveRecorder) \n\n" +
|
2022-04-03 14:52:11 +08:00
|
|
|
|
"除了 REST API 以外,录播姬还有 Graphql API 可以使用。\n\n" +
|
|
|
|
|
"API 中的 objectId 在重启后会重新生成,不保存到配置文件。",
|
2022-04-03 13:34:55 +08:00
|
|
|
|
Version = "v1"
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var filePath = Path.Combine(AppContext.BaseDirectory, "BililiveRecorder.Web.xml");
|
|
|
|
|
c.IncludeXmlComments(filePath);
|
|
|
|
|
})
|
|
|
|
|
.AddRouting(c =>
|
|
|
|
|
{
|
|
|
|
|
c.LowercaseUrls = true;
|
|
|
|
|
c.LowercaseQueryStrings = true;
|
|
|
|
|
})
|
|
|
|
|
.AddMvcCore(option =>
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
})
|
2022-04-03 14:47:54 +08:00
|
|
|
|
.AddApiExplorer()
|
|
|
|
|
.AddNewtonsoftJson();
|
2021-05-30 19:16:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
2022-04-05 13:42:14 +08:00
|
|
|
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
|
|
|
|
{
|
|
|
|
|
const string PAGE404 = "/404.html";
|
|
|
|
|
|
2024-03-09 21:02:44 +08:00
|
|
|
|
if (app.ApplicationServices.GetService<DisableOpenAccessWarningConfig>() is null)
|
|
|
|
|
{
|
|
|
|
|
app.UseMiddleware<OpenAccessWarningMiddleware>();
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-09 16:45:07 +08:00
|
|
|
|
app
|
|
|
|
|
.UseCors()
|
|
|
|
|
.UseMiddleware<BasicAuthMiddleware>()
|
|
|
|
|
.UseWebSockets();
|
2022-04-05 13:42:14 +08:00
|
|
|
|
|
|
|
|
|
app.Use(static next => async context =>
|
2021-05-30 19:16:20 +08:00
|
|
|
|
{
|
2022-04-05 13:42:14 +08:00
|
|
|
|
// 404 页面处理
|
|
|
|
|
await next(context);
|
2022-04-03 13:34:55 +08:00
|
|
|
|
|
2022-04-05 13:42:14 +08:00
|
|
|
|
if (!context.Response.HasStarted
|
|
|
|
|
&& !context.Response.ContentLength.HasValue
|
|
|
|
|
&& string.IsNullOrEmpty(context.Response.ContentType)
|
|
|
|
|
&& context.Response.StatusCode == StatusCodes.Status404NotFound)
|
|
|
|
|
{
|
|
|
|
|
var originalPath = context.Request.Path;
|
|
|
|
|
var originalQueryString = context.Request.QueryString;
|
|
|
|
|
try
|
|
|
|
|
{
|
2022-06-04 03:39:33 +08:00
|
|
|
|
if (originalPath.StartsWithSegments("/ui"))
|
|
|
|
|
{
|
2022-08-26 18:56:46 +08:00
|
|
|
|
context.Items["webui-spa-path"] = originalPath;
|
2022-06-04 03:39:33 +08:00
|
|
|
|
context.Request.Path = "/ui/";
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
context.Request.Path = PAGE404;
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-05 13:42:14 +08:00
|
|
|
|
context.Request.QueryString = new QueryString();
|
|
|
|
|
await next(context);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
context.Request.Path = originalPath;
|
|
|
|
|
context.Request.QueryString = originalQueryString;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2022-04-03 13:34:55 +08:00
|
|
|
|
|
2022-04-05 13:42:14 +08:00
|
|
|
|
app.UseRouting();
|
2022-04-03 13:34:55 +08:00
|
|
|
|
|
2022-04-05 13:42:14 +08:00
|
|
|
|
static void OnPrepareResponse(StaticFileResponseContext context)
|
|
|
|
|
{
|
|
|
|
|
if (context.Context.Request.Path == PAGE404)
|
2021-05-30 19:16:20 +08:00
|
|
|
|
{
|
2022-04-05 13:42:14 +08:00
|
|
|
|
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";
|
|
|
|
|
|
2023-02-08 11:59:49 +08:00
|
|
|
|
ctp.Mappings[".mkv"] = "video/x-matroska";
|
|
|
|
|
ctp.Mappings[".mk3d"] = "video/x-matroska";
|
|
|
|
|
ctp.Mappings[".mka"] = "audio/x-matroska";
|
|
|
|
|
ctp.Mappings[".mks"] = "video/x-matroska";
|
|
|
|
|
ctp.Mappings[".srt"] = "text/plain";
|
|
|
|
|
ctp.Mappings[".ass"] = "text/plain";
|
|
|
|
|
ctp.Mappings[".ssa"] = "text/plain";
|
|
|
|
|
|
2022-08-26 18:56:46 +08:00
|
|
|
|
var compositeFileProvider = app.ApplicationServices.GetRequiredService<CompositeFileProvider>();
|
2022-04-05 16:05:48 +08:00
|
|
|
|
var sharedStaticFiles = new SharedOptions()
|
2022-04-05 13:42:14 +08:00
|
|
|
|
{
|
|
|
|
|
// 在运行的 exe 旁边新建一个 wwwroot 文件夹,会优先使用里面的内容,然后 fallback 到打包的资源文件
|
2022-08-26 18:56:46 +08:00
|
|
|
|
FileProvider = compositeFileProvider,
|
2022-04-05 13:42:14 +08:00
|
|
|
|
RedirectToAppendTrailingSlash = true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
app
|
2022-04-05 16:05:48 +08:00
|
|
|
|
.UseDefaultFiles(new DefaultFilesOptions(sharedStaticFiles))
|
|
|
|
|
.UseStaticFiles(new StaticFileOptions(sharedStaticFiles)
|
2022-04-05 13:42:14 +08:00
|
|
|
|
{
|
|
|
|
|
ContentTypeProvider = ctp,
|
|
|
|
|
OnPrepareResponse = OnPrepareResponse
|
|
|
|
|
})
|
|
|
|
|
//.UseDirectoryBrowser(new DirectoryBrowserOptions(shared))
|
|
|
|
|
;
|
|
|
|
|
|
2022-04-05 16:05:48 +08:00
|
|
|
|
try
|
|
|
|
|
{
|
2022-06-08 00:58:08 +08:00
|
|
|
|
if (app.ApplicationServices.GetService<BililiveRecorderFileExplorerSettings>()?.Enable ?? false)
|
2022-04-05 16:05:48 +08:00
|
|
|
|
{
|
2022-06-08 00:58:08 +08:00
|
|
|
|
var sharedRecordingFiles = new SharedOptions
|
|
|
|
|
{
|
2024-11-22 22:52:28 +08:00
|
|
|
|
FileProvider = new PhysicalFileProvider(app.ApplicationServices.GetRequiredService<IRecorder>().Config.Global.WorkDirectory!),
|
2022-06-08 00:58:08 +08:00
|
|
|
|
RequestPath = "/file",
|
|
|
|
|
RedirectToAppendTrailingSlash = true,
|
|
|
|
|
};
|
2023-02-08 11:59:49 +08:00
|
|
|
|
app
|
|
|
|
|
.UseStaticFiles(new StaticFileOptions(sharedRecordingFiles)
|
|
|
|
|
{
|
|
|
|
|
ContentTypeProvider = ctp,
|
|
|
|
|
ServeUnknownFileTypes = true,
|
|
|
|
|
})
|
|
|
|
|
.UseDirectoryBrowser(new DirectoryBrowserOptions(sharedRecordingFiles)
|
|
|
|
|
{
|
|
|
|
|
Formatter = new BililiveRecorderDirectoryFormatter(),
|
|
|
|
|
});
|
2022-06-08 00:58:08 +08:00
|
|
|
|
}
|
2022-04-05 16:05:48 +08:00
|
|
|
|
}
|
|
|
|
|
catch (Exception) { }
|
|
|
|
|
|
2022-04-05 13:42:14 +08:00
|
|
|
|
app
|
|
|
|
|
.UseEndpoints(endpoints =>
|
|
|
|
|
{
|
|
|
|
|
endpoints.MapControllers();
|
|
|
|
|
|
|
|
|
|
endpoints.MapGraphQL<RecorderSchema>();
|
|
|
|
|
endpoints.MapGraphQLWebSockets<RecorderSchema>();
|
2022-04-03 13:34:55 +08:00
|
|
|
|
|
2022-04-05 13:42:14 +08:00
|
|
|
|
endpoints.MapSwagger();
|
2022-06-04 03:39:33 +08:00
|
|
|
|
endpoints.MapGraphQLPlayground("graphql/playground");
|
|
|
|
|
endpoints.MapGraphQLGraphiQL("graphql/graphiql");
|
|
|
|
|
endpoints.MapGraphQLAltair("graphql/altair");
|
|
|
|
|
endpoints.MapGraphQLVoyager("graphql/voyager");
|
2022-04-05 13:42:14 +08:00
|
|
|
|
})
|
|
|
|
|
.UseSwaggerUI(c =>
|
|
|
|
|
{
|
|
|
|
|
c.SwaggerEndpoint("brec/swagger.json", "录播姬 REST API");
|
|
|
|
|
})
|
|
|
|
|
.Use(static next => static context =>
|
2021-05-30 19:16:20 +08:00
|
|
|
|
{
|
2022-04-05 13:42:14 +08:00
|
|
|
|
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
|
|
|
|
return Task.CompletedTask;
|
2022-04-03 13:34:55 +08:00
|
|
|
|
});
|
2022-04-05 13:42:14 +08:00
|
|
|
|
}
|
2021-05-30 19:16:20 +08:00
|
|
|
|
}
|
|
|
|
|
}
|