From abd62dc378bd809cedec816283c741fd322b824b Mon Sep 17 00:00:00 2001 From: genteure Date: Sun, 3 Apr 2022 13:34:55 +0800 Subject: [PATCH] Web: Add REST style API --- .../Types/Config.gen.cs | 4 +- .../Types/RecorderVersion.cs | 2 +- BililiveRecorder.Web/Api/ConfigController.cs | 49 +++ .../Api/DataMappingProfile.cs | 23 + BililiveRecorder.Web/Api/RoomController.cs | 403 ++++++++++++++++++ BililiveRecorder.Web/Api/VersionController.cs | 15 + .../BililiveRecorder.Web.csproj | 5 + BililiveRecorder.Web/FakeRecorderForWeb.cs | 2 + BililiveRecorder.Web/Models/CreateRoomDto.cs | 8 + .../Models/GlobalConfigDto.cs | 13 + BililiveRecorder.Web/Models/RestApiError.cs | 9 + .../Models/RestApiErrorCode.cs | 25 ++ BililiveRecorder.Web/Models/RoomConfigDto.cs | 7 + BililiveRecorder.Web/Models/RoomDto.cs | 20 + BililiveRecorder.Web/Models/RoomStatsDto.cs | 15 + BililiveRecorder.Web/Startup.cs | 77 +++- 16 files changed, 662 insertions(+), 15 deletions(-) create mode 100644 BililiveRecorder.Web/Api/ConfigController.cs create mode 100644 BililiveRecorder.Web/Api/DataMappingProfile.cs create mode 100644 BililiveRecorder.Web/Api/RoomController.cs create mode 100644 BililiveRecorder.Web/Api/VersionController.cs create mode 100644 BililiveRecorder.Web/Models/CreateRoomDto.cs create mode 100644 BililiveRecorder.Web/Models/GlobalConfigDto.cs create mode 100644 BililiveRecorder.Web/Models/RestApiError.cs create mode 100644 BililiveRecorder.Web/Models/RestApiErrorCode.cs create mode 100644 BililiveRecorder.Web/Models/RoomConfigDto.cs create mode 100644 BililiveRecorder.Web/Models/RoomDto.cs create mode 100644 BililiveRecorder.Web/Models/RoomStatsDto.cs diff --git a/BililiveRecorder.Web.Schemas/Types/Config.gen.cs b/BililiveRecorder.Web.Schemas/Types/Config.gen.cs index 15382b3..26c32aa 100644 --- a/BililiveRecorder.Web.Schemas/Types/Config.gen.cs +++ b/BililiveRecorder.Web.Schemas/Types/Config.gen.cs @@ -85,7 +85,7 @@ namespace BililiveRecorder.Web.Schemas.Types } } - internal class SetRoomConfig + public class SetRoomConfig // TODO MOVE THIS TYPE { public bool? AutoRecord { get; set; } public Optional? OptionalRecordMode { get; set; } @@ -130,7 +130,7 @@ namespace BililiveRecorder.Web.Schemas.Types } } - internal class SetGlobalConfig + public class SetGlobalConfig // TODO MOVE THIS TYPE { public Optional? OptionalRecordMode { get; set; } public Optional? OptionalCuttingMode { get; set; } diff --git a/BililiveRecorder.Web.Schemas/Types/RecorderVersion.cs b/BililiveRecorder.Web.Schemas/Types/RecorderVersion.cs index db03a7c..5b65b1e 100644 --- a/BililiveRecorder.Web.Schemas/Types/RecorderVersion.cs +++ b/BililiveRecorder.Web.Schemas/Types/RecorderVersion.cs @@ -2,7 +2,7 @@ namespace BililiveRecorder.Web.Schemas.Types { public class RecorderVersion { - internal static readonly RecorderVersion Instance = new(); + public static readonly RecorderVersion Instance = new(); public string Major { get; } = GitVersionInformation.Major; public string Minor { get; } = GitVersionInformation.Minor; diff --git a/BililiveRecorder.Web/Api/ConfigController.cs b/BililiveRecorder.Web/Api/ConfigController.cs new file mode 100644 index 0000000..bfc31a6 --- /dev/null +++ b/BililiveRecorder.Web/Api/ConfigController.cs @@ -0,0 +1,49 @@ +using System; +using AutoMapper; +using BililiveRecorder.Core; +using BililiveRecorder.Core.Config.V2; +using BililiveRecorder.Web.Models; +using BililiveRecorder.Web.Schemas.Types; +using Microsoft.AspNetCore.Mvc; + +namespace BililiveRecorder.Web.Api +{ + [ApiController, Route("api/[controller]", Name = "[controller] [action]")] + public class ConfigController : ControllerBase + { + private readonly IMapper mapper; + private readonly IRecorder recorder; + + public ConfigController(IMapper mapper, IRecorder recorder) + { + this.mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + this.recorder = recorder ?? throw new ArgumentNullException(nameof(recorder)); + } + + /// + /// 获取软件默认设置 + /// + /// + [HttpGet("default")] + public ActionResult GetDefaultConfig() => DefaultConfig.Instance; + + /// + /// 获取全局设置 + /// + /// + [HttpGet("global")] + public ActionResult GetGlobalConfig() => this.mapper.Map(this.recorder.Config.Global); + + /// + /// 设置全局设置 + /// + /// + /// + [HttpPost("global")] + public ActionResult SetGlobalConfig([FromBody] SetGlobalConfig config) + { + config.ApplyTo(this.recorder.Config.Global); + return this.mapper.Map(this.recorder.Config.Global); + } + } +} diff --git a/BililiveRecorder.Web/Api/DataMappingProfile.cs b/BililiveRecorder.Web/Api/DataMappingProfile.cs new file mode 100644 index 0000000..6aae48b --- /dev/null +++ b/BililiveRecorder.Web/Api/DataMappingProfile.cs @@ -0,0 +1,23 @@ +using AutoMapper; +using BililiveRecorder.Core; +using BililiveRecorder.Core.Config.V2; +using BililiveRecorder.Web.Models; + +namespace BililiveRecorder.Web.Api +{ + public class DataMappingProfile : Profile + { + public DataMappingProfile() + { + this.CreateMap() + .ForMember(x => x.RoomId, x => x.MapFrom(s => s.RoomConfig.RoomId)) + .ForMember(x => x.AutoRecord, x => x.MapFrom(s => s.RoomConfig.AutoRecord)); + + this.CreateMap(); + + this.CreateMap(); + + this.CreateMap(); + } + } +} diff --git a/BililiveRecorder.Web/Api/RoomController.cs b/BililiveRecorder.Web/Api/RoomController.cs new file mode 100644 index 0000000..192c1bc --- /dev/null +++ b/BililiveRecorder.Web/Api/RoomController.cs @@ -0,0 +1,403 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using BililiveRecorder.Core; +using BililiveRecorder.Web.Models; +using BililiveRecorder.Web.Schemas.Types; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace BililiveRecorder.Web.Api +{ + [ApiController, Route("api/[controller]", Name = "[controller] [action]")] + public class RoomController : ControllerBase + { + private readonly IMapper mapper; + private readonly IRecorder recorder; + + public RoomController(IMapper mapper, IRecorder recorder) + { + this.mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + this.recorder = recorder ?? throw new ArgumentNullException(nameof(recorder)); + } + + private IRoom? FetchRoom(int roomId) => this.recorder.Rooms.FirstOrDefault(x => x.ShortId == roomId || x.RoomConfig.RoomId == roomId); + + private IRoom? FetchRoom(Guid objectId) => this.recorder.Rooms.FirstOrDefault(x => x.ObjectId == objectId); + + /// + /// 列出所有直播间 + /// + /// + [HttpGet] + public RoomDto[] GetRooms() => this.mapper.Map(this.recorder.Rooms); + + #region Create & Delete + + /// + /// 添加直播间 + /// + /// + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status400BadRequest)] + public ActionResult CreateRoom([FromBody] CreateRoomDto createRoom) + { + if (createRoom.RoomId <= 0) + return this.BadRequest(new RestApiError { Code = RestApiErrorCode.RoomidOutOfRange, Message = "Roomid must be greater than 0." }); + + var room = this.FetchRoom(createRoom.RoomId); + + if (room is not null) + return this.BadRequest(new RestApiError { Code = RestApiErrorCode.RoomExist, Message = "Can not add the same room multiple times." }); + + room = this.recorder.AddRoom(createRoom.RoomId, createRoom.AutoRecord); + + return this.mapper.Map(room); + } + + /// + /// 删除直播间 + /// + /// + /// + [HttpDelete("{roomId:int}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult DeleteRoom(int roomId) + { + var room = this.FetchRoom(roomId); + + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + + this.recorder.RemoveRoom(room); + + return this.mapper.Map(room); + } + + /// + /// 删除直播间 + /// + /// + /// + [HttpDelete("{objectId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult DeleteRoom(Guid objectId) + { + var room = this.FetchRoom(objectId); + + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + + this.recorder.RemoveRoom(room); + + return this.mapper.Map(room); + } + + #endregion + #region Get Room + + /// + /// 读取一个直播间 + /// + /// + /// + [HttpGet("{roomId:int}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult GetRoom(int roomId) + { + var room = this.FetchRoom(roomId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + return this.mapper.Map(room); + } + + /// + /// 读取一个直播间 + /// + /// + /// + [HttpGet("{objectId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult GetRoom(Guid objectId) + { + var room = this.FetchRoom(objectId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + return this.mapper.Map(room); + } + + #endregion + #region Get Room Stats + + /// + /// 读取直播间统计信息 + /// + /// + /// + [HttpGet("{roomId:int}/stats")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult GetRoomStats(int roomId) + { + var room = this.FetchRoom(roomId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + return this.mapper.Map(room.Stats); + } + + /// + /// 读取直播间统计信息 + /// + /// + /// + [HttpGet("{objectId:guid}/stats")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult GetRoomStats(Guid objectId) + { + var room = this.FetchRoom(objectId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + return this.mapper.Map(room.Stats); + } + + #endregion + #region Room Config + + /// + /// 读取直播间设置 + /// + /// + /// + [HttpGet("{roomId:int}/config")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult GetRoomConfig(int roomId) + { + var room = this.FetchRoom(roomId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + return this.mapper.Map(room.RoomConfig); + } + + /// + /// 读取直播间设置 + /// + /// + /// + [HttpGet("{objectId:guid}/config")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult GetRoomConfig(Guid objectId) + { + var room = this.FetchRoom(objectId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + return this.mapper.Map(room.RoomConfig); + } + + /// + /// 修改直播间设置 + /// + /// + /// + /// + [HttpPost("{roomId:int}/config")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult SetRoomConfig(int roomId, [FromBody] SetRoomConfig config) + { + var room = this.FetchRoom(roomId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + + config.ApplyTo(room.RoomConfig); + + return this.mapper.Map(room.RoomConfig); + } + + /// + /// 修改直播间设置 + /// + /// + /// + /// + [HttpPost("{objectId:guid}/config")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult SetRoomConfig(Guid objectId, [FromBody] SetRoomConfig config) + { + var room = this.FetchRoom(objectId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + + config.ApplyTo(room.RoomConfig); + + return this.mapper.Map(room.RoomConfig); + } + + #endregion + #region Room Action + + /// + /// 开始录制 + /// + /// + /// + [HttpPost("{roomId:int}/start")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult StartRecording(int roomId) + { + var room = this.FetchRoom(roomId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + + room.StartRecord(); + + return this.mapper.Map(room); + } + + /// + /// 开始录制 + /// + /// + /// + [HttpPost("{objectId:guid}/start")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult StartRecording(Guid objectId) + { + var room = this.FetchRoom(objectId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + + room.StartRecord(); + + return this.mapper.Map(room); + } + + /// + /// 停止录制 + /// + /// + /// + [HttpPost("{roomId:int}/stop")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult StopRecording(int roomId) + { + var room = this.FetchRoom(roomId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + + room.StopRecord(); + + return this.mapper.Map(room); + } + + /// + /// 停止录制 + /// + /// + /// + [HttpPost("{objectId:guid}/stop")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult StopRecording(Guid objectId) + { + var room = this.FetchRoom(objectId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + + room.StopRecord(); + + return this.mapper.Map(room); + } + + /// + /// 手动分段 + /// + /// + /// + [HttpPost("{roomId:int}/split")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult SplitRecording(int roomId) + { + var room = this.FetchRoom(roomId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + + room.SplitOutput(); + + return this.mapper.Map(room); + } + + /// + /// 手动分段 + /// + /// + /// + [HttpPost("{objectId:guid}/split")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public ActionResult SplitRecording(Guid objectId) + { + var room = this.FetchRoom(objectId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + + room.SplitOutput(); + + return this.mapper.Map(room); + } + + /// + /// 刷新直播间信息 + /// + /// + /// + [HttpPost("{roomId:int}/refresh")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public async Task> RefreshRecordingAsync(int roomId) + { + var room = this.FetchRoom(roomId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + + await room.RefreshRoomInfoAsync().ConfigureAwait(false); + + return this.mapper.Map(room); + } + + /// + /// 刷新直播间信息 + /// + /// + /// + [HttpPost("{objectId:guid}/refresh")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] + public async Task> RefreshRecordingAsync(Guid objectId) + { + var room = this.FetchRoom(objectId); + if (room is null) + return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); + + await room.RefreshRoomInfoAsync().ConfigureAwait(false); + + return this.mapper.Map(room); + } + + #endregion + } +} diff --git a/BililiveRecorder.Web/Api/VersionController.cs b/BililiveRecorder.Web/Api/VersionController.cs new file mode 100644 index 0000000..1d5bdaf --- /dev/null +++ b/BililiveRecorder.Web/Api/VersionController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; + +namespace BililiveRecorder.Web.Api +{ + [ApiController, Route("api/[controller]", Name = "[controller] [action]")] + public class VersionController : ControllerBase + { + /// + /// 读取软件版本信息 + /// + /// + [HttpGet] + public Schemas.Types.RecorderVersion GetVersion() => Schemas.Types.RecorderVersion.Instance; + } +} diff --git a/BililiveRecorder.Web/BililiveRecorder.Web.csproj b/BililiveRecorder.Web/BililiveRecorder.Web.csproj index d2f65b6..4daca37 100644 --- a/BililiveRecorder.Web/BililiveRecorder.Web.csproj +++ b/BililiveRecorder.Web/BililiveRecorder.Web.csproj @@ -2,10 +2,15 @@ net5.0 + True + 1701;1702;1591 + + + diff --git a/BililiveRecorder.Web/FakeRecorderForWeb.cs b/BililiveRecorder.Web/FakeRecorderForWeb.cs index 13e665b..4aa4ce2 100644 --- a/BililiveRecorder.Web/FakeRecorderForWeb.cs +++ b/BililiveRecorder.Web/FakeRecorderForWeb.cs @@ -15,6 +15,7 @@ namespace BililiveRecorder.Web public ReadOnlyObservableCollection Rooms { get; } = new ReadOnlyObservableCollection(new ObservableCollection()); +#pragma warning disable CS0067 public event EventHandler>? RecordSessionStarted; public event EventHandler>? RecordSessionEnded; public event EventHandler>? RecordFileOpening; @@ -22,6 +23,7 @@ namespace BililiveRecorder.Web public event EventHandler>? NetworkingStats; public event EventHandler>? RecordingStats; public event PropertyChangedEventHandler? PropertyChanged; +#pragma warning restore CS0067 public IRoom AddRoom(int roomid) => null!; diff --git a/BililiveRecorder.Web/Models/CreateRoomDto.cs b/BililiveRecorder.Web/Models/CreateRoomDto.cs new file mode 100644 index 0000000..593dc60 --- /dev/null +++ b/BililiveRecorder.Web/Models/CreateRoomDto.cs @@ -0,0 +1,8 @@ +namespace BililiveRecorder.Web.Models +{ + public class CreateRoomDto + { + public int RoomId { get; set; } + public bool AutoRecord { get; set; } + } +} diff --git a/BililiveRecorder.Web/Models/GlobalConfigDto.cs b/BililiveRecorder.Web/Models/GlobalConfigDto.cs new file mode 100644 index 0000000..23330fa --- /dev/null +++ b/BililiveRecorder.Web/Models/GlobalConfigDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BililiveRecorder.Web.Models +{ + public class GlobalConfigDto + { + + } +} diff --git a/BililiveRecorder.Web/Models/RestApiError.cs b/BililiveRecorder.Web/Models/RestApiError.cs new file mode 100644 index 0000000..587646e --- /dev/null +++ b/BililiveRecorder.Web/Models/RestApiError.cs @@ -0,0 +1,9 @@ +namespace BililiveRecorder.Web.Models +{ + public class RestApiError + { + public RestApiErrorCode Code { get; set; } + + public string Message { get; set; } = string.Empty; + } +} diff --git a/BililiveRecorder.Web/Models/RestApiErrorCode.cs b/BililiveRecorder.Web/Models/RestApiErrorCode.cs new file mode 100644 index 0000000..233d4c2 --- /dev/null +++ b/BililiveRecorder.Web/Models/RestApiErrorCode.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; + +namespace BililiveRecorder.Web.Models +{ + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum RestApiErrorCode + { + /// + /// 错误 + /// + Unknown, + /// + /// 房间号不在允许的范围内 + /// + RoomidOutOfRange, + /// + /// 房间已存在 + /// + RoomExist, + /// + /// 房间不存在 + /// + RoomNotFound, + } +} diff --git a/BililiveRecorder.Web/Models/RoomConfigDto.cs b/BililiveRecorder.Web/Models/RoomConfigDto.cs new file mode 100644 index 0000000..b9f228f --- /dev/null +++ b/BililiveRecorder.Web/Models/RoomConfigDto.cs @@ -0,0 +1,7 @@ +namespace BililiveRecorder.Web.Models +{ + public class RoomConfigDto + { + // TODO auto generate this + } +} diff --git a/BililiveRecorder.Web/Models/RoomDto.cs b/BililiveRecorder.Web/Models/RoomDto.cs new file mode 100644 index 0000000..a6d1678 --- /dev/null +++ b/BililiveRecorder.Web/Models/RoomDto.cs @@ -0,0 +1,20 @@ +using System; + +namespace BililiveRecorder.Web.Models +{ + public class RoomDto + { + public Guid ObjectId { get; set; } + public int RoomId { get; set; } + public bool AutoRecord { get; set; } + public int ShortId { get; set; } + public string? Name { get; set; } + public string? Title { get; set; } + public string? AreaNameParent { get; set; } + public string? AreaNameChild { get; set; } + public bool Recording { get; set; } + public bool Streaming { get; set; } + public bool DanmakuConnected { get; set; } + public bool AutoRecordForThisSession { get; set; } + } +} diff --git a/BililiveRecorder.Web/Models/RoomStatsDto.cs b/BililiveRecorder.Web/Models/RoomStatsDto.cs new file mode 100644 index 0000000..1a99499 --- /dev/null +++ b/BililiveRecorder.Web/Models/RoomStatsDto.cs @@ -0,0 +1,15 @@ +using System; + +namespace BililiveRecorder.Web.Models +{ + public class RoomStatsDto + { + public TimeSpan SessionDuration { get; set; } + public TimeSpan SessionMaxTimestamp { get; set; } + public TimeSpan FileMaxTimestamp { get; set; } + public double DurationRatio { get; set; } + public long TotalInputBytes { get; set; } + public long TotalOutputBytes { get; set; } + public double NetworkMbps { get; set; } + } +} diff --git a/BililiveRecorder.Web/Startup.cs b/BililiveRecorder.Web/Startup.cs index c075738..03301dd 100644 --- a/BililiveRecorder.Web/Startup.cs +++ b/BililiveRecorder.Web/Startup.cs @@ -1,5 +1,8 @@ +using System; using System.Diagnostics; +using System.IO; using BililiveRecorder.Core; +using BililiveRecorder.Web.Api; using BililiveRecorder.Web.Schemas; using GraphQL; using GraphQL.Server; @@ -13,6 +16,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; namespace BililiveRecorder.Web { @@ -38,6 +42,12 @@ namespace BililiveRecorder.Web services.TryAddSingleton(new FakeRecorderForWeb()); + services.AddAutoMapper(c => + { + c.AddProfile(); + }); + + // Graphql API services .AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader())) .AddSingleton() @@ -49,34 +59,77 @@ namespace BililiveRecorder.Web var logger = provider.GetRequiredService>(); options.UnhandledExceptionDelegate = ctx => logger.LogWarning(ctx.OriginalException, "Unhandled GraphQL Exception"); }) + .AddDefaultEndpointSelectorPolicy() //.AddSystemTextJson() .AddNewtonsoftJson() .AddWebSockets() .AddGraphTypes(typeof(RecorderSchema)) ; + + // REST API + services + .AddSwaggerGen(c => + { + c.SwaggerDoc("brec", new OpenApiInfo + { + Title = "录播姬 REST API", + Description = "录播姬网站 [rec.danmuji.org](https://rec.danmuji.org/) \n录播姬 GitHub [Bililive/BililiveRecorder](https://github.com/Bililive/BililiveRecorder) \n\n除了 REST API 以外,录播姬还有 Graphql API 可以使用。", + Version = "v1" + }); + + var filePath = Path.Combine(AppContext.BaseDirectory, "BililiveRecorder.Web.xml"); + c.IncludeXmlComments(filePath); + }) + .AddRouting(c => + { + c.LowercaseUrls = true; + c.LowercaseQueryStrings = true; + }) + .AddMvcCore(option => + { + + }) + .AddApiExplorer(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) => app .UseCors() .UseWebSockets() - .UseGraphQLWebSockets() - .UseGraphQL() - .UseGraphQLPlayground() - .UseGraphQLGraphiQL() - .UseGraphQLAltair() - .UseGraphQLVoyager() - .Use(next => async context => + .UseRouting() + .UseEndpoints(endpoints => { - if (context.Request.Path == "/") + endpoints.MapControllers(); + + endpoints.MapGraphQL(); + endpoints.MapGraphQLWebSockets(); + + endpoints.MapSwagger(); + endpoints.MapGraphQLPlayground(); + endpoints.MapGraphQLGraphiQL(); + endpoints.MapGraphQLAltair(); + endpoints.MapGraphQLVoyager(); + + endpoints.MapGet("/", async context => { context.Response.ContentType = "text/html"; await context.Response.WriteAsync(ConstStrings.HOME_PAGE_HTML, encoding: System.Text.Encoding.UTF8).ConfigureAwait(false); - } - else + }); + + endpoints.MapGet("favicon.ico", async context => { - context.Response.Redirect("/"); - } + context.Response.StatusCode = 404; + await context.Response.WriteAsync(string.Empty); + }); + }) + .UseSwaggerUI(c => + { + c.SwaggerEndpoint("brec/swagger.json", "录播姬 REST API"); + }) + .Use(next => async context => + { + context.Response.Redirect("/"); + await context.Response.WriteAsync(string.Empty); }) ; }