From cce7d1c6904d17080b96b76e86a888b32a580730 Mon Sep 17 00:00:00 2001 From: Genteure Date: Fri, 1 Jan 2021 14:46:27 +0800 Subject: [PATCH] Add config v2 --- BililiveRecorder.Cli/Program.cs | 2 +- BililiveRecorder.Core/AssemblyAttribute.cs | 3 + BililiveRecorder.Core/BasicDanmakuWriter.cs | 150 +++---- BililiveRecorder.Core/BililiveAPI.cs | 40 +- .../BililiveRecorder.Core.csproj | 2 + .../Callback/BasicWebhook.cs | 10 +- BililiveRecorder.Core/Config/ConfigBase.cs | 15 + BililiveRecorder.Core/Config/ConfigMapper.cs | 68 +++ BililiveRecorder.Core/Config/ConfigParser.cs | 137 +++--- BililiveRecorder.Core/Config/ConfigWrapper.cs | 20 - .../Config/{ => V1}/ConfigV1.cs | 59 +-- .../Config/V1/ConfigV1Wrapper.cs | 13 + .../Config/{ => V1}/RoomV1.cs | 4 +- BililiveRecorder.Core/Config/V2/Config.gen.cs | 382 +++++++++++++++++ BililiveRecorder.Core/Config/V2/ConfigV2.cs | 44 ++ .../Config/V2/build_config.data.js | 126 ++++++ .../Config/V2/build_config.js | 79 ++++ BililiveRecorder.Core/CoreModule.cs | 9 +- BililiveRecorder.Core/DanmakuModel.cs | 78 ++-- BililiveRecorder.Core/IRecordedRoom.cs | 3 + BililiveRecorder.Core/IRecorder.cs | 5 +- BililiveRecorder.Core/IStreamMonitor.cs | 1 - BililiveRecorder.Core/RecordedRoom.cs | 395 +++++++++--------- BililiveRecorder.Core/Recorder.cs | 143 ++++--- BililiveRecorder.Core/Repeat.cs | 2 +- BililiveRecorder.Core/StreamMonitor.cs | 173 ++++---- BililiveRecorder.Core/StreamStatus.cs | 6 +- .../FlvClipProcessor.cs | 48 +-- BililiveRecorder.FlvProcessor/FlvMetadata.cs | 52 +-- .../FlvStreamProcessor.cs | 214 +++++----- BililiveRecorder.FlvProcessor/FlvTag.cs | 48 +-- BililiveRecorder.WPF/App.xaml.cs | 2 +- .../BililiveRecorder.WPF.csproj | 14 + .../Controls/AddRoomCard.xaml.cs | 12 +- .../Controls/AddRoomFailedDialog.xaml.cs | 2 +- .../Controls/CloseWindowConfirmDialog.xaml.cs | 2 +- .../Controls/DeleteRoomConfirmDialog.xaml.cs | 2 +- .../Controls/PerRoomSettingsDialog.xaml | 87 ++++ .../Controls/PerRoomSettingsDialog.xaml.cs | 28 ++ BililiveRecorder.WPF/Controls/RoomCard.xaml | 8 +- .../Controls/RoomCard.xaml.cs | 25 +- .../Controls/SettingWithDefault.xaml | 28 ++ .../Controls/SettingWithDefault.xaml.cs | 49 +++ .../Controls/TaskbarIconControl.xaml | 2 +- .../Controls/TaskbarIconControl.xaml.cs | 5 +- .../WorkDirectorySelectorDialog.xaml.cs | 12 +- .../Converters/BoolToValueConverter.cs | 10 +- .../Converters/MultiBoolToValueConverter.cs | 4 +- .../Converters/NullValueTemplateSelector.cs | 2 +- .../Converters/RoomListInterceptConverter.cs | 11 +- .../MockData/MockRecordedRoom.cs | 41 +- BililiveRecorder.WPF/MockData/MockRecorder.cs | 64 +-- BililiveRecorder.WPF/Models/Commands.cs | 6 +- BililiveRecorder.WPF/Models/LogModel.cs | 20 +- BililiveRecorder.WPF/Models/RootModel.cs | 14 +- BililiveRecorder.WPF/NewMainWindow.xaml.cs | 44 +- .../Pages/AdvancedSettingsPage.xaml | 71 ++-- .../Pages/AdvancedSettingsPage.xaml.cs | 2 +- .../Pages/AnnouncementPage.xaml.cs | 24 +- BililiveRecorder.WPF/Pages/LogPage.xaml.cs | 4 +- BililiveRecorder.WPF/Pages/RoomListPage.xaml | 6 +- .../Pages/RoomListPage.xaml.cs | 145 ++++--- BililiveRecorder.WPF/Pages/RootPage.xaml.cs | 52 +-- BililiveRecorder.WPF/Pages/SettingsPage.xaml | 21 +- .../Pages/SettingsPage.xaml.cs | 17 +- .../Properties/AssemblyInfo.cs | 2 - BililiveRecorder.sln | 11 + .../BililiveRecorder.UnitTest.Core.csproj | 27 ++ .../ConfigTests.cs | 80 ++++ 69 files changed, 2199 insertions(+), 1088 deletions(-) create mode 100644 BililiveRecorder.Core/AssemblyAttribute.cs create mode 100644 BililiveRecorder.Core/Config/ConfigBase.cs create mode 100644 BililiveRecorder.Core/Config/ConfigMapper.cs delete mode 100644 BililiveRecorder.Core/Config/ConfigWrapper.cs rename BililiveRecorder.Core/Config/{ => V1}/ConfigV1.cs (63%) create mode 100644 BililiveRecorder.Core/Config/V1/ConfigV1Wrapper.cs rename BililiveRecorder.Core/Config/{ => V1}/RoomV1.cs (79%) create mode 100644 BililiveRecorder.Core/Config/V2/Config.gen.cs create mode 100644 BililiveRecorder.Core/Config/V2/ConfigV2.cs create mode 100644 BililiveRecorder.Core/Config/V2/build_config.data.js create mode 100644 BililiveRecorder.Core/Config/V2/build_config.js create mode 100644 BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml create mode 100644 BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml.cs create mode 100644 BililiveRecorder.WPF/Controls/SettingWithDefault.xaml create mode 100644 BililiveRecorder.WPF/Controls/SettingWithDefault.xaml.cs create mode 100644 test/BililiveRecorder.UnitTest.Core/BililiveRecorder.UnitTest.Core.csproj create mode 100644 test/BililiveRecorder.UnitTest.Core/ConfigTests.cs diff --git a/BililiveRecorder.Cli/Program.cs b/BililiveRecorder.Cli/Program.cs index 2d42ece..d4f4604 100644 --- a/BililiveRecorder.Cli/Program.cs +++ b/BililiveRecorder.Cli/Program.cs @@ -5,7 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Autofac; using BililiveRecorder.Core; -using BililiveRecorder.Core.Config; +using BililiveRecorder.Core.Config.V1; using BililiveRecorder.FlvProcessor; using CommandLine; using Newtonsoft.Json; diff --git a/BililiveRecorder.Core/AssemblyAttribute.cs b/BililiveRecorder.Core/AssemblyAttribute.cs new file mode 100644 index 0000000..d73562b --- /dev/null +++ b/BililiveRecorder.Core/AssemblyAttribute.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("BililiveRecorder.UnitTest.Core")] diff --git a/BililiveRecorder.Core/BasicDanmakuWriter.cs b/BililiveRecorder.Core/BasicDanmakuWriter.cs index 737c513..eaa8e7e 100644 --- a/BililiveRecorder.Core/BasicDanmakuWriter.cs +++ b/BililiveRecorder.Core/BasicDanmakuWriter.cs @@ -1,11 +1,12 @@ -using BililiveRecorder.Core.Config; using System; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Xml; +using BililiveRecorder.Core.Config.V2; +#nullable enable namespace BililiveRecorder.Core { public class BasicDanmakuWriter : IBasicDanmakuWriter @@ -21,12 +22,12 @@ namespace BililiveRecorder.Core private static readonly Regex invalidXMLChars = new Regex(@"(? string.IsNullOrEmpty(text) ? string.Empty : invalidXMLChars.Replace(text, string.Empty); - private XmlWriter xmlWriter = null; + private XmlWriter? xmlWriter = null; private DateTimeOffset offset = DateTimeOffset.UtcNow; private uint writeCount = 0; - private readonly ConfigV1 config; + private readonly RoomConfig config; - public BasicDanmakuWriter(ConfigV1 config) + public BasicDanmakuWriter(RoomConfig config) { this.config = config ?? throw new ArgumentNullException(nameof(config)); } @@ -35,62 +36,63 @@ namespace BililiveRecorder.Core public void EnableWithPath(string path, IRecordedRoom recordedRoom) { - if (disposedValue) return; + if (this.disposedValue) return; - semaphoreSlim.Wait(); + this.semaphoreSlim.Wait(); try { - if (xmlWriter != null) + if (this.xmlWriter != null) { - xmlWriter.Close(); - xmlWriter.Dispose(); - xmlWriter = null; + this.xmlWriter.Close(); + this.xmlWriter.Dispose(); + this.xmlWriter = null; } try { Directory.CreateDirectory(Path.GetDirectoryName(path)); } catch (Exception) { } var stream = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.Read); - xmlWriter = XmlWriter.Create(stream, xmlWriterSettings); - WriteStartDocument(xmlWriter, recordedRoom); - offset = DateTimeOffset.UtcNow; - writeCount = 0; + this.xmlWriter = XmlWriter.Create(stream, xmlWriterSettings); + this.WriteStartDocument(this.xmlWriter, recordedRoom); + this.offset = DateTimeOffset.UtcNow; + this.writeCount = 0; } finally { - semaphoreSlim.Release(); + this.semaphoreSlim.Release(); } } public void Disable() { - if (disposedValue) return; + if (this.disposedValue) return; - semaphoreSlim.Wait(); + this.semaphoreSlim.Wait(); try { - if (xmlWriter != null) + if (this.xmlWriter != null) { - xmlWriter.Close(); - xmlWriter.Dispose(); - xmlWriter = null; + this.xmlWriter.Close(); + this.xmlWriter.Dispose(); + this.xmlWriter = null; } } finally { - semaphoreSlim.Release(); + this.semaphoreSlim.Release(); } } public void Write(DanmakuModel danmakuModel) { - if (disposedValue) return; + if (this.disposedValue) return; - semaphoreSlim.Wait(); + this.semaphoreSlim.Wait(); try { - if (xmlWriter != null) + if (this.xmlWriter != null) { var write = true; + var recordDanmakuRaw = this.config.RecordDanmakuRaw; switch (danmakuModel.MsgType) { case MsgTypeEnum.Comment: @@ -99,58 +101,58 @@ namespace BililiveRecorder.Core var size = danmakuModel.RawObj?["info"]?[0]?[2]?.ToObject() ?? 25; var color = danmakuModel.RawObj?["info"]?[0]?[3]?.ToObject() ?? 0XFFFFFF; var st = danmakuModel.RawObj?["info"]?[0]?[4]?.ToObject() ?? 0L; - var ts = Math.Max((DateTimeOffset.FromUnixTimeMilliseconds(st) - offset).TotalSeconds, 0d); + var ts = Math.Max((DateTimeOffset.FromUnixTimeMilliseconds(st) - this.offset).TotalSeconds, 0d); - xmlWriter.WriteStartElement("d"); - xmlWriter.WriteAttributeString("p", $"{ts},{type},{size},{color},{st},0,{danmakuModel.UserID},0"); - xmlWriter.WriteAttributeString("user", danmakuModel.UserName); - if (config.RecordDanmakuRaw) - xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["info"]?.ToString(Newtonsoft.Json.Formatting.None)); - xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText)); - xmlWriter.WriteEndElement(); + this.xmlWriter.WriteStartElement("d"); + this.xmlWriter.WriteAttributeString("p", $"{ts},{type},{size},{color},{st},0,{danmakuModel.UserID},0"); + this.xmlWriter.WriteAttributeString("user", danmakuModel.UserName); + if (recordDanmakuRaw) + this.xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["info"]?.ToString(Newtonsoft.Json.Formatting.None)); + this.xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText)); + this.xmlWriter.WriteEndElement(); } break; case MsgTypeEnum.SuperChat: - if (config.RecordDanmakuSuperChat) + if (this.config.RecordDanmakuSuperChat) { - xmlWriter.WriteStartElement("sc"); - var ts = Math.Max((DateTimeOffset.UtcNow - offset).TotalSeconds, 0d); - xmlWriter.WriteAttributeString("ts", ts.ToString()); - xmlWriter.WriteAttributeString("user", danmakuModel.UserName); - xmlWriter.WriteAttributeString("price", danmakuModel.Price.ToString()); - xmlWriter.WriteAttributeString("time", danmakuModel.SCKeepTime.ToString()); - if (config.RecordDanmakuRaw) - xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); - xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText)); - xmlWriter.WriteEndElement(); + this.xmlWriter.WriteStartElement("sc"); + var ts = Math.Max((DateTimeOffset.UtcNow - this.offset).TotalSeconds, 0d); + this.xmlWriter.WriteAttributeString("ts", ts.ToString()); + this.xmlWriter.WriteAttributeString("user", danmakuModel.UserName); + this.xmlWriter.WriteAttributeString("price", danmakuModel.Price.ToString()); + this.xmlWriter.WriteAttributeString("time", danmakuModel.SCKeepTime.ToString()); + if (recordDanmakuRaw) + this.xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); + this.xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText)); + this.xmlWriter.WriteEndElement(); } break; case MsgTypeEnum.GiftSend: - if (config.RecordDanmakuGift) + if (this.config.RecordDanmakuGift) { - xmlWriter.WriteStartElement("gift"); - var ts = Math.Max((DateTimeOffset.UtcNow - offset).TotalSeconds, 0d); - xmlWriter.WriteAttributeString("ts", ts.ToString()); - xmlWriter.WriteAttributeString("user", danmakuModel.UserName); - xmlWriter.WriteAttributeString("giftname", danmakuModel.GiftName); - xmlWriter.WriteAttributeString("giftcount", danmakuModel.GiftCount.ToString()); - if (config.RecordDanmakuRaw) - xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); - xmlWriter.WriteEndElement(); + this.xmlWriter.WriteStartElement("gift"); + var ts = Math.Max((DateTimeOffset.UtcNow - this.offset).TotalSeconds, 0d); + this.xmlWriter.WriteAttributeString("ts", ts.ToString()); + this.xmlWriter.WriteAttributeString("user", danmakuModel.UserName); + this.xmlWriter.WriteAttributeString("giftname", danmakuModel.GiftName); + this.xmlWriter.WriteAttributeString("giftcount", danmakuModel.GiftCount.ToString()); + if (recordDanmakuRaw) + this.xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); + this.xmlWriter.WriteEndElement(); } break; case MsgTypeEnum.GuardBuy: - if (config.RecordDanmakuGuard) + if (this.config.RecordDanmakuGuard) { - xmlWriter.WriteStartElement("guard"); - var ts = Math.Max((DateTimeOffset.UtcNow - offset).TotalSeconds, 0d); - xmlWriter.WriteAttributeString("ts", ts.ToString()); - xmlWriter.WriteAttributeString("user", danmakuModel.UserName); - xmlWriter.WriteAttributeString("level", danmakuModel.UserGuardLevel.ToString()); ; - xmlWriter.WriteAttributeString("count", danmakuModel.GiftCount.ToString()); - if (config.RecordDanmakuRaw) - xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); - xmlWriter.WriteEndElement(); + this.xmlWriter.WriteStartElement("guard"); + var ts = Math.Max((DateTimeOffset.UtcNow - this.offset).TotalSeconds, 0d); + this.xmlWriter.WriteAttributeString("ts", ts.ToString()); + this.xmlWriter.WriteAttributeString("user", danmakuModel.UserName); + this.xmlWriter.WriteAttributeString("level", danmakuModel.UserGuardLevel.ToString()); ; + this.xmlWriter.WriteAttributeString("count", danmakuModel.GiftCount.ToString()); + if (recordDanmakuRaw) + this.xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); + this.xmlWriter.WriteEndElement(); } break; default: @@ -158,16 +160,16 @@ namespace BililiveRecorder.Core break; } - if (write && writeCount++ >= config.RecordDanmakuFlushInterval) + if (write && this.writeCount++ >= this.config.RecordDanmakuFlushInterval) { - xmlWriter.Flush(); - writeCount = 0; + this.xmlWriter.Flush(); + this.writeCount = 0; } } } finally { - semaphoreSlim.Release(); + this.semaphoreSlim.Release(); } } @@ -202,26 +204,26 @@ namespace BililiveRecorder.Core protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!this.disposedValue) { if (disposing) { // dispose managed state (managed objects) - semaphoreSlim.Dispose(); - xmlWriter?.Close(); - xmlWriter?.Dispose(); + this.semaphoreSlim.Dispose(); + this.xmlWriter?.Close(); + this.xmlWriter?.Dispose(); } // free unmanaged resources (unmanaged objects) and override finalizer // set large fields to null - disposedValue = true; + this.disposedValue = true; } } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + this.Dispose(disposing: true); GC.SuppressFinalize(this); } } diff --git a/BililiveRecorder.Core/BililiveAPI.cs b/BililiveRecorder.Core/BililiveAPI.cs index 813fd53..44beead 100644 --- a/BililiveRecorder.Core/BililiveAPI.cs +++ b/BililiveRecorder.Core/BililiveAPI.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; -using BililiveRecorder.Core.Config; +using BililiveRecorder.Core.Config.V2; using Newtonsoft.Json.Linq; using NLog; @@ -20,26 +20,24 @@ namespace BililiveRecorder.Core private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private static readonly Random random = new Random(); - private readonly ConfigV1 Config; + private readonly GlobalConfig globalConfig; private readonly HttpClient danmakuhttpclient; private HttpClient httpclient; - public BililiveAPI(ConfigV1 config) + public BililiveAPI(GlobalConfig globalConfig) { - Config = config; - Config.PropertyChanged += (sender, e) => + this.globalConfig = globalConfig; + this.globalConfig.PropertyChanged += (sender, e) => { - if (e.PropertyName == nameof(Config.Cookie)) - { - ApplyCookieSettings(Config.Cookie); - } + if (e.PropertyName == nameof(this.globalConfig.Cookie)) + this.ApplyCookieSettings(this.globalConfig.Cookie); }; - ApplyCookieSettings(Config.Cookie); + this.ApplyCookieSettings(this.globalConfig.Cookie); - danmakuhttpclient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; - danmakuhttpclient.DefaultRequestHeaders.Add("Accept", HTTP_HEADER_ACCEPT); - danmakuhttpclient.DefaultRequestHeaders.Add("Referer", HTTP_HEADER_REFERER); - danmakuhttpclient.DefaultRequestHeaders.Add("User-Agent", Utils.UserAgent); + this.danmakuhttpclient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; + this.danmakuhttpclient.DefaultRequestHeaders.Add("Accept", HTTP_HEADER_ACCEPT); + this.danmakuhttpclient.DefaultRequestHeaders.Add("Referer", HTTP_HEADER_REFERER); + this.danmakuhttpclient.DefaultRequestHeaders.Add("User-Agent", Utils.UserAgent); } public void ApplyCookieSettings(string cookie_string) @@ -61,7 +59,7 @@ namespace BililiveRecorder.Core pclient.DefaultRequestHeaders.Add("Referer", HTTP_HEADER_REFERER); pclient.DefaultRequestHeaders.Add("User-Agent", Utils.UserAgent); pclient.DefaultRequestHeaders.Add("Cookie", cookie_string); - httpclient = pclient; + this.httpclient = pclient; } else { @@ -69,7 +67,7 @@ namespace BililiveRecorder.Core cleanclient.DefaultRequestHeaders.Add("Accept", HTTP_HEADER_ACCEPT); cleanclient.DefaultRequestHeaders.Add("Referer", HTTP_HEADER_REFERER); cleanclient.DefaultRequestHeaders.Add("User-Agent", Utils.UserAgent); - httpclient = cleanclient; + this.httpclient = cleanclient; } logger.Debug("设置 Cookie 成功"); } @@ -109,9 +107,9 @@ namespace BililiveRecorder.Core /// public async Task GetPlayUrlAsync(int roomid) { - var url = $@"{Config.LiveApiHost}/room/v1/Room/playUrl?cid={roomid}&quality=4&platform=web"; + var url = $@"{this.globalConfig.LiveApiHost}/room/v1/Room/playUrl?cid={roomid}&quality=4&platform=web"; // 随机选择一个 url - if ((await HttpGetJsonAsync(httpclient, url))?["data"]?["durl"] is JArray array) + if ((await this.HttpGetJsonAsync(this.httpclient, url))?["data"]?["durl"] is JArray array) { var urls = array.Select(t => t?["url"]?.ToObject()); var distinct = urls.Distinct().ToArray(); @@ -134,14 +132,14 @@ namespace BililiveRecorder.Core { try { - var room = await HttpGetJsonAsync(httpclient, $@"https://api.live.bilibili.com/room/v1/Room/get_info?id={roomid}"); + var room = await this.HttpGetJsonAsync(this.httpclient, $@"https://api.live.bilibili.com/room/v1/Room/get_info?id={roomid}"); if (room?["code"]?.ToObject() != 0) { logger.Warn("不能获取 {roomid} 的信息1: {errormsg}", roomid, room?["message"]?.ToObject() ?? "网络超时"); return null; } - var user = await HttpGetJsonAsync(httpclient, $@"https://api.live.bilibili.com/live_user/v1/UserInfo/get_anchor_in_room?roomid={roomid}"); + var user = await this.HttpGetJsonAsync(this.httpclient, $@"https://api.live.bilibili.com/live_user/v1/UserInfo/get_anchor_in_room?roomid={roomid}"); if (user?["code"]?.ToObject() != 0) { logger.Warn("不能获取 {roomid} 的信息2: {errormsg}", roomid, user?["message"]?.ToObject() ?? "网络超时"); @@ -174,7 +172,7 @@ namespace BililiveRecorder.Core { try { - var result = await HttpGetJsonAsync(danmakuhttpclient, $@"https://api.live.bilibili.com/room/v1/Danmu/getConf?room_id={roomid}&platform=pc&player=web"); + var result = await this.HttpGetJsonAsync(this.danmakuhttpclient, $@"https://api.live.bilibili.com/room/v1/Danmu/getConf?room_id={roomid}&platform=pc&player=web"); if (result?["code"]?.ToObject() == 0) { diff --git a/BililiveRecorder.Core/BililiveRecorder.Core.csproj b/BililiveRecorder.Core/BililiveRecorder.Core.csproj index 573d2aa..7161adb 100644 --- a/BililiveRecorder.Core/BililiveRecorder.Core.csproj +++ b/BililiveRecorder.Core/BililiveRecorder.Core.csproj @@ -20,6 +20,8 @@ + + diff --git a/BililiveRecorder.Core/Callback/BasicWebhook.cs b/BililiveRecorder.Core/Callback/BasicWebhook.cs index d92ffdd..b563f77 100644 --- a/BililiveRecorder.Core/Callback/BasicWebhook.cs +++ b/BililiveRecorder.Core/Callback/BasicWebhook.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; -using BililiveRecorder.Core.Config; +using BililiveRecorder.Core.Config.V2; using Newtonsoft.Json; using NLog; @@ -15,7 +15,7 @@ namespace BililiveRecorder.Core.Callback private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private static readonly HttpClient client; - private readonly ConfigV1 Config; + private readonly ConfigV2 Config; static BasicWebhook() { @@ -23,21 +23,21 @@ namespace BililiveRecorder.Core.Callback client.DefaultRequestHeaders.Add("User-Agent", $"BililiveRecorder/{typeof(BasicWebhook).Assembly.GetName().Version}-{BuildInfo.HeadShaShort}"); } - public BasicWebhook(ConfigV1 config) + public BasicWebhook(ConfigV2 config) { this.Config = config ?? throw new ArgumentNullException(nameof(config)); } public async void Send(RecordEndData data) { - var urls = this.Config.WebHookUrls; + var urls = this.Config.Global.WebHookUrls; if (string.IsNullOrWhiteSpace(urls)) return; var dataStr = JsonConvert.SerializeObject(data, Formatting.None); using var body = new ByteArrayContent(Encoding.UTF8.GetBytes(dataStr)); body.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - var tasks = urls + var tasks = urls! .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.Trim()) .Where(x => !string.IsNullOrWhiteSpace(x)) diff --git a/BililiveRecorder.Core/Config/ConfigBase.cs b/BililiveRecorder.Core/Config/ConfigBase.cs new file mode 100644 index 0000000..055f14d --- /dev/null +++ b/BililiveRecorder.Core/Config/ConfigBase.cs @@ -0,0 +1,15 @@ +using JsonSubTypes; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace BililiveRecorder.Core.Config +{ + [JsonConverter(typeof(JsonSubtypes), nameof(Version))] + [JsonSubtypes.KnownSubType(typeof(V1.ConfigV1Wrapper), 1)] + [JsonSubtypes.KnownSubType(typeof(V2.ConfigV2), 2)] + public abstract class ConfigBase + { + [JsonProperty("version")] + public virtual int Version { get; internal protected set; } + } +} diff --git a/BililiveRecorder.Core/Config/ConfigMapper.cs b/BililiveRecorder.Core/Config/ConfigMapper.cs new file mode 100644 index 0000000..d2b5a49 --- /dev/null +++ b/BililiveRecorder.Core/Config/ConfigMapper.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using BililiveRecorder.FlvProcessor; + +#nullable enable +#pragma warning disable CS0612 // obsolete +namespace BililiveRecorder.Core.Config +{ + internal static class ConfigMapper + { + public static V2.ConfigV2 Map1To2(V1.ConfigV1 v1) + { + var map = new Dictionary(); + + AddMap(map, x => x.EnabledFeature, x => x.EnabledFeature); + AddMap(map, x => x.ClipLengthPast, x => x.ClipLengthPast); + AddMap(map, x => x.ClipLengthFuture, x => x.ClipLengthFuture); + AddMap(map, x => x.TimingStreamRetry, x => x.TimingStreamRetry); + AddMap(map, x => x.TimingStreamConnect, x => x.TimingStreamConnect); + AddMap(map, x => x.TimingDanmakuRetry, x => x.TimingDanmakuRetry); + AddMap(map, x => x.TimingCheckInterval, x => x.TimingCheckInterval); + AddMap(map, x => x.TimingWatchdogTimeout, x => x.TimingWatchdogTimeout); + AddMap(map, x => x.RecordDanmakuFlushInterval, x => x.RecordDanmakuFlushInterval); + AddMap(map, x => x.Cookie, x => x.Cookie); + AddMap(map, x => x.WebHookUrls, x => x.WebHookUrls); + AddMap(map, x => x.LiveApiHost, x => x.LiveApiHost); + AddMap(map, x => x.RecordFilenameFormat, x => x.RecordFilenameFormat); + AddMap(map, x => x.ClipFilenameFormat, x => x.ClipFilenameFormat); + + AddMap(map, x => x.CuttingMode, x => x.CuttingMode); + AddMap(map, x => x.CuttingNumber, x => x.CuttingNumber); + AddMap(map, x => x.RecordDanmaku, x => x.RecordDanmaku); + AddMap(map, x => x.RecordDanmakuRaw, x => x.RecordDanmakuRaw); + AddMap(map, x => x.RecordDanmakuSuperChat, x => x.RecordDanmakuSuperChat); + AddMap(map, x => x.RecordDanmakuGift, x => x.RecordDanmakuGift); + AddMap(map, x => x.RecordDanmakuGuard, x => x.RecordDanmakuGuard); + + var def = new V1.ConfigV1(); // old default + var v2 = new V2.ConfigV2(); + + foreach (var item in map) + { + var data = item.Key.GetValue(v1); + if (!(data?.Equals(item.Key.GetValue(def)) ?? true)) + item.Value.SetValue(v2.Global, data); + } + + v2.Rooms = v1.RoomList.Select(x => new V2.RoomConfig { RoomId = x.Roomid, AutoRecord = x.Enabled }).ToList(); + + return v2; + } + + private static void AddMap(Dictionary map, Expression> keyExpr, Expression> valueExpr) + { + var key = GetProperty(keyExpr); + var value = GetProperty(valueExpr); + if ((key is null) || (value is null)) + return; + map.Add(key, value); + } + + private static PropertyInfo? GetProperty(Expression> expression) + => (expression.Body as MemberExpression)?.Member as PropertyInfo; + } +} diff --git a/BililiveRecorder.Core/Config/ConfigParser.cs b/BililiveRecorder.Core/Config/ConfigParser.cs index a93e4d7..884b7e7 100644 --- a/BililiveRecorder.Core/Config/ConfigParser.cs +++ b/BililiveRecorder.Core/Config/ConfigParser.cs @@ -1,88 +1,111 @@ -using Newtonsoft.Json; using System; using System.IO; +using System.Text; +using Newtonsoft.Json; using NLog; +#nullable enable namespace BililiveRecorder.Core.Config { public static class ConfigParser { + private const string CONFIG_FILE_NAME = "config.json"; private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - public static bool Load(string directory, ConfigV1 config = null) + public static V2.ConfigV2? LoadFrom(string directory) { - if (!Directory.Exists(directory)) + try { - return false; - } + if (!Directory.Exists(directory)) + return null; - var filepath = Path.Combine(directory, "config.json"); - if (File.Exists(filepath)) - { - try + var filepath = Path.Combine(directory, CONFIG_FILE_NAME); + + if (!File.Exists(filepath)) { - var cw = JsonConvert.DeserializeObject(File.ReadAllText(filepath)); - switch (cw.Version) - { - case 1: - { - var v1 = JsonConvert.DeserializeObject(cw.Data); - v1.CopyPropertiesTo(config); - return true; - // (v1.ToV2()).CopyPropertiesTo(config); - } - /** - * case 2: - * { - * var v2 = JsonConvert.DeserializeObject(cw.Data); - * v2.CopyPropertiesTo(config); - * return true; - * } - * */ - default: - // version not supported - // TODO: return status enum - return false; - } - } - catch (Exception ex) - { - logger.Error(ex, "Failed to parse config!"); - return false; + logger.Debug("Config file does not exist. \"{path}\"", filepath); + return new V2.ConfigV2(); } + + logger.Debug("Loading config from path \"{path}\".", filepath); + var json = File.ReadAllText(filepath, Encoding.UTF8); + return LoadJson(json); + } - else + catch (Exception ex) { - new ConfigV1().CopyPropertiesTo(config); - return true; + logger.Error(ex, "从文件加载设置时出错"); + return null; } } - public static bool Save(string directory, ConfigV1 config = null) + public static V2.ConfigV2? LoadJson(string json) { - if (config == null) { config = new ConfigV1(); } - if (!Directory.Exists(directory)) - { - // User should create the directory - // TODO: return enum - return false; - } - var filepath = Path.Combine(directory, "config.json"); try { - var data = JsonConvert.SerializeObject(config); - var cw = JsonConvert.SerializeObject(new ConfigWrapper() + logger.Debug("Config json: {config}", json); + + var configBase = JsonConvert.DeserializeObject(json); + switch (configBase) { - Version = 1, - Data = data - }); - File.WriteAllText(filepath, cw); + case V1.ConfigV1Wrapper v1: + { + logger.Debug("读取到 config v1"); +#pragma warning disable CS0612 + var v1Data = JsonConvert.DeserializeObject(v1.Data); +#pragma warning restore CS0612 + var newConfig = ConfigMapper.Map1To2(v1Data); + + return newConfig; + } + case V2.ConfigV2 v2: + logger.Debug("读取到 config v2"); + return v2; + default: + logger.Error("读取到不支持的设置版本"); + return null; + } + } + catch (Exception ex) + { + logger.Error(ex, "解析设置时出错"); + return null; + } + } + + public static bool SaveTo(string directory, V2.ConfigV2 config) + { + var json = SaveJson(config); + try + { + if (!Directory.Exists(directory)) + return false; + + var filepath = Path.Combine(directory, CONFIG_FILE_NAME); + + if (json is not null) + File.WriteAllText(filepath, json, Encoding.UTF8); + return true; } - catch (Exception) + catch (Exception ex) { + logger.Error(ex, "保存设置时出错(写入文件)"); return false; - // TODO: Log Exception + } + } + + public static string? SaveJson(V2.ConfigV2 config) + { + try + { + var json = JsonConvert.SerializeObject(config); + return json; + } + catch (Exception ex) + { + logger.Error(ex, "保存设置时出错(序列化)"); + return null; } } } diff --git a/BililiveRecorder.Core/Config/ConfigWrapper.cs b/BililiveRecorder.Core/Config/ConfigWrapper.cs deleted file mode 100644 index 860a46b..0000000 --- a/BililiveRecorder.Core/Config/ConfigWrapper.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Newtonsoft.Json; - -namespace BililiveRecorder.Core.Config -{ - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - internal class ConfigWrapper - { - /// - /// Config Version - /// - [JsonProperty("version")] - public int Version { get; set; } - - /// - /// Config Data String - /// - [JsonProperty("data")] - public string Data { get; set; } - } -} diff --git a/BililiveRecorder.Core/Config/ConfigV1.cs b/BililiveRecorder.Core/Config/V1/ConfigV1.cs similarity index 63% rename from BililiveRecorder.Core/Config/ConfigV1.cs rename to BililiveRecorder.Core/Config/V1/ConfigV1.cs index 147bb32..4b93ddf 100644 --- a/BililiveRecorder.Core/Config/ConfigV1.cs +++ b/BililiveRecorder.Core/Config/V1/ConfigV1.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; @@ -5,8 +6,9 @@ using BililiveRecorder.FlvProcessor; using Newtonsoft.Json; using NLog; -namespace BililiveRecorder.Core.Config +namespace BililiveRecorder.Core.Config.V1 { + [Obsolete] [JsonObject(memberSerialization: MemberSerialization.OptIn)] public class ConfigV1 : INotifyPropertyChanged { @@ -17,7 +19,7 @@ namespace BililiveRecorder.Core.Config /// [JsonIgnore] [Utils.DoNotCopyProperty] - public string WorkDirectory { get => _workDirectory; set => SetField(ref _workDirectory, value); } + public string WorkDirectory { get => this._workDirectory; set => this.SetField(ref this._workDirectory, value); } /// @@ -30,122 +32,122 @@ namespace BililiveRecorder.Core.Config /// 启用的功能 /// [JsonProperty("feature")] - public EnabledFeature EnabledFeature { get => _enabledFeature; set => SetField(ref _enabledFeature, value); } + public EnabledFeature EnabledFeature { get => this._enabledFeature; set => this.SetField(ref this._enabledFeature, value); } /// /// 剪辑-过去的时长(秒) /// [JsonProperty("clip_length_future")] - public uint ClipLengthFuture { get => _clipLengthFuture; set => SetField(ref _clipLengthFuture, value); } + public uint ClipLengthFuture { get => this._clipLengthFuture; set => this.SetField(ref this._clipLengthFuture, value); } /// /// 剪辑-将来的时长(秒) /// [JsonProperty("clip_length_past")] - public uint ClipLengthPast { get => _clipLengthPast; set => SetField(ref _clipLengthPast, value); } + public uint ClipLengthPast { get => this._clipLengthPast; set => this.SetField(ref this._clipLengthPast, value); } /// /// 自动切割模式 /// [JsonProperty("cutting_mode")] - public AutoCuttingMode CuttingMode { get => _cuttingMode; set => SetField(ref _cuttingMode, value); } + public AutoCuttingMode CuttingMode { get => this._cuttingMode; set => this.SetField(ref this._cuttingMode, value); } /// /// 自动切割数值(分钟/MiB) /// [JsonProperty("cutting_number")] - public uint CuttingNumber { get => _cuttingNumber; set => SetField(ref _cuttingNumber, value); } + public uint CuttingNumber { get => this._cuttingNumber; set => this.SetField(ref this._cuttingNumber, value); } /// /// 录制断开重连时间间隔 毫秒 /// [JsonProperty("timing_stream_retry")] - public uint TimingStreamRetry { get => _timingStreamRetry; set => SetField(ref _timingStreamRetry, value); } + public uint TimingStreamRetry { get => this._timingStreamRetry; set => this.SetField(ref this._timingStreamRetry, value); } /// /// 连接直播服务器超时时间 毫秒 /// [JsonProperty("timing_stream_connect")] - public uint TimingStreamConnect { get => _timingStreamConnect; set => SetField(ref _timingStreamConnect, value); } + public uint TimingStreamConnect { get => this._timingStreamConnect; set => this.SetField(ref this._timingStreamConnect, value); } /// /// 弹幕服务器重连时间间隔 毫秒 /// [JsonProperty("timing_danmaku_retry")] - public uint TimingDanmakuRetry { get => _timingDanmakuRetry; set => SetField(ref _timingDanmakuRetry, value); } + public uint TimingDanmakuRetry { get => this._timingDanmakuRetry; set => this.SetField(ref this._timingDanmakuRetry, value); } /// /// HTTP API 检查时间间隔 秒 /// [JsonProperty("timing_check_interval")] - public uint TimingCheckInterval { get => _timingCheckInterval; set => SetField(ref _timingCheckInterval, value); } + public uint TimingCheckInterval { get => this._timingCheckInterval; set => this.SetField(ref this._timingCheckInterval, value); } /// /// 最大未收到新直播数据时间 毫秒 /// [JsonProperty("timing_watchdog_timeout")] - public uint TimingWatchdogTimeout { get => _timingWatchdogTimeout; set => SetField(ref _timingWatchdogTimeout, value); } + public uint TimingWatchdogTimeout { get => this._timingWatchdogTimeout; set => this.SetField(ref this._timingWatchdogTimeout, value); } /// /// 请求 API 时使用的 Cookie /// [JsonProperty("cookie")] - public string Cookie { get => _cookie; set => SetField(ref _cookie, value); } + public string Cookie { get => this._cookie; set => this.SetField(ref this._cookie, value); } /// /// 是否同时录制弹幕 /// [JsonProperty("record_danmaku")] - public bool RecordDanmaku { get => _recordDanmaku; set => SetField(ref _recordDanmaku, value); } + public bool RecordDanmaku { get => this._recordDanmaku; set => this.SetField(ref this._recordDanmaku, value); } /// /// 是否记录弹幕原始数据 /// [JsonProperty("record_danmaku_raw")] - public bool RecordDanmakuRaw { get => _recordDanmakuRaw; set => SetField(ref _recordDanmakuRaw, value); } + public bool RecordDanmakuRaw { get => this._recordDanmakuRaw; set => this.SetField(ref this._recordDanmakuRaw, value); } /// /// 是否同时录制 SuperChat /// [JsonProperty("record_danmaku_sc")] - public bool RecordDanmakuSuperChat { get => _recordDanmakuSuperChat; set => SetField(ref _recordDanmakuSuperChat, value); } + public bool RecordDanmakuSuperChat { get => this._recordDanmakuSuperChat; set => this.SetField(ref this._recordDanmakuSuperChat, value); } /// /// 是否同时录制 礼物 /// [JsonProperty("record_danmaku_gift")] - public bool RecordDanmakuGift { get => _recordDanmakuGift; set => SetField(ref _recordDanmakuGift, value); } + public bool RecordDanmakuGift { get => this._recordDanmakuGift; set => this.SetField(ref this._recordDanmakuGift, value); } /// /// 是否同时录制 上船 /// [JsonProperty("record_danmaku_guard")] - public bool RecordDanmakuGuard { get => _recordDanmakuGuard; set => SetField(ref _recordDanmakuGuard, value); } + public bool RecordDanmakuGuard { get => this._recordDanmakuGuard; set => this.SetField(ref this._recordDanmakuGuard, value); } /// /// 触发 的弹幕个数 /// [JsonProperty("record_danmaku_flush_interval")] - public uint RecordDanmakuFlushInterval { get => _recordDanmakuFlushInterval; set => SetField(ref _recordDanmakuFlushInterval, value); } + public uint RecordDanmakuFlushInterval { get => this._recordDanmakuFlushInterval; set => this.SetField(ref this._recordDanmakuFlushInterval, value); } /// /// 替换api.live.bilibili.com服务器为其他反代,可以支持在云服务器上录制 /// [JsonProperty("live_api_host")] - public string LiveApiHost { get => _liveApiHost; set => SetField(ref _liveApiHost, value); } + public string LiveApiHost { get => this._liveApiHost; set => this.SetField(ref this._liveApiHost, value); } [JsonProperty("record_filename_format")] public string RecordFilenameFormat { - get => _record_filename_format; - set => SetField(ref _record_filename_format, value); + get => this._record_filename_format; + set => this.SetField(ref this._record_filename_format, value); } [JsonProperty("clip_filename_format")] public string ClipFilenameFormat { - get => _clip_filename_format; - set => SetField(ref _clip_filename_format, value); + get => this._clip_filename_format; + set => this.SetField(ref this._clip_filename_format, value); } /// @@ -154,8 +156,8 @@ namespace BililiveRecorder.Core.Config [JsonProperty("webhook_urls")] public string WebHookUrls { - get => _webhook_urls; - set => SetField(ref _webhook_urls, value); + get => this._webhook_urls; + set => this.SetField(ref this._webhook_urls, value); } #region INotifyPropertyChanged @@ -163,9 +165,8 @@ namespace BililiveRecorder.Core.Config protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = "") { - if (EqualityComparer.Default.Equals(field, value)) { return false; } - logger.Trace("设置 [{0}] 的值已从 [{1}] 修改到 [{2}]", propertyName, field, value); - field = value; OnPropertyChanged(propertyName); return true; + if (EqualityComparer.Default.Equals(field, value)) return false; logger.Trace("设置 [{0}] 的值已从 [{1}] 修改到 [{2}]", propertyName, field, value); + field = value; this.OnPropertyChanged(propertyName); return true; } #endregion diff --git a/BililiveRecorder.Core/Config/V1/ConfigV1Wrapper.cs b/BililiveRecorder.Core/Config/V1/ConfigV1Wrapper.cs new file mode 100644 index 0000000..832e067 --- /dev/null +++ b/BililiveRecorder.Core/Config/V1/ConfigV1Wrapper.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace BililiveRecorder.Core.Config.V1 +{ + internal sealed class ConfigV1Wrapper : ConfigBase + { + /// + /// Config Data String + /// + [JsonProperty("data")] + public string Data { get; set; } + } +} diff --git a/BililiveRecorder.Core/Config/RoomV1.cs b/BililiveRecorder.Core/Config/V1/RoomV1.cs similarity index 79% rename from BililiveRecorder.Core/Config/RoomV1.cs rename to BililiveRecorder.Core/Config/V1/RoomV1.cs index 6b12c91..d322878 100644 --- a/BililiveRecorder.Core/Config/RoomV1.cs +++ b/BililiveRecorder.Core/Config/V1/RoomV1.cs @@ -1,6 +1,6 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace BililiveRecorder.Core.Config +namespace BililiveRecorder.Core.Config.V1 { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] public class RoomV1 diff --git a/BililiveRecorder.Core/Config/V2/Config.gen.cs b/BililiveRecorder.Core/Config/V2/Config.gen.cs new file mode 100644 index 0000000..c6994fa --- /dev/null +++ b/BililiveRecorder.Core/Config/V2/Config.gen.cs @@ -0,0 +1,382 @@ +// ****************************** +// GENERATED CODE, DO NOT EDIT. +// RUN FORMATTER AFTER GENERATE +// ****************************** +using System.ComponentModel; +using BililiveRecorder.FlvProcessor; +using HierarchicalPropertyDefault; +using Newtonsoft.Json; + +#nullable enable +namespace BililiveRecorder.Core.Config.V2 +{ + [JsonObject(MemberSerialization.OptIn)] + public sealed partial class RoomConfig : HierarchicalObject + { + /// + /// 房间号 + /// + public int RoomId { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasRoomId { get => this.GetPropertyHasValue(nameof(this.RoomId)); set => this.SetPropertyHasValue(value, nameof(this.RoomId)); } + [JsonProperty(nameof(RoomId)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalRoomId { get => this.GetPropertyValueOptional(nameof(this.RoomId)); set => this.SetPropertyValueOptional(value, nameof(this.RoomId)); } + + /// + /// 是否启用自动录制 + /// + public bool AutoRecord { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasAutoRecord { get => this.GetPropertyHasValue(nameof(this.AutoRecord)); set => this.SetPropertyHasValue(value, nameof(this.AutoRecord)); } + [JsonProperty(nameof(AutoRecord)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalAutoRecord { get => this.GetPropertyValueOptional(nameof(this.AutoRecord)); set => this.SetPropertyValueOptional(value, nameof(this.AutoRecord)); } + + /// + /// 录制文件自动切割模式 + /// + public AutoCuttingMode CuttingMode { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasCuttingMode { get => this.GetPropertyHasValue(nameof(this.CuttingMode)); set => this.SetPropertyHasValue(value, nameof(this.CuttingMode)); } + [JsonProperty(nameof(CuttingMode)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalCuttingMode { get => this.GetPropertyValueOptional(nameof(this.CuttingMode)); set => this.SetPropertyValueOptional(value, nameof(this.CuttingMode)); } + + /// + /// 录制文件自动切割数值(分钟/MiB) + /// + public uint CuttingNumber { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasCuttingNumber { get => this.GetPropertyHasValue(nameof(this.CuttingNumber)); set => this.SetPropertyHasValue(value, nameof(this.CuttingNumber)); } + [JsonProperty(nameof(CuttingNumber)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalCuttingNumber { get => this.GetPropertyValueOptional(nameof(this.CuttingNumber)); set => this.SetPropertyValueOptional(value, nameof(this.CuttingNumber)); } + + /// + /// 是否同时录制弹幕 + /// + public bool RecordDanmaku { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasRecordDanmaku { get => this.GetPropertyHasValue(nameof(this.RecordDanmaku)); set => this.SetPropertyHasValue(value, nameof(this.RecordDanmaku)); } + [JsonProperty(nameof(RecordDanmaku)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalRecordDanmaku { get => this.GetPropertyValueOptional(nameof(this.RecordDanmaku)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmaku)); } + + /// + /// 是否记录弹幕原始数据 + /// + public bool RecordDanmakuRaw { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasRecordDanmakuRaw { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuRaw)); set => this.SetPropertyHasValue(value, nameof(this.RecordDanmakuRaw)); } + [JsonProperty(nameof(RecordDanmakuRaw)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalRecordDanmakuRaw { get => this.GetPropertyValueOptional(nameof(this.RecordDanmakuRaw)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuRaw)); } + + /// + /// 是否同时录制 SuperChat + /// + public bool RecordDanmakuSuperChat { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasRecordDanmakuSuperChat { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuSuperChat)); set => this.SetPropertyHasValue(value, nameof(this.RecordDanmakuSuperChat)); } + [JsonProperty(nameof(RecordDanmakuSuperChat)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalRecordDanmakuSuperChat { get => this.GetPropertyValueOptional(nameof(this.RecordDanmakuSuperChat)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuSuperChat)); } + + /// + /// 是否同时录制 礼物 + /// + public bool RecordDanmakuGift { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasRecordDanmakuGift { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuGift)); set => this.SetPropertyHasValue(value, nameof(this.RecordDanmakuGift)); } + [JsonProperty(nameof(RecordDanmakuGift)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalRecordDanmakuGift { get => this.GetPropertyValueOptional(nameof(this.RecordDanmakuGift)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGift)); } + + /// + /// 是否同时录制 上船 + /// + public bool RecordDanmakuGuard { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasRecordDanmakuGuard { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyHasValue(value, nameof(this.RecordDanmakuGuard)); } + [JsonProperty(nameof(RecordDanmakuGuard)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalRecordDanmakuGuard { get => this.GetPropertyValueOptional(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGuard)); } + + /// + /// 启用的功能 + /// + public EnabledFeature EnabledFeature => this.GetPropertyValue(); + + /// + /// 剪辑-过去的时长(秒) + /// + public uint ClipLengthPast => this.GetPropertyValue(); + + /// + /// 剪辑-将来的时长(秒) + /// + public uint ClipLengthFuture => this.GetPropertyValue(); + + /// + /// 录制断开重连时间间隔 毫秒 + /// + public uint TimingStreamRetry => this.GetPropertyValue(); + + /// + /// 连接直播服务器超时时间 毫秒 + /// + public uint TimingStreamConnect => this.GetPropertyValue(); + + /// + /// 弹幕服务器重连时间间隔 毫秒 + /// + public uint TimingDanmakuRetry => this.GetPropertyValue(); + + /// + /// HTTP API 检查时间间隔 秒 + /// + public uint TimingCheckInterval => this.GetPropertyValue(); + + /// + /// 最大未收到新直播数据时间 毫秒 + /// + public uint TimingWatchdogTimeout => this.GetPropertyValue(); + + /// + /// 触发 的弹幕个数 + /// + public uint RecordDanmakuFlushInterval => this.GetPropertyValue(); + + /// + /// 请求 API 时使用的 Cookie + /// + public string? Cookie => this.GetPropertyValue(); + + /// + /// 录制文件写入结束 Webhook 地址 每行一个 + /// + public string? WebHookUrls => this.GetPropertyValue(); + + /// + /// 替换 api.live.bilibili.com 服务器为其他反代,可以支持在云服务器上录制 + /// + public string? LiveApiHost => this.GetPropertyValue(); + + /// + /// 录制文件名模板 + /// + public string? RecordFilenameFormat => this.GetPropertyValue(); + + /// + /// 剪辑文件名模板 + /// + public string? ClipFilenameFormat => this.GetPropertyValue(); + + } + + [JsonObject(MemberSerialization.OptIn)] + public sealed partial class GlobalConfig : HierarchicalObject + { + /// + /// 启用的功能 + /// + public EnabledFeature EnabledFeature { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasEnabledFeature { get => this.GetPropertyHasValue(nameof(this.EnabledFeature)); set => this.SetPropertyHasValue(value, nameof(this.EnabledFeature)); } + [JsonProperty(nameof(EnabledFeature)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalEnabledFeature { get => this.GetPropertyValueOptional(nameof(this.EnabledFeature)); set => this.SetPropertyValueOptional(value, nameof(this.EnabledFeature)); } + + /// + /// 剪辑-过去的时长(秒) + /// + public uint ClipLengthPast { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasClipLengthPast { get => this.GetPropertyHasValue(nameof(this.ClipLengthPast)); set => this.SetPropertyHasValue(value, nameof(this.ClipLengthPast)); } + [JsonProperty(nameof(ClipLengthPast)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalClipLengthPast { get => this.GetPropertyValueOptional(nameof(this.ClipLengthPast)); set => this.SetPropertyValueOptional(value, nameof(this.ClipLengthPast)); } + + /// + /// 剪辑-将来的时长(秒) + /// + public uint ClipLengthFuture { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasClipLengthFuture { get => this.GetPropertyHasValue(nameof(this.ClipLengthFuture)); set => this.SetPropertyHasValue(value, nameof(this.ClipLengthFuture)); } + [JsonProperty(nameof(ClipLengthFuture)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalClipLengthFuture { get => this.GetPropertyValueOptional(nameof(this.ClipLengthFuture)); set => this.SetPropertyValueOptional(value, nameof(this.ClipLengthFuture)); } + + /// + /// 录制断开重连时间间隔 毫秒 + /// + public uint TimingStreamRetry { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasTimingStreamRetry { get => this.GetPropertyHasValue(nameof(this.TimingStreamRetry)); set => this.SetPropertyHasValue(value, nameof(this.TimingStreamRetry)); } + [JsonProperty(nameof(TimingStreamRetry)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalTimingStreamRetry { get => this.GetPropertyValueOptional(nameof(this.TimingStreamRetry)); set => this.SetPropertyValueOptional(value, nameof(this.TimingStreamRetry)); } + + /// + /// 连接直播服务器超时时间 毫秒 + /// + public uint TimingStreamConnect { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasTimingStreamConnect { get => this.GetPropertyHasValue(nameof(this.TimingStreamConnect)); set => this.SetPropertyHasValue(value, nameof(this.TimingStreamConnect)); } + [JsonProperty(nameof(TimingStreamConnect)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalTimingStreamConnect { get => this.GetPropertyValueOptional(nameof(this.TimingStreamConnect)); set => this.SetPropertyValueOptional(value, nameof(this.TimingStreamConnect)); } + + /// + /// 弹幕服务器重连时间间隔 毫秒 + /// + public uint TimingDanmakuRetry { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasTimingDanmakuRetry { get => this.GetPropertyHasValue(nameof(this.TimingDanmakuRetry)); set => this.SetPropertyHasValue(value, nameof(this.TimingDanmakuRetry)); } + [JsonProperty(nameof(TimingDanmakuRetry)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalTimingDanmakuRetry { get => this.GetPropertyValueOptional(nameof(this.TimingDanmakuRetry)); set => this.SetPropertyValueOptional(value, nameof(this.TimingDanmakuRetry)); } + + /// + /// HTTP API 检查时间间隔 秒 + /// + public uint TimingCheckInterval { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasTimingCheckInterval { get => this.GetPropertyHasValue(nameof(this.TimingCheckInterval)); set => this.SetPropertyHasValue(value, nameof(this.TimingCheckInterval)); } + [JsonProperty(nameof(TimingCheckInterval)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalTimingCheckInterval { get => this.GetPropertyValueOptional(nameof(this.TimingCheckInterval)); set => this.SetPropertyValueOptional(value, nameof(this.TimingCheckInterval)); } + + /// + /// 最大未收到新直播数据时间 毫秒 + /// + public uint TimingWatchdogTimeout { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasTimingWatchdogTimeout { get => this.GetPropertyHasValue(nameof(this.TimingWatchdogTimeout)); set => this.SetPropertyHasValue(value, nameof(this.TimingWatchdogTimeout)); } + [JsonProperty(nameof(TimingWatchdogTimeout)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalTimingWatchdogTimeout { get => this.GetPropertyValueOptional(nameof(this.TimingWatchdogTimeout)); set => this.SetPropertyValueOptional(value, nameof(this.TimingWatchdogTimeout)); } + + /// + /// 触发 的弹幕个数 + /// + public uint RecordDanmakuFlushInterval { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasRecordDanmakuFlushInterval { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuFlushInterval)); set => this.SetPropertyHasValue(value, nameof(this.RecordDanmakuFlushInterval)); } + [JsonProperty(nameof(RecordDanmakuFlushInterval)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalRecordDanmakuFlushInterval { get => this.GetPropertyValueOptional(nameof(this.RecordDanmakuFlushInterval)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuFlushInterval)); } + + /// + /// 请求 API 时使用的 Cookie + /// + public string? Cookie { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasCookie { get => this.GetPropertyHasValue(nameof(this.Cookie)); set => this.SetPropertyHasValue(value, nameof(this.Cookie)); } + [JsonProperty(nameof(Cookie)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalCookie { get => this.GetPropertyValueOptional(nameof(this.Cookie)); set => this.SetPropertyValueOptional(value, nameof(this.Cookie)); } + + /// + /// 录制文件写入结束 Webhook 地址 每行一个 + /// + public string? WebHookUrls { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasWebHookUrls { get => this.GetPropertyHasValue(nameof(this.WebHookUrls)); set => this.SetPropertyHasValue(value, nameof(this.WebHookUrls)); } + [JsonProperty(nameof(WebHookUrls)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalWebHookUrls { get => this.GetPropertyValueOptional(nameof(this.WebHookUrls)); set => this.SetPropertyValueOptional(value, nameof(this.WebHookUrls)); } + + /// + /// 替换 api.live.bilibili.com 服务器为其他反代,可以支持在云服务器上录制 + /// + public string? LiveApiHost { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasLiveApiHost { get => this.GetPropertyHasValue(nameof(this.LiveApiHost)); set => this.SetPropertyHasValue(value, nameof(this.LiveApiHost)); } + [JsonProperty(nameof(LiveApiHost)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalLiveApiHost { get => this.GetPropertyValueOptional(nameof(this.LiveApiHost)); set => this.SetPropertyValueOptional(value, nameof(this.LiveApiHost)); } + + /// + /// 录制文件名模板 + /// + public string? RecordFilenameFormat { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasRecordFilenameFormat { get => this.GetPropertyHasValue(nameof(this.RecordFilenameFormat)); set => this.SetPropertyHasValue(value, nameof(this.RecordFilenameFormat)); } + [JsonProperty(nameof(RecordFilenameFormat)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalRecordFilenameFormat { get => this.GetPropertyValueOptional(nameof(this.RecordFilenameFormat)); set => this.SetPropertyValueOptional(value, nameof(this.RecordFilenameFormat)); } + + /// + /// 剪辑文件名模板 + /// + public string? ClipFilenameFormat { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasClipFilenameFormat { get => this.GetPropertyHasValue(nameof(this.ClipFilenameFormat)); set => this.SetPropertyHasValue(value, nameof(this.ClipFilenameFormat)); } + [JsonProperty(nameof(ClipFilenameFormat)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalClipFilenameFormat { get => this.GetPropertyValueOptional(nameof(this.ClipFilenameFormat)); set => this.SetPropertyValueOptional(value, nameof(this.ClipFilenameFormat)); } + + /// + /// 录制文件自动切割模式 + /// + public AutoCuttingMode CuttingMode { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasCuttingMode { get => this.GetPropertyHasValue(nameof(this.CuttingMode)); set => this.SetPropertyHasValue(value, nameof(this.CuttingMode)); } + [JsonProperty(nameof(CuttingMode)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalCuttingMode { get => this.GetPropertyValueOptional(nameof(this.CuttingMode)); set => this.SetPropertyValueOptional(value, nameof(this.CuttingMode)); } + + /// + /// 录制文件自动切割数值(分钟/MiB) + /// + public uint CuttingNumber { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasCuttingNumber { get => this.GetPropertyHasValue(nameof(this.CuttingNumber)); set => this.SetPropertyHasValue(value, nameof(this.CuttingNumber)); } + [JsonProperty(nameof(CuttingNumber)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalCuttingNumber { get => this.GetPropertyValueOptional(nameof(this.CuttingNumber)); set => this.SetPropertyValueOptional(value, nameof(this.CuttingNumber)); } + + /// + /// 是否同时录制弹幕 + /// + public bool RecordDanmaku { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasRecordDanmaku { get => this.GetPropertyHasValue(nameof(this.RecordDanmaku)); set => this.SetPropertyHasValue(value, nameof(this.RecordDanmaku)); } + [JsonProperty(nameof(RecordDanmaku)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalRecordDanmaku { get => this.GetPropertyValueOptional(nameof(this.RecordDanmaku)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmaku)); } + + /// + /// 是否记录弹幕原始数据 + /// + public bool RecordDanmakuRaw { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasRecordDanmakuRaw { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuRaw)); set => this.SetPropertyHasValue(value, nameof(this.RecordDanmakuRaw)); } + [JsonProperty(nameof(RecordDanmakuRaw)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalRecordDanmakuRaw { get => this.GetPropertyValueOptional(nameof(this.RecordDanmakuRaw)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuRaw)); } + + /// + /// 是否同时录制 SuperChat + /// + public bool RecordDanmakuSuperChat { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasRecordDanmakuSuperChat { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuSuperChat)); set => this.SetPropertyHasValue(value, nameof(this.RecordDanmakuSuperChat)); } + [JsonProperty(nameof(RecordDanmakuSuperChat)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalRecordDanmakuSuperChat { get => this.GetPropertyValueOptional(nameof(this.RecordDanmakuSuperChat)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuSuperChat)); } + + /// + /// 是否同时录制 礼物 + /// + public bool RecordDanmakuGift { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasRecordDanmakuGift { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuGift)); set => this.SetPropertyHasValue(value, nameof(this.RecordDanmakuGift)); } + [JsonProperty(nameof(RecordDanmakuGift)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalRecordDanmakuGift { get => this.GetPropertyValueOptional(nameof(this.RecordDanmakuGift)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGift)); } + + /// + /// 是否同时录制 上船 + /// + public bool RecordDanmakuGuard { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasRecordDanmakuGuard { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyHasValue(value, nameof(this.RecordDanmakuGuard)); } + [JsonProperty(nameof(RecordDanmakuGuard)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalRecordDanmakuGuard { get => this.GetPropertyValueOptional(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGuard)); } + + } + + public sealed partial class DefaultConfig + { + internal static readonly DefaultConfig Instance = new DefaultConfig(); + private DefaultConfig() { } + + public EnabledFeature EnabledFeature => EnabledFeature.RecordOnly; + + public uint ClipLengthPast => 20; + + public uint ClipLengthFuture => 10; + + public uint TimingStreamRetry => 6 * 1000; + + public uint TimingStreamConnect => 5 * 1000; + + public uint TimingDanmakuRetry => 15 * 1000; + + public uint TimingCheckInterval => 5 * 60; + + public uint TimingWatchdogTimeout => 10 * 1000; + + public uint RecordDanmakuFlushInterval => 20; + + public string Cookie => string.Empty; + + public string WebHookUrls => string.Empty; + + public string LiveApiHost => "https://api.live.bilibili.com"; + + public string RecordFilenameFormat => @"{roomid}-{name}/录制-{roomid}-{date}-{time}-{title}.flv"; + + public string ClipFilenameFormat => @"{roomid}-{name}/剪辑片段-{roomid}-{date}-{time}-{title}.flv"; + + public AutoCuttingMode CuttingMode => AutoCuttingMode.Disabled; + + public uint CuttingNumber => 100; + + public bool RecordDanmaku => false; + + public bool RecordDanmakuRaw => false; + + public bool RecordDanmakuSuperChat => true; + + public bool RecordDanmakuGift => false; + + public bool RecordDanmakuGuard => true; + + } + +} diff --git a/BililiveRecorder.Core/Config/V2/ConfigV2.cs b/BililiveRecorder.Core/Config/V2/ConfigV2.cs new file mode 100644 index 0000000..d29f7bb --- /dev/null +++ b/BililiveRecorder.Core/Config/V2/ConfigV2.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +#nullable enable +namespace BililiveRecorder.Core.Config.V2 +{ + public class ConfigV2 : ConfigBase + { + public override int Version => 2; + + [JsonProperty("global")] + public GlobalConfig Global { get; set; } = new GlobalConfig(); + + [JsonProperty("rooms")] + public List Rooms { get; set; } = new List(); + } + + public partial class RoomConfig + { + public RoomConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name })) + { } + + internal void SetParent(GlobalConfig? config) => this.Parent = config; + + public string? WorkDirectory => this.GetPropertyValue(); + } + + public partial class GlobalConfig + { + public GlobalConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name })) + { + this.Parent = DefaultConfig.Instance; + } + + /// + /// 当前工作目录 + /// + public string? WorkDirectory + { + get => this.GetPropertyValue(); + set => this.SetPropertyValue(value); + } + } +} diff --git a/BililiveRecorder.Core/Config/V2/build_config.data.js b/BililiveRecorder.Core/Config/V2/build_config.data.js new file mode 100644 index 0000000..b92ebc1 --- /dev/null +++ b/BililiveRecorder.Core/Config/V2/build_config.data.js @@ -0,0 +1,126 @@ +module.exports = { + "global": [{ + "name": "EnabledFeature", + "type": "EnabledFeature", + "desc": "启用的功能", + "default": "EnabledFeature.RecordOnly" + }, { + "name": "ClipLengthPast", + "type": "uint", + "desc": "剪辑-过去的时长(秒)", + "default": "20" + }, { + "name": "ClipLengthFuture", + "type": "uint", + "desc": "剪辑-将来的时长(秒)", + "default": "10" + }, { + "name": "TimingStreamRetry", + "type": "uint", + "desc": "录制断开重连时间间隔 毫秒", + "default": "6 * 1000" + }, { + "name": "TimingStreamConnect", + "type": "uint", + "desc": "连接直播服务器超时时间 毫秒", + "default": "5 * 1000" + }, { + "name": "TimingDanmakuRetry", + "type": "uint", + "desc": "弹幕服务器重连时间间隔 毫秒", + "default": "15 * 1000" + }, { + "name": "TimingCheckInterval", + "type": "uint", + "desc": "HTTP API 检查时间间隔 秒", + "default": "5 * 60" + }, { + "name": "TimingWatchdogTimeout", + "type": "uint", + "desc": "最大未收到新直播数据时间 毫秒", + "default": "10 * 1000" + }, { + "name": "RecordDanmakuFlushInterval", + "type": "uint", + "desc": "触发 的弹幕个数", + "default": "20" + }, { + "name": "Cookie", + "type": "string", + "desc": "请求 API 时使用的 Cookie", + "default": "string.Empty", + "nullable": true + }, { + "name": "WebHookUrls", + "type": "string", + "desc": "录制文件写入结束 Webhook 地址 每行一个", + "default": "string.Empty", + "nullable": true + }, { + "name": "LiveApiHost", + "type": "string", + "desc": "替换 api.live.bilibili.com 服务器为其他反代,可以支持在云服务器上录制", + "default": "\"https://api.live.bilibili.com\"", + "nullable": true + }, { + "name": "RecordFilenameFormat", + "type": "string", + "desc": "录制文件名模板", + "default": "@\"{roomid}-{name}/录制-{roomid}-{date}-{time}-{title}.flv\"", + "nullable": true + }, { + "name": "ClipFilenameFormat", + "type": "string", + "desc": "剪辑文件名模板", + "default": "@\"{roomid}-{name}/剪辑片段-{roomid}-{date}-{time}-{title}.flv\"", + "nullable": true + }, ], + "room": [{ + "name": "RoomId", + "type": "int", + "desc": "房间号", + "default": "default", + "without_global": true + }, { + "name": "AutoRecord", + "type": "bool", + "desc": "是否启用自动录制", + "default": "default", + "without_global": true + }, { + "name": "CuttingMode", + "type": "AutoCuttingMode", + "desc": "录制文件自动切割模式", + "default": "AutoCuttingMode.Disabled" + }, { + "name": "CuttingNumber", + "type": "uint", + "desc": "录制文件自动切割数值(分钟/MiB)", + "default": "100" + }, { + "name": "RecordDanmaku", + "type": "bool", + "desc": "是否同时录制弹幕", + "default": "false" + }, { + "name": "RecordDanmakuRaw", + "type": "bool", + "desc": "是否记录弹幕原始数据", + "default": "false" + }, { + "name": "RecordDanmakuSuperChat", + "type": "bool", + "desc": "是否同时录制 SuperChat", + "default": "true" + }, { + "name": "RecordDanmakuGift", + "type": "bool", + "desc": "是否同时录制 礼物", + "default": "false" + }, { + "name": "RecordDanmakuGuard", + "type": "bool", + "desc": "是否同时录制 上船", + "default": "true" + }, ] +} diff --git a/BililiveRecorder.Core/Config/V2/build_config.js b/BililiveRecorder.Core/Config/V2/build_config.js new file mode 100644 index 0000000..904f263 --- /dev/null +++ b/BililiveRecorder.Core/Config/V2/build_config.js @@ -0,0 +1,79 @@ +"use strict"; +const fs = require("fs"); +const data = require("./build_config.data.js"); + +const CODE_HEADER = + `// ****************************** +// GENERATED CODE, DO NOT EDIT. +// RUN FORMATTER AFTER GENERATE +// ****************************** +using System.ComponentModel; +using BililiveRecorder.FlvProcessor; +using HierarchicalPropertyDefault; +using Newtonsoft.Json; + +#nullable enable +namespace BililiveRecorder.Core.Config.V2 +{ +`; + +const CODE_FOOTER = `}\n`; + +let result = CODE_HEADER; + +function write_property(r) { + result += `/// \n/// ${r.desc}\n/// \n` + result += `public ${r.type}${!!r.nullable ? "?" : ""} ${r.name} { get => this.GetPropertyValue<${r.type}>(); set => this.SetPropertyValue(value); }\n` + result += `public bool Has${r.name} { get => this.GetPropertyHasValue(nameof(this.${r.name})); set => this.SetPropertyHasValue<${r.type}>(value, nameof(this.${r.name})); }\n` + result += `[JsonProperty(nameof(${r.name})), EditorBrowsable(EditorBrowsableState.Never)]\n` + result += `public Optional<${r.type}${!!r.nullable ? "?" : ""}> Optional${r.name} { get => this.GetPropertyValueOptional<${r.type}>(nameof(this.${r.name})); set => this.SetPropertyValueOptional(value, nameof(this.${r.name})); }\n\n` +} + +function write_readonly_property(r) { + result += `/// \n/// ${r.desc}\n/// \n` + result += `public ${r.type}${!!r.nullable ? "?" : ""} ${r.name} => this.GetPropertyValue<${r.type}>();\n\n` +} + +{ + result += "[JsonObject(MemberSerialization.OptIn)]\n" + result += "public sealed partial class RoomConfig : HierarchicalObject\n" + result += "{\n"; + + data.room.forEach(r => write_property(r)) + data.global.forEach(r => write_readonly_property(r)) + + result += "}\n\n" +} + +{ + result += "[JsonObject(MemberSerialization.OptIn)]\n" + result += "public sealed partial class GlobalConfig : HierarchicalObject\n" + result += "{\n"; + + data.global + .concat(data.room.filter(x => !x.without_global)) + .forEach(r => write_property(r)) + + result += "}\n\n" +} + +{ + result += `public sealed partial class DefaultConfig + { + internal static readonly DefaultConfig Instance = new DefaultConfig(); + private DefaultConfig() {}\n\n`; + + data.global + .concat(data.room.filter(x => !x.without_global)) + .forEach(r => { + result += `public ${r.type} ${r.name} => ${r.default};\n\n` + }) + + result += "}\n\n" +} + +result += CODE_FOOTER; + +fs.writeFileSync("./Config.gen.cs", result, { + encoding: "utf8" +}); diff --git a/BililiveRecorder.Core/CoreModule.cs b/BililiveRecorder.Core/CoreModule.cs index 6f3b3bf..7f10082 100644 --- a/BililiveRecorder.Core/CoreModule.cs +++ b/BililiveRecorder.Core/CoreModule.cs @@ -1,22 +1,21 @@ using System.Net.Sockets; using Autofac; -using BililiveRecorder.Core.Callback; -using BililiveRecorder.Core.Config; +using BililiveRecorder.Core.Config.V2; +#nullable enable namespace BililiveRecorder.Core { public class CoreModule : Module { public CoreModule() { - } protected override void Load(ContainerBuilder builder) { - builder.RegisterType().AsSelf().InstancePerMatchingLifetimeScope("recorder_root"); + builder.Register(x => x.Resolve().Config).As(); + builder.Register(x => x.Resolve().Global).As(); builder.RegisterType().AsSelf().InstancePerMatchingLifetimeScope("recorder_root"); - builder.RegisterType().AsSelf().InstancePerMatchingLifetimeScope("recorder_root"); builder.RegisterType().AsSelf().ExternallyOwned(); builder.RegisterType().As().ExternallyOwned(); builder.RegisterType().As().ExternallyOwned(); diff --git a/BililiveRecorder.Core/DanmakuModel.cs b/BililiveRecorder.Core/DanmakuModel.cs index 6596ad5..ede0a37 100644 --- a/BililiveRecorder.Core/DanmakuModel.cs +++ b/BililiveRecorder.Core/DanmakuModel.cs @@ -1,5 +1,5 @@ -using Newtonsoft.Json.Linq; -using System; +using System; +using Newtonsoft.Json.Linq; namespace BililiveRecorder.Core { @@ -68,8 +68,8 @@ namespace BililiveRecorder.Core [Obsolete("请使用 UserName")] public string CommentUser { - get { return UserName; } - set { UserName = value; } + get { return this.UserName; } + set { this.UserName = value; } } /// @@ -123,8 +123,8 @@ namespace BililiveRecorder.Core [Obsolete("请使用 UserName")] public string GiftUser { - get { return UserName; } - set { UserName = value; } + get { return this.UserName; } + set { this.UserName = value; } } /// @@ -136,7 +136,7 @@ namespace BililiveRecorder.Core /// 禮物數量 /// [Obsolete("请使用 GiftCount")] - public string GiftNum { get { return GiftCount.ToString(); } } + public string GiftNum { get { return this.GiftCount.ToString(); } } /// /// 礼物数量 @@ -198,56 +198,56 @@ namespace BililiveRecorder.Core public DanmakuModel(string JSON) { - RawData = JSON; - JSON_Version = 2; + this.RawData = JSON; + this.JSON_Version = 2; var obj = JObject.Parse(JSON); - RawObj = obj; + this.RawObj = obj; string cmd = obj["cmd"]?.ToObject(); switch (cmd) { case "LIVE": - MsgType = MsgTypeEnum.LiveStart; - RoomID = obj["roomid"].ToObject(); + this.MsgType = MsgTypeEnum.LiveStart; + this.RoomID = obj["roomid"].ToObject(); break; case "PREPARING": - MsgType = MsgTypeEnum.LiveEnd; - RoomID = obj["roomid"].ToObject(); + this.MsgType = MsgTypeEnum.LiveEnd; + this.RoomID = obj["roomid"].ToObject(); break; case "DANMU_MSG": - MsgType = MsgTypeEnum.Comment; - CommentText = obj["info"][1].ToObject(); - UserID = obj["info"][2][0].ToObject(); - UserName = obj["info"][2][1].ToObject(); - IsAdmin = obj["info"][2][2].ToObject() == "1"; - IsVIP = obj["info"][2][3].ToObject() == "1"; - UserGuardLevel = obj["info"][7].ToObject(); + this.MsgType = MsgTypeEnum.Comment; + this.CommentText = obj["info"][1].ToObject(); + this.UserID = obj["info"][2][0].ToObject(); + this.UserName = obj["info"][2][1].ToObject(); + this.IsAdmin = obj["info"][2][2].ToObject() == "1"; + this.IsVIP = obj["info"][2][3].ToObject() == "1"; + this.UserGuardLevel = obj["info"][7].ToObject(); break; case "SEND_GIFT": - MsgType = MsgTypeEnum.GiftSend; - GiftName = obj["data"]["giftName"].ToObject(); - UserName = obj["data"]["uname"].ToObject(); - UserID = obj["data"]["uid"].ToObject(); - GiftCount = obj["data"]["num"].ToObject(); + this.MsgType = MsgTypeEnum.GiftSend; + this.GiftName = obj["data"]["giftName"].ToObject(); + this.UserName = obj["data"]["uname"].ToObject(); + this.UserID = obj["data"]["uid"].ToObject(); + this.GiftCount = obj["data"]["num"].ToObject(); break; case "GUARD_BUY": { - MsgType = MsgTypeEnum.GuardBuy; - UserID = obj["data"]["uid"].ToObject(); - UserName = obj["data"]["username"].ToObject(); - UserGuardLevel = obj["data"]["guard_level"].ToObject(); - GiftName = UserGuardLevel == 3 ? "舰长" : UserGuardLevel == 2 ? "提督" : UserGuardLevel == 1 ? "总督" : ""; - GiftCount = obj["data"]["num"].ToObject(); + this.MsgType = MsgTypeEnum.GuardBuy; + this.UserID = obj["data"]["uid"].ToObject(); + this.UserName = obj["data"]["username"].ToObject(); + this.UserGuardLevel = obj["data"]["guard_level"].ToObject(); + this.GiftName = this.UserGuardLevel == 3 ? "舰长" : this.UserGuardLevel == 2 ? "提督" : this.UserGuardLevel == 1 ? "总督" : ""; + this.GiftCount = obj["data"]["num"].ToObject(); break; } case "SUPER_CHAT_MESSAGE": { - MsgType = MsgTypeEnum.SuperChat; - CommentText = obj["data"]["message"]?.ToString(); - UserID = obj["data"]["uid"].ToObject(); - UserName = obj["data"]["user_info"]["uname"].ToString(); - Price = obj["data"]["price"].ToObject(); - SCKeepTime = obj["data"]["time"].ToObject(); + this.MsgType = MsgTypeEnum.SuperChat; + this.CommentText = obj["data"]["message"]?.ToString(); + this.UserID = obj["data"]["uid"].ToObject(); + this.UserName = obj["data"]["user_info"]["uname"].ToString(); + this.Price = obj["data"]["price"].ToObject(); + this.SCKeepTime = obj["data"]["time"].ToObject(); break; } /* @@ -271,7 +271,7 @@ namespace BililiveRecorder.Core */ default: { - MsgType = MsgTypeEnum.Unknown; + this.MsgType = MsgTypeEnum.Unknown; break; } } diff --git a/BililiveRecorder.Core/IRecordedRoom.cs b/BililiveRecorder.Core/IRecordedRoom.cs index 8f7eb52..4343c77 100644 --- a/BililiveRecorder.Core/IRecordedRoom.cs +++ b/BililiveRecorder.Core/IRecordedRoom.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using BililiveRecorder.Core.Callback; +using BililiveRecorder.Core.Config.V2; using BililiveRecorder.FlvProcessor; #nullable enable @@ -10,6 +11,8 @@ namespace BililiveRecorder.Core { Guid Guid { get; } + RoomConfig RoomConfig { get; } + int ShortRoomId { get; } int RoomId { get; } string StreamerName { get; } diff --git a/BililiveRecorder.Core/IRecorder.cs b/BililiveRecorder.Core/IRecorder.cs index 1282c2b..74f8b7e 100644 --- a/BililiveRecorder.Core/IRecorder.cs +++ b/BililiveRecorder.Core/IRecorder.cs @@ -2,13 +2,14 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; -using BililiveRecorder.Core.Config; +using BililiveRecorder.Core.Config.V2; +#nullable enable namespace BililiveRecorder.Core { public interface IRecorder : INotifyPropertyChanged, INotifyCollectionChanged, IEnumerable, ICollection, IDisposable { - ConfigV1 Config { get; } + ConfigV2? Config { get; } bool Initialize(string workdir); diff --git a/BililiveRecorder.Core/IStreamMonitor.cs b/BililiveRecorder.Core/IStreamMonitor.cs index 3577eb1..5e26dfb 100644 --- a/BililiveRecorder.Core/IStreamMonitor.cs +++ b/BililiveRecorder.Core/IStreamMonitor.cs @@ -6,7 +6,6 @@ namespace BililiveRecorder.Core { public interface IStreamMonitor : IDisposable, INotifyPropertyChanged { - int Roomid { get; } bool IsMonitoring { get; } bool IsDanmakuConnected { get; } event RoomInfoUpdatedEvent RoomInfoUpdated; diff --git a/BililiveRecorder.Core/RecordedRoom.cs b/BililiveRecorder.Core/RecordedRoom.cs index 14463c4..d8dd21b 100644 --- a/BililiveRecorder.Core/RecordedRoom.cs +++ b/BililiveRecorder.Core/RecordedRoom.cs @@ -1,7 +1,3 @@ -using BililiveRecorder.Core.Callback; -using BililiveRecorder.Core.Config; -using BililiveRecorder.FlvProcessor; -using NLog; using System; using System.Collections.Generic; using System.ComponentModel; @@ -12,6 +8,10 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; +using BililiveRecorder.Core.Callback; +using BililiveRecorder.Core.Config.V2; +using BililiveRecorder.FlvProcessor; +using NLog; namespace BililiveRecorder.Core { @@ -21,67 +21,68 @@ namespace BililiveRecorder.Core private static readonly Random random = new Random(); private static readonly Version VERSION_1_0 = new Version(1, 0); - private int _roomid; - private int _realRoomid; + private int _shortRoomid; private string _streamerName; private string _title; private bool _isStreaming; public int ShortRoomId { - get => _roomid; + get => this._shortRoomid; private set { - if (value == _roomid) { return; } - _roomid = value; - TriggerPropertyChanged(nameof(ShortRoomId)); + if (value == this._shortRoomid) { return; } + this._shortRoomid = value; + this.TriggerPropertyChanged(nameof(this.ShortRoomId)); } } public int RoomId { - get => _realRoomid; + get => this.RoomConfig.RoomId; private set { - if (value == _realRoomid) { return; } - _realRoomid = value; - TriggerPropertyChanged(nameof(RoomId)); + if (value == this.RoomConfig.RoomId) { return; } + this.RoomConfig.RoomId = value; + this.TriggerPropertyChanged(nameof(this.RoomId)); } } public string StreamerName { - get => _streamerName; + get => this._streamerName; private set { - if (value == _streamerName) { return; } - _streamerName = value; - TriggerPropertyChanged(nameof(StreamerName)); + if (value == this._streamerName) { return; } + this._streamerName = value; + this.TriggerPropertyChanged(nameof(this.StreamerName)); } } public string Title { - get => _title; + get => this._title; private set { - if (value == _title) { return; } - _title = value; - TriggerPropertyChanged(nameof(Title)); + if (value == this._title) { return; } + this._title = value; + this.TriggerPropertyChanged(nameof(this.Title)); } } - public bool IsMonitoring => StreamMonitor.IsMonitoring; - public bool IsRecording => !(StreamDownloadTask?.IsCompleted ?? true); - public bool IsDanmakuConnected => StreamMonitor.IsDanmakuConnected; + public bool IsMonitoring => this.StreamMonitor.IsMonitoring; + public bool IsRecording => !(this.StreamDownloadTask?.IsCompleted ?? true); + public bool IsDanmakuConnected => this.StreamMonitor.IsDanmakuConnected; public bool IsStreaming { - get => _isStreaming; + get => this._isStreaming; private set { - if (value == _isStreaming) { return; } - _isStreaming = value; - TriggerPropertyChanged(nameof(IsStreaming)); + if (value == this._isStreaming) { return; } + this._isStreaming = value; + this.TriggerPropertyChanged(nameof(this.IsStreaming)); } } + public RoomConfig RoomConfig { get; } + private RecordEndData recordEndData; public event EventHandler RecordEnded; @@ -90,16 +91,15 @@ namespace BililiveRecorder.Core private IFlvStreamProcessor _processor; public IFlvStreamProcessor Processor { - get => _processor; + get => this._processor; private set { - if (value == _processor) { return; } - _processor = value; - TriggerPropertyChanged(nameof(Processor)); + if (value == this._processor) { return; } + this._processor = value; + this.TriggerPropertyChanged(nameof(this.Processor)); } } - private ConfigV1 _config { get; } private BililiveAPI BililiveAPI { get; } public IStreamMonitor StreamMonitor { get; } @@ -118,41 +118,57 @@ namespace BililiveRecorder.Core public DateTime LastUpdateDateTime { get; private set; } = DateTime.Now; public double DownloadSpeedPersentage { - get { return _DownloadSpeedPersentage; } - private set { if (value != _DownloadSpeedPersentage) { _DownloadSpeedPersentage = value; TriggerPropertyChanged(nameof(DownloadSpeedPersentage)); } } + get { return this._DownloadSpeedPersentage; } + private set { if (value != this._DownloadSpeedPersentage) { this._DownloadSpeedPersentage = value; this.TriggerPropertyChanged(nameof(this.DownloadSpeedPersentage)); } } } public double DownloadSpeedMegaBitps { - get { return _DownloadSpeedMegaBitps; } - private set { if (value != _DownloadSpeedMegaBitps) { _DownloadSpeedMegaBitps = value; TriggerPropertyChanged(nameof(DownloadSpeedMegaBitps)); } } + get { return this._DownloadSpeedMegaBitps; } + private set { if (value != this._DownloadSpeedMegaBitps) { this._DownloadSpeedMegaBitps = value; this.TriggerPropertyChanged(nameof(this.DownloadSpeedMegaBitps)); } } } public Guid Guid { get; } = Guid.NewGuid(); - public RecordedRoom(ConfigV1 config, - IBasicDanmakuWriter basicDanmakuWriter, - Func newIStreamMonitor, + // TODO: 重构 DI + public RecordedRoom(Func newBasicDanmakuWriter, + Func newIStreamMonitor, Func newIFlvStreamProcessor, BililiveAPI bililiveAPI, - int roomid) + RoomConfig roomConfig) { + this.RoomConfig = roomConfig; + this.StreamerName = "获取中..."; + + this.BililiveAPI = bililiveAPI; + this.newIFlvStreamProcessor = newIFlvStreamProcessor; - _config = config; - BililiveAPI = bililiveAPI; + this.basicDanmakuWriter = newBasicDanmakuWriter(this.RoomConfig); - this.basicDanmakuWriter = basicDanmakuWriter; + this.StreamMonitor = newIStreamMonitor(this.RoomConfig); + this.StreamMonitor.RoomInfoUpdated += this.StreamMonitor_RoomInfoUpdated; + this.StreamMonitor.StreamStarted += this.StreamMonitor_StreamStarted; + this.StreamMonitor.ReceivedDanmaku += this.StreamMonitor_ReceivedDanmaku; + this.StreamMonitor.PropertyChanged += this.StreamMonitor_PropertyChanged; - RoomId = roomid; - StreamerName = "获取中..."; + this.PropertyChanged += this.RecordedRoom_PropertyChanged; - StreamMonitor = newIStreamMonitor(RoomId); - StreamMonitor.RoomInfoUpdated += StreamMonitor_RoomInfoUpdated; - StreamMonitor.StreamStarted += StreamMonitor_StreamStarted; - StreamMonitor.ReceivedDanmaku += StreamMonitor_ReceivedDanmaku; - StreamMonitor.PropertyChanged += StreamMonitor_PropertyChanged; + this.StreamMonitor.FetchRoomInfoAsync(); - StreamMonitor.FetchRoomInfoAsync(); + if (this.RoomConfig.AutoRecord) + this.Start(); + } + + private void RecordedRoom_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(this.IsMonitoring): + this.RoomConfig.AutoRecord = this.IsMonitoring; + break; + default: + break; + } } private void StreamMonitor_PropertyChanged(object sender, PropertyChangedEventArgs e) @@ -160,7 +176,7 @@ namespace BililiveRecorder.Core switch (e.PropertyName) { case nameof(IStreamMonitor.IsDanmakuConnected): - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsDanmakuConnected))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsDanmakuConnected))); break; default: break; @@ -172,112 +188,116 @@ namespace BililiveRecorder.Core switch (e.Danmaku.MsgType) { case MsgTypeEnum.LiveStart: - IsStreaming = true; + this.IsStreaming = true; break; case MsgTypeEnum.LiveEnd: - IsStreaming = false; + this.IsStreaming = false; break; default: break; } - basicDanmakuWriter.Write(e.Danmaku); + this.basicDanmakuWriter.Write(e.Danmaku); } private void StreamMonitor_RoomInfoUpdated(object sender, RoomInfoUpdatedArgs e) { - RoomId = e.RoomInfo.RoomId; - ShortRoomId = e.RoomInfo.ShortRoomId; - StreamerName = e.RoomInfo.UserName; - Title = e.RoomInfo.Title; - IsStreaming = e.RoomInfo.IsStreaming; + // TODO: StreamMonitor 里的 RoomInfoUpdated Handler 也会设置一次 RoomId + // 暂时保持不变,此处的 RoomId 需要触发 PropertyChanged 事件 + this.RoomId = e.RoomInfo.RoomId; + this.ShortRoomId = e.RoomInfo.ShortRoomId; + this.StreamerName = e.RoomInfo.UserName; + this.Title = e.RoomInfo.Title; + this.IsStreaming = e.RoomInfo.IsStreaming; } public bool Start() { - if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom)); + // TODO: 重构: 删除 Start() Stop() 通过 RoomConfig.AutoRecord 控制监控状态和逻辑 + if (this.disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom)); - var r = StreamMonitor.Start(); - TriggerPropertyChanged(nameof(IsMonitoring)); + var r = this.StreamMonitor.Start(); + this.TriggerPropertyChanged(nameof(this.IsMonitoring)); return r; } public void Stop() { - if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom)); + // TODO: 见 Start() + if (this.disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom)); - StreamMonitor.Stop(); - TriggerPropertyChanged(nameof(IsMonitoring)); + this.StreamMonitor.Stop(); + this.TriggerPropertyChanged(nameof(this.IsMonitoring)); } public void RefreshRoomInfo() { - if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom)); - StreamMonitor.FetchRoomInfoAsync(); + if (this.disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom)); + this.StreamMonitor.FetchRoomInfoAsync(); } private void StreamMonitor_StreamStarted(object sender, StreamStartedArgs e) { - lock (StartupTaskLock) - if (!IsRecording && (StartupTask?.IsCompleted ?? true)) - StartupTask = _StartRecordAsync(); + lock (this.StartupTaskLock) + if (!this.IsRecording && (this.StartupTask?.IsCompleted ?? true)) + this.StartupTask = this._StartRecordAsync(); } public void StartRecord() { - if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom)); - StreamMonitor.Check(TriggerType.Manual); + if (this.disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom)); + this.StreamMonitor.Check(TriggerType.Manual); } public void StopRecord() { - if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom)); + if (this.disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom)); - _retry = false; + this._retry = false; try { - if (cancellationTokenSource != null) + if (this.cancellationTokenSource != null) { - cancellationTokenSource.Cancel(); - if (!(StreamDownloadTask?.Wait(TimeSpan.FromSeconds(2)) ?? true)) + this.cancellationTokenSource.Cancel(); + if (!(this.StreamDownloadTask?.Wait(TimeSpan.FromSeconds(2)) ?? true)) { - logger.Log(RoomId, LogLevel.Warn, "停止录制超时,尝试强制关闭连接,请检查网络连接是否稳定"); + logger.Log(this.RoomId, LogLevel.Warn, "停止录制超时,尝试强制关闭连接,请检查网络连接是否稳定"); - _stream?.Close(); - _stream?.Dispose(); - _response?.Dispose(); - StreamDownloadTask?.Wait(); + this._stream?.Close(); + this._stream?.Dispose(); + this._response?.Dispose(); + this.StreamDownloadTask?.Wait(); } } } catch (Exception ex) { - logger.Log(RoomId, LogLevel.Warn, "在尝试停止录制时发生错误,请检查网络连接是否稳定", ex); + logger.Log(this.RoomId, LogLevel.Warn, "在尝试停止录制时发生错误,请检查网络连接是否稳定", ex); } finally { - _retry = true; + this._retry = true; } } private async Task _StartRecordAsync() { - if (IsRecording) + if (this.IsRecording) { // TODO: 这里逻辑可能有问题,StartupTask 会变成当前这个已经结束的 - logger.Log(RoomId, LogLevel.Warn, "已经在录制中了"); + logger.Log(this.RoomId, LogLevel.Warn, "已经在录制中了"); return; } - cancellationTokenSource = new CancellationTokenSource(); - var token = cancellationTokenSource.Token; + this.cancellationTokenSource = new CancellationTokenSource(); + var token = this.cancellationTokenSource.Token; try { - var flv_path = await BililiveAPI.GetPlayUrlAsync(RoomId); + var flv_path = await this.BililiveAPI.GetPlayUrlAsync(this.RoomId); if (string.IsNullOrWhiteSpace(flv_path)) { - if (_retry) + if (this._retry) { - StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry); + this.StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)this.RoomConfig.TimingStreamRetry); } return; } @@ -291,7 +311,7 @@ namespace BililiveRecorder.Core })) { - client.Timeout = TimeSpan.FromMilliseconds(_config.TimingStreamConnect); + client.Timeout = TimeSpan.FromMilliseconds(this.RoomConfig.TimingStreamConnect); client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*")); client.DefaultRequestHeaders.UserAgent.Clear(); @@ -300,46 +320,46 @@ namespace BililiveRecorder.Core client.DefaultRequestHeaders.Add("Origin", "https://live.bilibili.com"); - logger.Log(RoomId, LogLevel.Info, "连接直播服务器 " + new Uri(flv_path).Host); - logger.Log(RoomId, LogLevel.Debug, "直播流地址: " + flv_path); + logger.Log(this.RoomId, LogLevel.Info, "连接直播服务器 " + new Uri(flv_path).Host); + logger.Log(this.RoomId, LogLevel.Debug, "直播流地址: " + flv_path); - _response = await client.GetAsync(flv_path, HttpCompletionOption.ResponseHeadersRead); + this._response = await client.GetAsync(flv_path, HttpCompletionOption.ResponseHeadersRead); } - if (_response.StatusCode == HttpStatusCode.Redirect || _response.StatusCode == HttpStatusCode.Moved) + if (this._response.StatusCode == HttpStatusCode.Redirect || this._response.StatusCode == HttpStatusCode.Moved) { // workaround for missing Referrer - flv_path = _response.Headers.Location.OriginalString; - _response.Dispose(); + flv_path = this._response.Headers.Location.OriginalString; + this._response.Dispose(); goto unwrap_redir; } - else if (_response.StatusCode != HttpStatusCode.OK) + else if (this._response.StatusCode != HttpStatusCode.OK) { - logger.Log(RoomId, LogLevel.Info, string.Format("尝试下载直播流时服务器返回了 ({0}){1}", _response.StatusCode, _response.ReasonPhrase)); + logger.Log(this.RoomId, LogLevel.Info, string.Format("尝试下载直播流时服务器返回了 ({0}){1}", this._response.StatusCode, this._response.ReasonPhrase)); - StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry); + this.StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)this.RoomConfig.TimingStreamRetry); _CleanupFlvRequest(); return; } else { - Processor = newIFlvStreamProcessor().Initialize(GetStreamFilePath, GetClipFilePath, _config.EnabledFeature, _config.CuttingMode); - Processor.ClipLengthFuture = _config.ClipLengthFuture; - Processor.ClipLengthPast = _config.ClipLengthPast; - Processor.CuttingNumber = _config.CuttingNumber; - Processor.StreamFinalized += (sender, e) => { basicDanmakuWriter.Disable(); }; - Processor.FileFinalized += (sender, size) => + this.Processor = this.newIFlvStreamProcessor().Initialize(this.GetStreamFilePath, this.GetClipFilePath, this.RoomConfig.EnabledFeature, this.RoomConfig.CuttingMode); + this.Processor.ClipLengthFuture = this.RoomConfig.ClipLengthFuture; + this.Processor.ClipLengthPast = this.RoomConfig.ClipLengthPast; + this.Processor.CuttingNumber = this.RoomConfig.CuttingNumber; + this.Processor.StreamFinalized += (sender, e) => { this.basicDanmakuWriter.Disable(); }; + this.Processor.FileFinalized += (sender, size) => { - if (recordEndData is null) return; - var data = recordEndData; - recordEndData = null; + if (this.recordEndData is null) return; + var data = this.recordEndData; + this.recordEndData = null; data.EndRecordTime = DateTimeOffset.Now; data.FileSize = size; RecordEnded?.Invoke(this, data); }; - Processor.OnMetaData += (sender, e) => + this.Processor.OnMetaData += (sender, e) => { e.Metadata["BililiveRecorder"] = new Dictionary() { @@ -353,26 +373,26 @@ namespace BililiveRecorder.Core }, { "roomid", - RoomId.ToString() + this.RoomId.ToString() }, { "streamername", - StreamerName + this.StreamerName }, }; }; - _stream = await _response.Content.ReadAsStreamAsync(); + this._stream = await this._response.Content.ReadAsStreamAsync(); try { - if (_response.Headers.ConnectionClose == false || (_response.Headers.ConnectionClose is null && _response.Version != VERSION_1_0)) - _stream.ReadTimeout = 3 * 1000; + if (this._response.Headers.ConnectionClose == false || (this._response.Headers.ConnectionClose is null && this._response.Version != VERSION_1_0)) + this._stream.ReadTimeout = 3 * 1000; } catch (InvalidOperationException) { } - StreamDownloadTask = Task.Run(_ReadStreamLoop); - TriggerPropertyChanged(nameof(IsRecording)); + this.StreamDownloadTask = Task.Run(_ReadStreamLoop); + this.TriggerPropertyChanged(nameof(this.IsRecording)); } } catch (TaskCanceledException) @@ -381,16 +401,16 @@ namespace BililiveRecorder.Core // useless exception message :/ _CleanupFlvRequest(); - logger.Log(RoomId, LogLevel.Warn, "连接直播服务器超时。"); - StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry); + logger.Log(this.RoomId, LogLevel.Warn, "连接直播服务器超时。"); + this.StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)this.RoomConfig.TimingStreamRetry); } catch (Exception ex) { _CleanupFlvRequest(); - logger.Log(RoomId, LogLevel.Error, "启动直播流下载出错。" + (_retry ? "将重试启动。" : ""), ex); - if (_retry) + logger.Log(this.RoomId, LogLevel.Error, "启动直播流下载出错。" + (this._retry ? "将重试启动。" : ""), ex); + if (this._retry) { - StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry); + this.StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)this.RoomConfig.TimingStreamRetry); } } return; @@ -403,17 +423,17 @@ namespace BililiveRecorder.Core byte[] buffer = new byte[BUF_SIZE]; while (!token.IsCancellationRequested) { - int bytesRead = await _stream.ReadAsync(buffer, 0, BUF_SIZE, token); + int bytesRead = await this._stream.ReadAsync(buffer, 0, BUF_SIZE, token); _UpdateDownloadSpeed(bytesRead); if (bytesRead != 0) { if (bytesRead != BUF_SIZE) { - Processor.AddBytes(buffer.Take(bytesRead).ToArray()); + this.Processor.AddBytes(buffer.Take(bytesRead).ToArray()); } else { - Processor.AddBytes(buffer); + this.Processor.AddBytes(buffer); } } else @@ -422,19 +442,19 @@ namespace BililiveRecorder.Core } } - logger.Log(RoomId, LogLevel.Info, + logger.Log(this.RoomId, LogLevel.Info, (token.IsCancellationRequested ? "本地操作结束当前录制。" : "服务器关闭直播流,可能是直播已结束。") - + (_retry ? "将重试启动。" : "")); - if (_retry) + + (this._retry ? "将重试启动。" : "")); + if (this._retry) { - StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry); + this.StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)this.RoomConfig.TimingStreamRetry); } } catch (Exception e) { if (e is ObjectDisposedException && token.IsCancellationRequested) { return; } - logger.Log(RoomId, LogLevel.Warn, "录播发生错误", e); + logger.Log(this.RoomId, LogLevel.Warn, "录播发生错误", e); } finally { @@ -443,55 +463,55 @@ namespace BililiveRecorder.Core } void _CleanupFlvRequest() { - if (Processor != null) + if (this.Processor != null) { - Processor.FinallizeFile(); - Processor.Dispose(); - Processor = null; + this.Processor.FinallizeFile(); + this.Processor.Dispose(); + this.Processor = null; } - _stream?.Dispose(); - _stream = null; - _response?.Dispose(); - _response = null; + this._stream?.Dispose(); + this._stream = null; + this._response?.Dispose(); + this._response = null; - _lastUpdateTimestamp = 0; - DownloadSpeedMegaBitps = 0d; - DownloadSpeedPersentage = 0d; - TriggerPropertyChanged(nameof(IsRecording)); + this._lastUpdateTimestamp = 0; + this.DownloadSpeedMegaBitps = 0d; + this.DownloadSpeedPersentage = 0d; + this.TriggerPropertyChanged(nameof(this.IsRecording)); } void _UpdateDownloadSpeed(int bytesRead) { DateTime now = DateTime.Now; - double passedSeconds = (now - LastUpdateDateTime).TotalSeconds; - _lastUpdateSize += bytesRead; + double passedSeconds = (now - this.LastUpdateDateTime).TotalSeconds; + this._lastUpdateSize += bytesRead; if (passedSeconds > 1.5) { - DownloadSpeedMegaBitps = _lastUpdateSize / passedSeconds * 8d / 1_000_000d; // mega bit per second - DownloadSpeedPersentage = (DownloadSpeedPersentage / 2) + ((Processor.TotalMaxTimestamp - _lastUpdateTimestamp) / passedSeconds / 1000 / 2); // ((RecordedTime/1000) / RealTime)% - _lastUpdateTimestamp = Processor.TotalMaxTimestamp; - _lastUpdateSize = 0; - LastUpdateDateTime = now; + this.DownloadSpeedMegaBitps = this._lastUpdateSize / passedSeconds * 8d / 1_000_000d; // mega bit per second + this.DownloadSpeedPersentage = (this.DownloadSpeedPersentage / 2) + ((this.Processor.TotalMaxTimestamp - this._lastUpdateTimestamp) / passedSeconds / 1000 / 2); // ((RecordedTime/1000) / RealTime)% + this._lastUpdateTimestamp = this.Processor.TotalMaxTimestamp; + this._lastUpdateSize = 0; + this.LastUpdateDateTime = now; } } } // Called by API or GUI - public void Clip() => Processor?.Clip(); + public void Clip() => this.Processor?.Clip(); - public void Shutdown() => Dispose(true); + public void Shutdown() => this.Dispose(true); private (string fullPath, string relativePath) GetStreamFilePath() { - var path = FormatFilename(_config.RecordFilenameFormat); + var path = this.FormatFilename(this.RoomConfig.RecordFilenameFormat); // 有点脏的写法,不过凑合吧 - if (_config.RecordDanmaku) + if (this.RoomConfig.RecordDanmaku) { var xmlpath = Path.ChangeExtension(path.fullPath, "xml"); - basicDanmakuWriter.EnableWithPath(xmlpath, this); + this.basicDanmakuWriter.EnableWithPath(xmlpath, this); } - recordEndData = new RecordEndData + this.recordEndData = new RecordEndData { RoomId = RoomId, Title = Title, @@ -503,7 +523,7 @@ namespace BililiveRecorder.Core return path; } - private string GetClipFilePath() => FormatFilename(_config.ClipFilenameFormat).fullPath; + private string GetClipFilePath() => this.FormatFilename(this.RoomConfig.ClipFilenameFormat).fullPath; private (string fullPath, string relativePath) FormatFilename(string formatString) { @@ -516,29 +536,30 @@ namespace BililiveRecorder.Core .Replace(@"{date}", date) .Replace(@"{time}", time) .Replace(@"{random}", randomStr) - .Replace(@"{roomid}", RoomId.ToString()) - .Replace(@"{title}", Title.RemoveInvalidFileName()) - .Replace(@"{name}", StreamerName.RemoveInvalidFileName()); + .Replace(@"{roomid}", this.RoomId.ToString()) + .Replace(@"{title}", this.Title.RemoveInvalidFileName()) + .Replace(@"{name}", this.StreamerName.RemoveInvalidFileName()); if (!relativePath.EndsWith(".flv", StringComparison.OrdinalIgnoreCase)) relativePath += ".flv"; relativePath = relativePath.RemoveInvalidFileName(ignore_slash: true); - var fullPath = Path.Combine(_config.WorkDirectory, relativePath); + var workDirectory = this.RoomConfig.WorkDirectory; + var fullPath = Path.Combine(workDirectory, relativePath); fullPath = Path.GetFullPath(fullPath); - if (!CheckPath(_config.WorkDirectory, Path.GetDirectoryName(fullPath))) + if (!CheckPath(workDirectory, Path.GetDirectoryName(fullPath))) { - logger.Log(RoomId, LogLevel.Warn, "录制文件位置超出允许范围,请检查设置。将写入到默认路径。"); - relativePath = Path.Combine(RoomId.ToString(), $"{RoomId}-{date}-{time}-{randomStr}.flv"); - fullPath = Path.Combine(_config.WorkDirectory, relativePath); + logger.Log(this.RoomId, LogLevel.Warn, "录制文件位置超出允许范围,请检查设置。将写入到默认路径。"); + relativePath = Path.Combine(this.RoomId.ToString(), $"{this.RoomId}-{date}-{time}-{randomStr}.flv"); + fullPath = Path.Combine(workDirectory, relativePath); } if (new FileInfo(relativePath).Exists) { - logger.Log(RoomId, LogLevel.Warn, "录制文件名冲突,请检查设置。将写入到默认路径。"); - relativePath = Path.Combine(RoomId.ToString(), $"{RoomId}-{date}-{time}-{randomStr}.flv"); - fullPath = Path.Combine(_config.WorkDirectory, relativePath); + logger.Log(this.RoomId, LogLevel.Warn, "录制文件名冲突,请检查设置。将写入到默认路径。"); + relativePath = Path.Combine(this.RoomId.ToString(), $"{this.RoomId}-{date}-{time}-{randomStr}.flv"); + fullPath = Path.Combine(workDirectory, relativePath); } return (fullPath, relativePath); @@ -575,34 +596,34 @@ namespace BililiveRecorder.Core protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!this.disposedValue) { if (disposing) { - Stop(); - StopRecord(); - Processor?.FinallizeFile(); - Processor?.Dispose(); - StreamMonitor?.Dispose(); - _response?.Dispose(); - _stream?.Dispose(); - cancellationTokenSource?.Dispose(); - basicDanmakuWriter?.Dispose(); + this.Stop(); + this.StopRecord(); + this.Processor?.FinallizeFile(); + this.Processor?.Dispose(); + this.StreamMonitor?.Dispose(); + this._response?.Dispose(); + this._stream?.Dispose(); + this.cancellationTokenSource?.Dispose(); + this.basicDanmakuWriter?.Dispose(); } - Processor = null; - _response = null; - _stream = null; - cancellationTokenSource = null; + this.Processor = null; + this._response = null; + this._stream = null; + this.cancellationTokenSource = null; - disposedValue = true; + this.disposedValue = true; } } public void Dispose() { // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。 - Dispose(true); + this.Dispose(true); } #endregion } diff --git a/BililiveRecorder.Core/Recorder.cs b/BililiveRecorder.Core/Recorder.cs index 75c00d4..366c868 100644 --- a/BililiveRecorder.Core/Recorder.cs +++ b/BililiveRecorder.Core/Recorder.cs @@ -6,18 +6,19 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Threading; -using System.Threading.Tasks; using BililiveRecorder.Core.Callback; using BililiveRecorder.Core.Config; +using BililiveRecorder.Core.Config.V2; using NLog; +#nullable enable namespace BililiveRecorder.Core { public class Recorder : IRecorder { private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - private readonly Func newIRecordedRoom; + private readonly Func newIRecordedRoom; private readonly CancellationTokenSource tokenSource; private bool _valid = false; @@ -25,24 +26,22 @@ namespace BililiveRecorder.Core private ObservableCollection Rooms { get; } = new ObservableCollection(); - public ConfigV1 Config { get; } + public ConfigV2? Config { get; private set; } - private BasicWebhook Webhook { get; } + private BasicWebhook? Webhook { get; set; } - public int Count => Rooms.Count; + public int Count => this.Rooms.Count; public bool IsReadOnly => true; - public IRecordedRoom this[int index] => Rooms[index]; + public IRecordedRoom this[int index] => this.Rooms[index]; - public Recorder(ConfigV1 config, BasicWebhook webhook, Func iRecordedRoom) + public Recorder(Func iRecordedRoom) { - newIRecordedRoom = iRecordedRoom ?? throw new ArgumentNullException(nameof(iRecordedRoom)); - Config = config ?? throw new ArgumentNullException(nameof(config)); - Webhook = webhook ?? throw new ArgumentNullException(nameof(webhook)); + this.newIRecordedRoom = iRecordedRoom ?? throw new ArgumentNullException(nameof(iRecordedRoom)); - tokenSource = new CancellationTokenSource(); - Repeat.Interval(TimeSpan.FromSeconds(3), DownloadWatchdog, tokenSource.Token); + this.tokenSource = new CancellationTokenSource(); + Repeat.Interval(TimeSpan.FromSeconds(3), this.DownloadWatchdog, this.tokenSource.Token); - Rooms.CollectionChanged += (sender, e) => + this.Rooms.CollectionChanged += (sender, e) => { logger.Trace($"Rooms.CollectionChanged;{e.Action};" + $"O:{e.OldItems?.Cast()?.Select(rr => rr.RoomId.ToString())?.Aggregate((current, next) => current + "," + next)};" + @@ -53,15 +52,15 @@ namespace BililiveRecorder.Core public bool Initialize(string workdir) { logger.Debug("Initialize: " + workdir); - if (ConfigParser.Load(directory: workdir, config: Config)) + var config = ConfigParser.LoadFrom(directory: workdir); + if (config is not null) { - _valid = true; - Config.WorkDirectory = workdir; - if ((Config.RoomList?.Count ?? 0) > 0) - { - Config.RoomList.ForEach((r) => AddRoom(r.Roomid, r.Enabled)); - } - ConfigParser.Save(Config.WorkDirectory, Config); + this.Config = config; + this.Config.Global.WorkDirectory = workdir; + this.Webhook = new BasicWebhook(this.Config); + this._valid = true; + this.Config.Rooms.ForEach(r => this.AddRoom(r)); + ConfigParser.SaveTo(this.Config.Global.WorkDirectory, this.Config); return true; } else @@ -75,7 +74,7 @@ namespace BililiveRecorder.Core /// /// 房间号(支持短号) /// - public void AddRoom(int roomid) => AddRoom(roomid, true); + public void AddRoom(int roomid) => this.AddRoom(roomid, true); /// /// 添加直播间到录播姬 @@ -87,21 +86,19 @@ namespace BililiveRecorder.Core { try { - if (!_valid) { throw new InvalidOperationException("Not Initialized"); } + if (!this._valid) { throw new InvalidOperationException("Not Initialized"); } if (roomid <= 0) { throw new ArgumentOutOfRangeException(nameof(roomid), "房间号需要大于0"); } - var rr = newIRecordedRoom(roomid); - if (enabled) + var config = new RoomConfig { - Task.Run(() => rr.Start()); - } + RoomId = roomid, + AutoRecord = enabled, + }; - logger.Debug("AddRoom 添加了 {roomid} 直播间 ", rr.RoomId); - rr.RecordEnded += this.RecordedRoom_RecordEnded; - Rooms.Add(rr); + this.AddRoom(config); } catch (Exception ex) { @@ -109,6 +106,29 @@ namespace BililiveRecorder.Core } } + /// + /// 添加直播间到录播姬 + /// + /// 房间设置 + public void AddRoom(RoomConfig roomConfig) + { + try + { + if (!this._valid) { throw new InvalidOperationException("Not Initialized"); } + + roomConfig.SetParent(this.Config?.Global); + var rr = this.newIRecordedRoom(roomConfig); + + logger.Debug("AddRoom 添加了 {roomid} 直播间 ", rr.RoomId); + rr.RecordEnded += this.RecordedRoom_RecordEnded; + this.Rooms.Add(rr); + } + catch (Exception ex) + { + logger.Debug(ex, "AddRoom 添加 {roomid} 直播间错误 ", roomConfig.RoomId); + } + } + /// /// 从录播姬移除直播间 /// @@ -116,56 +136,49 @@ namespace BililiveRecorder.Core public void RemoveRoom(IRecordedRoom rr) { if (rr is null) return; - if (!_valid) { throw new InvalidOperationException("Not Initialized"); } + if (!this._valid) { throw new InvalidOperationException("Not Initialized"); } rr.Shutdown(); - rr.RecordEnded -= RecordedRoom_RecordEnded; + rr.RecordEnded -= this.RecordedRoom_RecordEnded; logger.Debug("RemoveRoom 移除了直播间 {roomid}", rr.RoomId); - Rooms.Remove(rr); + this.Rooms.Remove(rr); } private void Shutdown() { - if (!_valid) { return; } + if (!this._valid) { return; } logger.Debug("Shutdown called."); - tokenSource.Cancel(); + this.tokenSource.Cancel(); - SaveConfigToFile(); + this.SaveConfigToFile(); - Rooms.ToList().ForEach(rr => + this.Rooms.ToList().ForEach(rr => { rr.Shutdown(); }); - Rooms.Clear(); + this.Rooms.Clear(); } - private void RecordedRoom_RecordEnded(object sender, RecordEndData e) => Webhook.Send(e); + private void RecordedRoom_RecordEnded(object sender, RecordEndData e) => this.Webhook?.Send(e); public void SaveConfigToFile() { - Config.RoomList = new List(); - Rooms.ToList().ForEach(rr => - { - Config.RoomList.Add(new RoomV1() - { - Roomid = rr.RoomId, - Enabled = rr.IsMonitoring, - }); - }); + if (this.Config is null) return; - ConfigParser.Save(Config.WorkDirectory, Config); + this.Config.Rooms = this.Rooms.Select(x => x.RoomConfig).ToList(); + ConfigParser.SaveTo(this.Config.Global.WorkDirectory!, this.Config); } private void DownloadWatchdog() { - if (!_valid) { return; } + if (!this._valid) { return; } try { - Rooms.ToList().ForEach(room => + this.Rooms.ToList().ForEach(room => { if (room.IsRecording) { - if (DateTime.Now - room.LastUpdateDateTime > TimeSpan.FromMilliseconds(Config.TimingWatchdogTimeout)) + if (DateTime.Now - room.LastUpdateDateTime > TimeSpan.FromMilliseconds(this.Config!.Global.TimingWatchdogTimeout)) { logger.Warn("服务器未断开连接但停止提供 [{roomid}] 直播间的直播数据,通常是录制侧网络不稳定导致,将会断开重连", room.RoomId); room.StopRecord(); @@ -183,37 +196,37 @@ namespace BililiveRecorder.Core void ICollection.Add(IRecordedRoom item) => throw new NotSupportedException("Collection is readonly"); void ICollection.Clear() => throw new NotSupportedException("Collection is readonly"); bool ICollection.Remove(IRecordedRoom item) => throw new NotSupportedException("Collection is readonly"); - bool ICollection.Contains(IRecordedRoom item) => Rooms.Contains(item); - void ICollection.CopyTo(IRecordedRoom[] array, int arrayIndex) => Rooms.CopyTo(array, arrayIndex); - public IEnumerator GetEnumerator() => Rooms.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => Rooms.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => Rooms.GetEnumerator(); + bool ICollection.Contains(IRecordedRoom item) => this.Rooms.Contains(item); + void ICollection.CopyTo(IRecordedRoom[] array, int arrayIndex) => this.Rooms.CopyTo(array, arrayIndex); + public IEnumerator GetEnumerator() => this.Rooms.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => this.Rooms.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => this.Rooms.GetEnumerator(); public event PropertyChangedEventHandler PropertyChanged { - add => (Rooms as INotifyPropertyChanged).PropertyChanged += value; - remove => (Rooms as INotifyPropertyChanged).PropertyChanged -= value; + add => (this.Rooms as INotifyPropertyChanged).PropertyChanged += value; + remove => (this.Rooms as INotifyPropertyChanged).PropertyChanged -= value; } public event NotifyCollectionChangedEventHandler CollectionChanged { - add => (Rooms as INotifyCollectionChanged).CollectionChanged += value; - remove => (Rooms as INotifyCollectionChanged).CollectionChanged -= value; + add => (this.Rooms as INotifyCollectionChanged).CollectionChanged += value; + remove => (this.Rooms as INotifyCollectionChanged).CollectionChanged -= value; } protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!this.disposedValue) { if (disposing) { // dispose managed state (managed objects) - Shutdown(); + this.Shutdown(); } // free unmanaged resources (unmanaged objects) and override finalizer // set large fields to null - disposedValue = true; + this.disposedValue = true; } } @@ -227,7 +240,7 @@ namespace BililiveRecorder.Core public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + this.Dispose(disposing: true); GC.SuppressFinalize(this); } } diff --git a/BililiveRecorder.Core/Repeat.cs b/BililiveRecorder.Core/Repeat.cs index e8445b2..1b9dfa5 100644 --- a/BililiveRecorder.Core/Repeat.cs +++ b/BililiveRecorder.Core/Repeat.cs @@ -30,7 +30,7 @@ namespace BililiveRecorder.Core } } - static class CancellationTokenExtensions + internal static class CancellationTokenExtensions { public static bool WaitCancellationRequested( this CancellationToken token, diff --git a/BililiveRecorder.Core/StreamMonitor.cs b/BililiveRecorder.Core/StreamMonitor.cs index 93400a7..4ecdcaf 100644 --- a/BililiveRecorder.Core/StreamMonitor.cs +++ b/BililiveRecorder.Core/StreamMonitor.cs @@ -7,7 +7,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; -using BililiveRecorder.Core.Config; +using BililiveRecorder.Core.Config.V2; using Newtonsoft.Json; using NLog; using Timer = System.Timers.Timer; @@ -31,7 +31,7 @@ namespace BililiveRecorder.Core private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private readonly Func funcTcpClient; - private readonly ConfigV1 config; + private readonly RoomConfig roomConfig; private readonly BililiveAPI bililiveAPI; private Exception dmError = null; @@ -42,73 +42,74 @@ namespace BililiveRecorder.Core private bool dmConnectionTriggered = false; private readonly Timer httpTimer; - public int Roomid { get; private set; } = 0; + private int RoomId { get => this.roomConfig.RoomId; set => this.roomConfig.RoomId = value; } + public bool IsMonitoring { get; private set; } = false; - public bool IsDanmakuConnected => dmClient?.Connected ?? false; + public bool IsDanmakuConnected => this.dmClient?.Connected ?? false; public event RoomInfoUpdatedEvent RoomInfoUpdated; public event StreamStartedEvent StreamStarted; public event ReceivedDanmakuEvt ReceivedDanmaku; public event PropertyChangedEventHandler PropertyChanged; - public StreamMonitor(int roomid, Func funcTcpClient, ConfigV1 config, BililiveAPI bililiveAPI) + public StreamMonitor(RoomConfig roomConfig, Func funcTcpClient, BililiveAPI bililiveAPI) { this.funcTcpClient = funcTcpClient; - this.config = config; + this.roomConfig = roomConfig; this.bililiveAPI = bililiveAPI; - Roomid = roomid; + ReceivedDanmaku += this.Receiver_ReceivedDanmaku; + RoomInfoUpdated += this.StreamMonitor_RoomInfoUpdated; - ReceivedDanmaku += Receiver_ReceivedDanmaku; - RoomInfoUpdated += StreamMonitor_RoomInfoUpdated; - - dmTokenSource = new CancellationTokenSource(); + this.dmTokenSource = new CancellationTokenSource(); Repeat.Interval(TimeSpan.FromSeconds(30), () => { - if (dmNetStream != null && dmNetStream.CanWrite) + if (this.dmNetStream != null && this.dmNetStream.CanWrite) { try { - SendSocketData(2); + this.SendSocketData(2); } catch (Exception) { } } - }, dmTokenSource.Token); + }, this.dmTokenSource.Token); - httpTimer = new Timer(config.TimingCheckInterval * 1000) + this.httpTimer = new Timer(roomConfig.TimingCheckInterval * 1000) { Enabled = false, AutoReset = true, SynchronizingObject = null, Site = null }; - httpTimer.Elapsed += (sender, e) => + this.httpTimer.Elapsed += (sender, e) => { try { - Check(TriggerType.HttpApi); + this.Check(TriggerType.HttpApi); } catch (Exception ex) { - logger.Log(Roomid, LogLevel.Warn, "获取直播间开播状态出错", ex); + logger.Log(this.RoomId, LogLevel.Warn, "获取直播间开播状态出错", ex); } }; - config.PropertyChanged += (sender, e) => + roomConfig.PropertyChanged += (sender, e) => { - if (e.PropertyName.Equals(nameof(config.TimingCheckInterval))) + if (e.PropertyName.Equals(nameof(roomConfig.TimingCheckInterval))) { - httpTimer.Interval = config.TimingCheckInterval * 1000; + this.httpTimer.Interval = roomConfig.TimingCheckInterval * 1000; } }; } private void StreamMonitor_RoomInfoUpdated(object sender, RoomInfoUpdatedArgs e) { - Roomid = e.RoomInfo.RoomId; - if (!dmConnectionTriggered) + this.RoomId = e.RoomInfo.RoomId; + // TODO: RecordedRoom 里的 RoomInfoUpdated Handler 也会设置一次 RoomId + // 暂时保持不变,此处需要使用请求返回的房间号连接弹幕服务器 + if (!this.dmConnectionTriggered) { - dmConnectionTriggered = true; - Task.Run(() => ConnectWithRetryAsync()); + this.dmConnectionTriggered = true; + Task.Run(() => this.ConnectWithRetryAsync()); } } @@ -117,7 +118,7 @@ namespace BililiveRecorder.Core switch (e.Danmaku.MsgType) { case MsgTypeEnum.LiveStart: - if (IsMonitoring) + if (this.IsMonitoring) { Task.Run(() => StreamStarted?.Invoke(this, new StreamStartedArgs() { type = TriggerType.Danmaku })); } @@ -133,31 +134,31 @@ namespace BililiveRecorder.Core public bool Start() { - if (disposedValue) + if (this.disposedValue) { throw new ObjectDisposedException(nameof(StreamMonitor)); } - IsMonitoring = true; - httpTimer.Start(); - Check(TriggerType.HttpApi); + this.IsMonitoring = true; + this.httpTimer.Start(); + this.Check(TriggerType.HttpApi); return true; } public void Stop() { - if (disposedValue) + if (this.disposedValue) { throw new ObjectDisposedException(nameof(StreamMonitor)); } - IsMonitoring = false; - httpTimer.Stop(); + this.IsMonitoring = false; + this.httpTimer.Stop(); } public void Check(TriggerType type, int millisecondsDelay = 0) { - if (disposedValue) + if (this.disposedValue) { throw new ObjectDisposedException(nameof(StreamMonitor)); } @@ -170,7 +171,7 @@ namespace BililiveRecorder.Core Task.Run(async () => { await Task.Delay(millisecondsDelay).ConfigureAwait(false); - if ((await FetchRoomInfoAsync().ConfigureAwait(false)).IsStreaming) + if ((await this.FetchRoomInfoAsync().ConfigureAwait(false)).IsStreaming) { StreamStarted?.Invoke(this, new StreamStartedArgs() { type = type }); } @@ -179,7 +180,7 @@ namespace BililiveRecorder.Core public async Task FetchRoomInfoAsync() { - RoomInfo roomInfo = await bililiveAPI.GetRoomInfoAsync(Roomid).ConfigureAwait(false); + RoomInfo roomInfo = await this.bililiveAPI.GetRoomInfoAsync(this.RoomId).ConfigureAwait(false); if (roomInfo != null) RoomInfoUpdated?.Invoke(this, new RoomInfoUpdatedArgs { RoomInfo = roomInfo }); return roomInfo; @@ -191,46 +192,46 @@ namespace BililiveRecorder.Core private async Task ConnectWithRetryAsync() { bool connect_result = false; - while (!IsDanmakuConnected && !dmTokenSource.Token.IsCancellationRequested) + while (!this.IsDanmakuConnected && !this.dmTokenSource.Token.IsCancellationRequested) { - logger.Log(Roomid, LogLevel.Info, "连接弹幕服务器..."); - connect_result = await ConnectAsync().ConfigureAwait(false); + logger.Log(this.RoomId, LogLevel.Info, "连接弹幕服务器..."); + connect_result = await this.ConnectAsync().ConfigureAwait(false); if (!connect_result) - await Task.Delay((int)Math.Max(config.TimingDanmakuRetry, 0)); + await Task.Delay((int)Math.Max(this.roomConfig.TimingDanmakuRetry, 0)); } if (connect_result) { - logger.Log(Roomid, LogLevel.Info, "弹幕服务器连接成功"); + logger.Log(this.RoomId, LogLevel.Info, "弹幕服务器连接成功"); } } private async Task ConnectAsync() { - if (IsDanmakuConnected) { return true; } + if (this.IsDanmakuConnected) { return true; } try { - var (token, host, port) = await bililiveAPI.GetDanmuConf(Roomid); + var (token, host, port) = await this.bililiveAPI.GetDanmuConf(this.RoomId); - logger.Log(Roomid, LogLevel.Debug, $"连接弹幕服务器 {host}:{port} {(string.IsNullOrWhiteSpace(token) ? "无" : "有")} token"); + logger.Log(this.RoomId, LogLevel.Debug, $"连接弹幕服务器 {host}:{port} {(string.IsNullOrWhiteSpace(token) ? "无" : "有")} token"); - dmClient = funcTcpClient(); - await dmClient.ConnectAsync(host, port).ConfigureAwait(false); - dmNetStream = dmClient.GetStream(); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsDanmakuConnected))); + this.dmClient = this.funcTcpClient(); + await this.dmClient.ConnectAsync(host, port).ConfigureAwait(false); + this.dmNetStream = this.dmClient.GetStream(); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsDanmakuConnected))); - dmReceiveMessageLoopThread = new Thread(ReceiveMessageLoop) + this.dmReceiveMessageLoopThread = new Thread(this.ReceiveMessageLoop) { - Name = "ReceiveMessageLoop " + Roomid, + Name = "ReceiveMessageLoop " + this.RoomId, IsBackground = true }; - dmReceiveMessageLoopThread.Start(); + this.dmReceiveMessageLoopThread.Start(); var hello = JsonConvert.SerializeObject(new { uid = 0, - roomid = Roomid, + roomid = this.RoomId, protover = 2, platform = "web", clientver = "1.11.0", @@ -238,30 +239,30 @@ namespace BililiveRecorder.Core key = token, }, Formatting.None); - SendSocketData(7, hello); - SendSocketData(2); + this.SendSocketData(7, hello); + this.SendSocketData(2); return true; } catch (Exception ex) { - dmError = ex; - logger.Log(Roomid, LogLevel.Warn, "连接弹幕服务器错误", ex); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsDanmakuConnected))); + this.dmError = ex; + logger.Log(this.RoomId, LogLevel.Warn, "连接弹幕服务器错误", ex); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsDanmakuConnected))); return false; } } private void ReceiveMessageLoop() { - logger.Log(Roomid, LogLevel.Trace, "ReceiveMessageLoop Started"); + logger.Log(this.RoomId, LogLevel.Trace, "ReceiveMessageLoop Started"); try { var stableBuffer = new byte[16]; var buffer = new byte[4096]; - while (IsDanmakuConnected) + while (this.IsDanmakuConnected) { - dmNetStream.ReadB(stableBuffer, 0, 16); + this.dmNetStream.ReadB(stableBuffer, 0, 16); Parse2Protocol(stableBuffer, out DanmakuProtocol protocol); if (protocol.PacketLength < 16) @@ -280,7 +281,7 @@ namespace BililiveRecorder.Core buffer = new byte[payloadlength]; } - dmNetStream.ReadB(buffer, 0, payloadlength); + this.dmNetStream.ReadB(buffer, 0, payloadlength); if (protocol.Version == 2 && protocol.Action == 5) // 处理deflate消息 { @@ -324,7 +325,7 @@ namespace BililiveRecorder.Core } catch (Exception ex) { - logger.Log(Roomid, LogLevel.Warn, "", ex); + logger.Log(this.RoomId, LogLevel.Warn, "", ex); } break; default: @@ -335,25 +336,25 @@ namespace BililiveRecorder.Core } catch (Exception ex) { - dmError = ex; + this.dmError = ex; // logger.Error(ex); - logger.Log(Roomid, LogLevel.Debug, "Disconnected"); - dmClient?.Close(); - dmNetStream = null; - if (!(dmTokenSource?.IsCancellationRequested ?? true)) + logger.Log(this.RoomId, LogLevel.Debug, "Disconnected"); + this.dmClient?.Close(); + this.dmNetStream = null; + if (!(this.dmTokenSource?.IsCancellationRequested ?? true)) { - logger.Log(Roomid, LogLevel.Warn, "弹幕连接被断开,将尝试重连", ex); + logger.Log(this.RoomId, LogLevel.Warn, "弹幕连接被断开,将尝试重连", ex); Task.Run(async () => { - await Task.Delay((int)Math.Max(config.TimingDanmakuRetry, 0)); - await ConnectWithRetryAsync(); + await Task.Delay((int)Math.Max(this.roomConfig.TimingDanmakuRetry, 0)); + await this.ConnectWithRetryAsync(); }); } } finally { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsDanmakuConnected))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsDanmakuConnected))); } } @@ -382,8 +383,8 @@ namespace BililiveRecorder.Core { ms.Write(playload, 0, playload.Length); } - dmNetStream.Write(buffer, 0, buffer.Length); - dmNetStream.Flush(); + this.dmNetStream.Write(buffer, 0, buffer.Length); + this.dmNetStream.Flush(); } } @@ -423,11 +424,11 @@ namespace BililiveRecorder.Core /// public void ChangeEndian() { - PacketLength = IPAddress.HostToNetworkOrder(PacketLength); - HeaderLength = IPAddress.HostToNetworkOrder(HeaderLength); - Version = IPAddress.HostToNetworkOrder(Version); - Action = IPAddress.HostToNetworkOrder(Action); - Parameter = IPAddress.HostToNetworkOrder(Parameter); + this.PacketLength = IPAddress.HostToNetworkOrder(this.PacketLength); + this.HeaderLength = IPAddress.HostToNetworkOrder(this.HeaderLength); + this.Version = IPAddress.HostToNetworkOrder(this.Version); + this.Action = IPAddress.HostToNetworkOrder(this.Action); + this.Parameter = IPAddress.HostToNetworkOrder(this.Parameter); } } @@ -439,25 +440,25 @@ namespace BililiveRecorder.Core protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!this.disposedValue) { if (disposing) { - dmTokenSource?.Cancel(); - dmTokenSource?.Dispose(); - httpTimer?.Dispose(); - dmClient?.Close(); + this.dmTokenSource?.Cancel(); + this.dmTokenSource?.Dispose(); + this.httpTimer?.Dispose(); + this.dmClient?.Close(); } - dmNetStream = null; - disposedValue = true; + this.dmNetStream = null; + this.disposedValue = true; } } public void Dispose() { // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。 - Dispose(true); + this.Dispose(true); } #endregion } diff --git a/BililiveRecorder.Core/StreamStatus.cs b/BililiveRecorder.Core/StreamStatus.cs index 7d33a50..046e15e 100644 --- a/BililiveRecorder.Core/StreamStatus.cs +++ b/BililiveRecorder.Core/StreamStatus.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace BililiveRecorder.Core +namespace BililiveRecorder.Core { public enum TriggerType { diff --git a/BililiveRecorder.FlvProcessor/FlvClipProcessor.cs b/BililiveRecorder.FlvProcessor/FlvClipProcessor.cs index a823b4e..48ef3ec 100644 --- a/BililiveRecorder.FlvProcessor/FlvClipProcessor.cs +++ b/BililiveRecorder.FlvProcessor/FlvClipProcessor.cs @@ -1,7 +1,7 @@ -using NLog; using System; using System.Collections.Generic; using System.IO; +using NLog; namespace BililiveRecorder.FlvProcessor { @@ -25,22 +25,22 @@ namespace BililiveRecorder.FlvProcessor public IFlvClipProcessor Initialize(string path, IFlvMetadata metadata, List head, List data, uint seconds) { this.path = path; - Header = metadata; // TODO: Copy a copy, do not share - HTags = head; - Tags = data; - target = Tags[Tags.Count - 1].TimeStamp + (int)(seconds * FlvStreamProcessor.SEC_TO_MS); + this.Header = metadata; // TODO: Copy a copy, do not share + this.HTags = head; + this.Tags = data; + this.target = this.Tags[this.Tags.Count - 1].TimeStamp + (int)(seconds * FlvStreamProcessor.SEC_TO_MS); logger.Debug("Clip 创建 Tags.Count={0} Tags[0].TimeStamp={1} Tags[Tags.Count-1].TimeStamp={2} Tags里秒数={3}", - Tags.Count, Tags[0].TimeStamp, Tags[Tags.Count - 1].TimeStamp, (Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp) / 1000d); + this.Tags.Count, this.Tags[0].TimeStamp, this.Tags[this.Tags.Count - 1].TimeStamp, (this.Tags[this.Tags.Count - 1].TimeStamp - this.Tags[0].TimeStamp) / 1000d); return this; } public void AddTag(IFlvTag tag) { - Tags.Add(tag); - if (tag.TimeStamp >= target) + this.Tags.Add(tag); + if (tag.TimeStamp >= this.target) { - FinallizeFile(); + this.FinallizeFile(); } } @@ -48,40 +48,40 @@ namespace BililiveRecorder.FlvProcessor { try { - if (!Directory.Exists(Path.GetDirectoryName(path))) + if (!Directory.Exists(Path.GetDirectoryName(this.path))) { - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(this.path)); } - using (var fs = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite)) + using (var fs = new FileStream(this.path, FileMode.CreateNew, FileAccess.ReadWrite)) { fs.Write(FlvStreamProcessor.FLV_HEADER_BYTES, 0, FlvStreamProcessor.FLV_HEADER_BYTES.Length); fs.Write(new byte[] { 0, 0, 0, 0, }, 0, 4); - double clipDuration = (Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp) / 1000d; - Header["duration"] = clipDuration; - Header["lasttimestamp"] = (double)(Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp); + double clipDuration = (this.Tags[this.Tags.Count - 1].TimeStamp - this.Tags[0].TimeStamp) / 1000d; + this.Header["duration"] = clipDuration; + this.Header["lasttimestamp"] = (double)(this.Tags[this.Tags.Count - 1].TimeStamp - this.Tags[0].TimeStamp); - var t = funcFlvTag(); + var t = this.funcFlvTag(); t.TagType = TagType.DATA; - if (Header.ContainsKey("BililiveRecorder")) + if (this.Header.ContainsKey("BililiveRecorder")) { // TODO: 更好的写法 - (Header["BililiveRecorder"] as Dictionary)["starttime"] = DateTime.UtcNow - TimeSpan.FromSeconds(clipDuration); + (this.Header["BililiveRecorder"] as Dictionary)["starttime"] = DateTime.UtcNow - TimeSpan.FromSeconds(clipDuration); } - t.Data = Header.ToBytes(); + t.Data = this.Header.ToBytes(); t.WriteTo(fs); - int offset = Tags[0].TimeStamp; + int offset = this.Tags[0].TimeStamp; - HTags.ForEach(tag => tag.WriteTo(fs)); - Tags.ForEach(tag => tag.WriteTo(fs, offset)); + this.HTags.ForEach(tag => tag.WriteTo(fs)); + this.Tags.ForEach(tag => tag.WriteTo(fs, offset)); - logger.Info("剪辑已保存:{0}", Path.GetFileName(path)); + logger.Info("剪辑已保存:{0}", Path.GetFileName(this.path)); fs.Close(); } - Tags.Clear(); + this.Tags.Clear(); } catch (IOException ex) { diff --git a/BililiveRecorder.FlvProcessor/FlvMetadata.cs b/BililiveRecorder.FlvProcessor/FlvMetadata.cs index 626d550..5b9c918 100644 --- a/BililiveRecorder.FlvProcessor/FlvMetadata.cs +++ b/BililiveRecorder.FlvProcessor/FlvMetadata.cs @@ -11,20 +11,20 @@ namespace BililiveRecorder.FlvProcessor { private IDictionary Meta { get; set; } = new Dictionary(); - public ICollection Keys => Meta.Keys; + public ICollection Keys => this.Meta.Keys; - public ICollection Values => Meta.Values; + public ICollection Values => this.Meta.Values; - public int Count => Meta.Count; + public int Count => this.Meta.Count; public bool IsReadOnly => false; - public object this[string key] { get => Meta[key]; set => Meta[key] = value; } + public object this[string key] { get => this.Meta[key]; set => this.Meta[key] = value; } public FlvMetadata() { - Meta["duration"] = 0.0; - Meta["lasttimestamp"] = 0.0; + this.Meta["duration"] = 0.0; + this.Meta["lasttimestamp"] = 0.0; } public FlvMetadata(byte[] data) @@ -37,24 +37,24 @@ namespace BililiveRecorder.FlvProcessor { throw new Exception("Isn't onMetadata"); } - Meta = DecodeScriptDataValue(data, ref readHead) as Dictionary; + this.Meta = DecodeScriptDataValue(data, ref readHead) as Dictionary; - if (!Meta.ContainsKey("duration")) + if (!this.Meta.ContainsKey("duration")) { - Meta["duration"] = 0d; + this.Meta["duration"] = 0d; } - if (!Meta.ContainsKey("lasttimestamp")) + if (!this.Meta.ContainsKey("lasttimestamp")) { - Meta["lasttimestamp"] = 0d; + this.Meta["lasttimestamp"] = 0d; } - Meta.Remove(""); - foreach (var item in Meta.ToArray()) + this.Meta.Remove(""); + foreach (var item in this.Meta.ToArray()) { if (item.Value is string text) { - Meta[item.Key] = text.Replace("\0", ""); + this.Meta[item.Key] = text.Replace("\0", ""); } } } @@ -64,7 +64,7 @@ namespace BililiveRecorder.FlvProcessor using (var ms = new MemoryStream()) { EncodeScriptDataValue(ms, "onMetaData"); - EncodeScriptDataValue(ms, Meta); + EncodeScriptDataValue(ms, this.Meta); return ms.ToArray(); } } @@ -278,57 +278,57 @@ namespace BililiveRecorder.FlvProcessor public void Add(string key, object value) { - Meta.Add(key, value); + this.Meta.Add(key, value); } public bool ContainsKey(string key) { - return Meta.ContainsKey(key); + return this.Meta.ContainsKey(key); } public bool Remove(string key) { - return Meta.Remove(key); + return this.Meta.Remove(key); } public bool TryGetValue(string key, out object value) { - return Meta.TryGetValue(key, out value); + return this.Meta.TryGetValue(key, out value); } public void Add(KeyValuePair item) { - Meta.Add(item); + this.Meta.Add(item); } public void Clear() { - Meta.Clear(); + this.Meta.Clear(); } public bool Contains(KeyValuePair item) { - return Meta.Contains(item); + return this.Meta.Contains(item); } public void CopyTo(KeyValuePair[] array, int arrayIndex) { - Meta.CopyTo(array, arrayIndex); + this.Meta.CopyTo(array, arrayIndex); } public bool Remove(KeyValuePair item) { - return Meta.Remove(item); + return this.Meta.Remove(item); } public IEnumerator> GetEnumerator() { - return Meta.GetEnumerator(); + return this.Meta.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { - return Meta.GetEnumerator(); + return this.Meta.GetEnumerator(); } diff --git a/BililiveRecorder.FlvProcessor/FlvStreamProcessor.cs b/BililiveRecorder.FlvProcessor/FlvStreamProcessor.cs index 231e098..0b61d96 100644 --- a/BililiveRecorder.FlvProcessor/FlvStreamProcessor.cs +++ b/BililiveRecorder.FlvProcessor/FlvStreamProcessor.cs @@ -49,7 +49,7 @@ namespace BililiveRecorder.FlvProcessor private int _tagAudioCount = 0; public int TotalMaxTimestamp { get; private set; } = 0; - public int CurrentMaxTimestamp { get => TotalMaxTimestamp - _writeTimeStamp; } + public int CurrentMaxTimestamp { get => this.TotalMaxTimestamp - this._writeTimeStamp; } private readonly Func funcFlvClipProcessor; private readonly Func funcFlvMetadata; @@ -83,56 +83,56 @@ namespace BililiveRecorder.FlvProcessor public IFlvStreamProcessor Initialize(Func<(string fullPath, string relativePath)> getStreamFileName, Func getClipFileName, EnabledFeature enabledFeature, AutoCuttingMode autoCuttingMode) { - GetStreamFileName = getStreamFileName; - GetClipFileName = getClipFileName; - EnabledFeature = enabledFeature; - CuttingMode = autoCuttingMode; + this.GetStreamFileName = getStreamFileName; + this.GetClipFileName = getClipFileName; + this.EnabledFeature = enabledFeature; + this.CuttingMode = autoCuttingMode; return this; } private void OpenNewRecordFile() { - var (fullPath, relativePath) = GetStreamFileName(); + var (fullPath, relativePath) = this.GetStreamFileName(); logger.Debug("打开新录制文件: " + fullPath); try { Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); } catch (Exception) { } - _targetFile = new FileStream(fullPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read | FileShare.Delete); + this._targetFile = new FileStream(fullPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read | FileShare.Delete); - if (_headerParsed) + if (this._headerParsed) { - _targetFile.Write(FlvStreamProcessor.FLV_HEADER_BYTES, 0, FlvStreamProcessor.FLV_HEADER_BYTES.Length); - _targetFile.Write(new byte[] { 0, 0, 0, 0, }, 0, 4); + this._targetFile.Write(FlvStreamProcessor.FLV_HEADER_BYTES, 0, FlvStreamProcessor.FLV_HEADER_BYTES.Length); + this._targetFile.Write(new byte[] { 0, 0, 0, 0, }, 0, 4); - var script_tag = funcFlvTag(); + var script_tag = this.funcFlvTag(); script_tag.TagType = TagType.DATA; - if (Metadata.ContainsKey("BililiveRecorder")) + if (this.Metadata.ContainsKey("BililiveRecorder")) { // TODO: 更好的写法 - (Metadata["BililiveRecorder"] as Dictionary)["starttime"] = DateTime.UtcNow; + (this.Metadata["BililiveRecorder"] as Dictionary)["starttime"] = DateTime.UtcNow; } - script_tag.Data = Metadata.ToBytes(); - script_tag.WriteTo(_targetFile); + script_tag.Data = this.Metadata.ToBytes(); + script_tag.WriteTo(this._targetFile); - _headerTags.ForEach(tag => tag.WriteTo(_targetFile)); + this._headerTags.ForEach(tag => tag.WriteTo(this._targetFile)); } } public void AddBytes(byte[] data) { - lock (_writelock) + lock (this._writelock) { - if (_finalized) { return; /*throw new InvalidOperationException("Processor Already Closed");*/ } - if (_leftover != null) + if (this._finalized) { return; /*throw new InvalidOperationException("Processor Already Closed");*/ } + if (this._leftover != null) { - byte[] c = new byte[_leftover.Length + data.Length]; - _leftover.CopyTo(c, 0); - data.CopyTo(c, _leftover.Length); - _leftover = null; - ParseBytes(c); + byte[] c = new byte[this._leftover.Length + data.Length]; + this._leftover.CopyTo(c, 0); + data.CopyTo(c, this._leftover.Length); + this._leftover = null; + this.ParseBytes(c); } else { - ParseBytes(data); + this.ParseBytes(data); } } } @@ -140,14 +140,14 @@ namespace BililiveRecorder.FlvProcessor private void ParseBytes(byte[] data) { int position = 0; - if (!_headerParsed) { ReadFlvHeader(); } + if (!this._headerParsed) { ReadFlvHeader(); } while (position < data.Length) { - if (_currentTag == null) + if (this._currentTag == null) { if (!ParseTagHead()) { - _leftover = data.Skip(position).ToArray(); + this._leftover = data.Skip(position).ToArray(); break; } } @@ -159,7 +159,7 @@ namespace BililiveRecorder.FlvProcessor if (data.Length - position < 15) { return false; } byte[] b = new byte[4]; - IFlvTag tag = funcFlvTag(); + IFlvTag tag = this.funcFlvTag(); // Previous Tag Size UI24 position += 4; @@ -186,21 +186,21 @@ namespace BililiveRecorder.FlvProcessor tag.StreamId[1] = data[position++]; tag.StreamId[2] = data[position++]; - _currentTag = tag; + this._currentTag = tag; return true; } void FillTagData() { - int toRead = Math.Min(data.Length - position, _currentTag.TagSize - (int)_data.Position); - _data.Write(buffer: data, offset: position, count: toRead); + int toRead = Math.Min(data.Length - position, this._currentTag.TagSize - (int)this._data.Position); + this._data.Write(buffer: data, offset: position, count: toRead); position += toRead; - if ((int)_data.Position == _currentTag.TagSize) + if ((int)this._data.Position == this._currentTag.TagSize) { - _currentTag.Data = _data.ToArray(); - _data.SetLength(0); // reset data buffer - TagCreated(_currentTag); - _currentTag = null; + this._currentTag.Data = this._data.ToArray(); + this._data.SetLength(0); // reset data buffer + this.TagCreated(this._currentTag); + this._currentTag = null; } } void ReadFlvHeader() @@ -223,108 +223,108 @@ namespace BililiveRecorder.FlvProcessor throw new NotSupportedException("Not FLV Stream or Not Supported"); // TODO: custom Exception. } - _headerParsed = true; + this._headerParsed = true; position += FLV_HEADER_BYTES.Length; } } private void TagCreated(IFlvTag tag) { - if (Metadata == null) + if (this.Metadata == null) { ParseMetadata(); } else { - if (!_hasOffset) { ParseTimestampOffset(); } + if (!this._hasOffset) { ParseTimestampOffset(); } SetTimestamp(); - if (EnabledFeature.IsRecordEnabled()) { ProcessRecordLogic(); } - if (EnabledFeature.IsClipEnabled()) { ProcessClipLogic(); } + if (this.EnabledFeature.IsRecordEnabled()) { ProcessRecordLogic(); } + if (this.EnabledFeature.IsClipEnabled()) { ProcessClipLogic(); } TagProcessed?.Invoke(this, new TagProcessedArgs() { Tag = tag }); } return; void SetTimestamp() { - if (_hasOffset) + if (this._hasOffset) { - tag.SetTimeStamp(tag.TimeStamp - _baseTimeStamp); - TotalMaxTimestamp = Math.Max(TotalMaxTimestamp, tag.TimeStamp); + tag.SetTimeStamp(tag.TimeStamp - this._baseTimeStamp); + this.TotalMaxTimestamp = Math.Max(this.TotalMaxTimestamp, tag.TimeStamp); } else { tag.SetTimeStamp(0); } } void ProcessRecordLogic() { - if (CuttingMode != AutoCuttingMode.Disabled && tag.IsVideoKeyframe) + if (this.CuttingMode != AutoCuttingMode.Disabled && tag.IsVideoKeyframe) { - bool byTime = (CuttingMode == AutoCuttingMode.ByTime) && (CurrentMaxTimestamp / 1000 >= CuttingNumber * 60); - bool bySize = (CuttingMode == AutoCuttingMode.BySize) && ((_targetFile.Length / 1024 / 1024) >= CuttingNumber); + bool byTime = (this.CuttingMode == AutoCuttingMode.ByTime) && (this.CurrentMaxTimestamp / 1000 >= this.CuttingNumber * 60); + bool bySize = (this.CuttingMode == AutoCuttingMode.BySize) && ((this._targetFile.Length / 1024 / 1024) >= this.CuttingNumber); if (byTime || bySize) { - FinallizeCurrentFile(); - OpenNewRecordFile(); - _writeTimeStamp = TotalMaxTimestamp; + this.FinallizeCurrentFile(); + this.OpenNewRecordFile(); + this._writeTimeStamp = this.TotalMaxTimestamp; } } - if (!(_targetFile?.CanWrite ?? false)) + if (!(this._targetFile?.CanWrite ?? false)) { - OpenNewRecordFile(); + this.OpenNewRecordFile(); } - tag.WriteTo(_targetFile, _writeTimeStamp); + tag.WriteTo(this._targetFile, this._writeTimeStamp); } void ProcessClipLogic() { - _tags.Add(tag); + this._tags.Add(tag); // 移除过旧的数据 - if (TotalMaxTimestamp - _lasttimeRemovedTimestamp > 800) + if (this.TotalMaxTimestamp - this._lasttimeRemovedTimestamp > 800) { - _lasttimeRemovedTimestamp = TotalMaxTimestamp; - int max_remove_index = _tags.FindLastIndex(x => x.IsVideoKeyframe && ((TotalMaxTimestamp - x.TimeStamp) > (ClipLengthPast * SEC_TO_MS))); + this._lasttimeRemovedTimestamp = this.TotalMaxTimestamp; + int max_remove_index = this._tags.FindLastIndex(x => x.IsVideoKeyframe && ((this.TotalMaxTimestamp - x.TimeStamp) > (this.ClipLengthPast * SEC_TO_MS))); if (max_remove_index > 0) { - _tags.RemoveRange(0, max_remove_index); + this._tags.RemoveRange(0, max_remove_index); } // Tags.RemoveRange(0, max_remove_index + 1 - 1); // 给将来的备注:这里是故意 + 1 - 1 的,因为要保留选中的那个关键帧, + 1 就把关键帧删除了 } - Clips.ToList().ForEach(fcp => fcp.AddTag(tag)); + this.Clips.ToList().ForEach(fcp => fcp.AddTag(tag)); } void ParseTimestampOffset() { if (tag.TagType == TagType.VIDEO) { - _tagVideoCount++; - if (_tagVideoCount < 2) + this._tagVideoCount++; + if (this._tagVideoCount < 2) { logger.Debug("第一个 Video Tag 时间戳 {0} ms", tag.TimeStamp); - _headerTags.Add(tag); + this._headerTags.Add(tag); } else { - _baseTimeStamp = tag.TimeStamp; - _hasOffset = true; - logger.Debug("重设时间戳 {0} 毫秒", _baseTimeStamp); + this._baseTimeStamp = tag.TimeStamp; + this._hasOffset = true; + logger.Debug("重设时间戳 {0} 毫秒", this._baseTimeStamp); } } else if (tag.TagType == TagType.AUDIO) { - _tagAudioCount++; - if (_tagAudioCount < 2) + this._tagAudioCount++; + if (this._tagAudioCount < 2) { logger.Debug("第一个 Audio Tag 时间戳 {0} ms", tag.TimeStamp); - _headerTags.Add(tag); + this._headerTags.Add(tag); } else { - _baseTimeStamp = tag.TimeStamp; - _hasOffset = true; - logger.Debug("重设时间戳 {0} 毫秒", _baseTimeStamp); + this._baseTimeStamp = tag.TimeStamp; + this._hasOffset = true; + logger.Debug("重设时间戳 {0} 毫秒", this._baseTimeStamp); } } } @@ -332,15 +332,15 @@ namespace BililiveRecorder.FlvProcessor { if (tag.TagType == TagType.DATA) { - _targetFile?.Write(FLV_HEADER_BYTES, 0, FLV_HEADER_BYTES.Length); - _targetFile?.Write(new byte[] { 0, 0, 0, 0, }, 0, 4); + this._targetFile?.Write(FLV_HEADER_BYTES, 0, FLV_HEADER_BYTES.Length); + this._targetFile?.Write(new byte[] { 0, 0, 0, 0, }, 0, 4); - Metadata = funcFlvMetadata(tag.Data); + this.Metadata = this.funcFlvMetadata(tag.Data); OnMetaData?.Invoke(this, new FlvMetadataArgs() { Metadata = Metadata }); - tag.Data = Metadata.ToBytes(); - tag.WriteTo(_targetFile); + tag.Data = this.Metadata.ToBytes(); + tag.WriteTo(this._targetFile); } else { @@ -351,19 +351,19 @@ namespace BililiveRecorder.FlvProcessor public IFlvClipProcessor Clip() { - if (!EnabledFeature.IsClipEnabled()) { return null; } - lock (_writelock) + if (!this.EnabledFeature.IsClipEnabled()) { return null; } + lock (this._writelock) { - if (_finalized) + if (this._finalized) { return null; // throw new InvalidOperationException("Processor Already Closed"); } - logger.Info("剪辑处理中,将会保存过去 {0} 秒和将来 {1} 秒的直播流", (_tags[_tags.Count - 1].TimeStamp - _tags[0].TimeStamp) / 1000d, ClipLengthFuture); - IFlvClipProcessor clip = funcFlvClipProcessor().Initialize(GetClipFileName(), Metadata, _headerTags, new List(_tags.ToArray()), ClipLengthFuture); - clip.ClipFinalized += (sender, e) => { Clips.Remove(e.ClipProcessor); }; - Clips.Add(clip); + logger.Info("剪辑处理中,将会保存过去 {0} 秒和将来 {1} 秒的直播流", (this._tags[this._tags.Count - 1].TimeStamp - this._tags[0].TimeStamp) / 1000d, this.ClipLengthFuture); + IFlvClipProcessor clip = this.funcFlvClipProcessor().Initialize(this.GetClipFileName(), this.Metadata, this._headerTags, new List(this._tags.ToArray()), this.ClipLengthFuture); + clip.ClipFinalized += (sender, e) => { this.Clips.Remove(e.ClipProcessor); }; + this.Clips.Add(clip); return clip; } } @@ -372,16 +372,16 @@ namespace BililiveRecorder.FlvProcessor { try { - var fileSize = _targetFile?.Length ?? -1; - logger.Debug("正在关闭当前录制文件: " + _targetFile?.Name); - Metadata["duration"] = CurrentMaxTimestamp / 1000.0; - Metadata["lasttimestamp"] = (double)CurrentMaxTimestamp; - byte[] metadata = Metadata.ToBytes(); + var fileSize = this._targetFile?.Length ?? -1; + logger.Debug("正在关闭当前录制文件: " + this._targetFile?.Name); + this.Metadata["duration"] = this.CurrentMaxTimestamp / 1000.0; + this.Metadata["lasttimestamp"] = (double)this.CurrentMaxTimestamp; + byte[] metadata = this.Metadata.ToBytes(); // 13 for FLV header & "0th" tag size // 11 for 1st tag header - _targetFile?.Seek(13 + 11, SeekOrigin.Begin); - _targetFile?.Write(metadata, 0, metadata.Length); + this._targetFile?.Seek(13 + 11, SeekOrigin.Begin); + this._targetFile?.Write(metadata, 0, metadata.Length); if (fileSize > 0) FileFinalized?.Invoke(this, fileSize); @@ -396,30 +396,30 @@ namespace BililiveRecorder.FlvProcessor } finally { - _targetFile?.Close(); - _targetFile = null; + this._targetFile?.Close(); + this._targetFile = null; } } public void FinallizeFile() { - if (!_finalized) + if (!this._finalized) { - lock (_writelock) + lock (this._writelock) { try { - FinallizeCurrentFile(); + this.FinallizeCurrentFile(); } finally { - _targetFile?.Close(); - _data.Close(); - _tags.Clear(); + this._targetFile?.Close(); + this._data.Close(); + this._tags.Clear(); - _finalized = true; + this._finalized = true; - Clips.ToList().ForEach(fcp => fcp.FinallizeFile()); // TODO: check + this.Clips.ToList().ForEach(fcp => fcp.FinallizeFile()); // TODO: check StreamFinalized?.Invoke(this, new StreamFinalizedArgs() { StreamProcessor = this }); } } @@ -431,24 +431,24 @@ namespace BililiveRecorder.FlvProcessor protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!this.disposedValue) { if (disposing) { - _data.Dispose(); - _targetFile?.Dispose(); + this._data.Dispose(); + this._targetFile?.Dispose(); OnMetaData = null; StreamFinalized = null; TagProcessed = null; } - _tags.Clear(); - disposedValue = true; + this._tags.Clear(); + this.disposedValue = true; } } public void Dispose() { - Dispose(true); + this.Dispose(true); } #endregion } diff --git a/BililiveRecorder.FlvProcessor/FlvTag.cs b/BililiveRecorder.FlvProcessor/FlvTag.cs index 015d8d7..c4ad4a2 100644 --- a/BililiveRecorder.FlvProcessor/FlvTag.cs +++ b/BililiveRecorder.FlvProcessor/FlvTag.cs @@ -15,10 +15,10 @@ namespace BililiveRecorder.FlvProcessor public int Profile { get; private set; } = -1; public int Level { get; private set; } = -1; - public byte[] Data { get => _data; set { _data = value; ParseInfo(); } } + public byte[] Data { get => this._data; set { this._data = value; this.ParseInfo(); } } private byte[] _data = null; - public void SetTimeStamp(int timestamp) => TimeStamp = timestamp; + public void SetTimeStamp(int timestamp) => this.TimeStamp = timestamp; private void ParseInfo() { @@ -46,42 +46,42 @@ namespace BililiveRecorder.FlvProcessor * AVCLevelIndication * */ - IsVideoKeyframe = false; - Profile = -1; - Level = -1; + this.IsVideoKeyframe = false; + this.Profile = -1; + this.Level = -1; - if (TagType != TagType.VIDEO) { return; } - if (Data.Length < 9) { return; } + if (this.TagType != TagType.VIDEO) { return; } + if (this.Data.Length < 9) { return; } // Not AVC Keyframe - if (Data[0] != 0x17) { return; } + if (this.Data[0] != 0x17) { return; } - IsVideoKeyframe = true; + this.IsVideoKeyframe = true; // Isn't AVCDecoderConfigurationRecord - if (Data[1] != 0x00) { return; } + if (this.Data[1] != 0x00) { return; } // version is not 1 - if (Data[5] != 0x01) { return; } + if (this.Data[5] != 0x01) { return; } - Profile = Data[6]; - Level = Data[8]; + this.Profile = this.Data[6]; + this.Level = this.Data[8]; #if DEBUG - Debug.WriteLine("Video Profile: " + Profile + ", Level: " + Level); + Debug.WriteLine("Video Profile: " + this.Profile + ", Level: " + this.Level); #endif } public byte[] ToBytes(bool useDataSize, int offset = 0) { var tag = new byte[11]; - tag[0] = (byte)TagType; - var size = BitConverter.GetBytes(useDataSize ? Data.Length : TagSize).ToBE(); + tag[0] = (byte)this.TagType; + var size = BitConverter.GetBytes(useDataSize ? this.Data.Length : this.TagSize).ToBE(); Buffer.BlockCopy(size, 1, tag, 1, 3); - byte[] timing = BitConverter.GetBytes(TimeStamp - offset).ToBE(); + byte[] timing = BitConverter.GetBytes(this.TimeStamp - offset).ToBE(); Buffer.BlockCopy(timing, 1, tag, 4, 3); Buffer.BlockCopy(timing, 0, tag, 7, 1); - Buffer.BlockCopy(StreamId, 0, tag, 8, 3); + Buffer.BlockCopy(this.StreamId, 0, tag, 8, 3); return tag; } @@ -90,22 +90,22 @@ namespace BililiveRecorder.FlvProcessor { if (stream != null) { - var vs = ToBytes(true, offset); + var vs = this.ToBytes(true, offset); stream.Write(vs, 0, vs.Length); - stream.Write(Data, 0, Data.Length); - stream.Write(BitConverter.GetBytes(Data.Length + vs.Length).ToBE(), 0, 4); + stream.Write(this.Data, 0, this.Data.Length); + stream.Write(BitConverter.GetBytes(this.Data.Length + vs.Length).ToBE(), 0, 4); } } private int _ParseIsVideoKeyframe() { - if (TagType != TagType.VIDEO) { return 0; } - if (Data.Length < 1) { return -1; } + if (this.TagType != TagType.VIDEO) { return 0; } + if (this.Data.Length < 1) { return -1; } const byte mask = 0b00001111; const byte compare = 0b00011111; - return (Data[0] | mask) == compare ? 1 : 0; + return (this.Data[0] | mask) == compare ? 1 : 0; } } } diff --git a/BililiveRecorder.WPF/App.xaml.cs b/BililiveRecorder.WPF/App.xaml.cs index 3a86b4d..39bc6bc 100644 --- a/BililiveRecorder.WPF/App.xaml.cs +++ b/BililiveRecorder.WPF/App.xaml.cs @@ -82,7 +82,7 @@ namespace BililiveRecorder.WPF logger.Warn(ex, "检查更新时出错,如持续出错请联系开发者 rec@danmuji.org"); } - _ = Task.Run(async () => { await Task.Delay(TimeSpan.FromDays(1)); await RunCheckUpdate(); }); + _ = Task.Run(async () => { await Task.Delay(TimeSpan.FromDays(1)); await this.RunCheckUpdate(); }); } private void Application_SessionEnding(object sender, SessionEndingCancelEventArgs e) diff --git a/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj b/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj index 43fb0d3..f32dcaa 100644 --- a/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj +++ b/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj @@ -96,9 +96,15 @@ DeleteRoomConfirmDialog.xaml + + PerRoomSettingsDialog.xaml + RoomCard.xaml + + SettingWithDefault.xaml + TaskbarIconControl.xaml @@ -145,10 +151,18 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/BililiveRecorder.WPF/Controls/AddRoomCard.xaml.cs b/BililiveRecorder.WPF/Controls/AddRoomCard.xaml.cs index b8e489b..947470e 100644 --- a/BililiveRecorder.WPF/Controls/AddRoomCard.xaml.cs +++ b/BililiveRecorder.WPF/Controls/AddRoomCard.xaml.cs @@ -14,26 +14,26 @@ namespace BililiveRecorder.WPF.Controls public AddRoomCard() { - InitializeComponent(); + this.InitializeComponent(); } private void AddRoom() { - AddRoomRequested?.Invoke(this, InputTextBox.Text); - InputTextBox.Text = string.Empty; - InputTextBox.Focus(); + AddRoomRequested?.Invoke(this, this.InputTextBox.Text); + this.InputTextBox.Text = string.Empty; + this.InputTextBox.Focus(); } private void Button_Click(object sender, RoutedEventArgs e) { - AddRoom(); + this.AddRoom(); } private void InputTextBox_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { - AddRoom(); + this.AddRoom(); } } } diff --git a/BililiveRecorder.WPF/Controls/AddRoomFailedDialog.xaml.cs b/BililiveRecorder.WPF/Controls/AddRoomFailedDialog.xaml.cs index bc9420b..95637e4 100644 --- a/BililiveRecorder.WPF/Controls/AddRoomFailedDialog.xaml.cs +++ b/BililiveRecorder.WPF/Controls/AddRoomFailedDialog.xaml.cs @@ -7,7 +7,7 @@ namespace BililiveRecorder.WPF.Controls { public AddRoomFailedDialog() { - InitializeComponent(); + this.InitializeComponent(); } } } diff --git a/BililiveRecorder.WPF/Controls/CloseWindowConfirmDialog.xaml.cs b/BililiveRecorder.WPF/Controls/CloseWindowConfirmDialog.xaml.cs index 13d2d26..fed15e9 100644 --- a/BililiveRecorder.WPF/Controls/CloseWindowConfirmDialog.xaml.cs +++ b/BililiveRecorder.WPF/Controls/CloseWindowConfirmDialog.xaml.cs @@ -7,7 +7,7 @@ namespace BililiveRecorder.WPF.Controls { public CloseWindowConfirmDialog() { - InitializeComponent(); + this.InitializeComponent(); } } } diff --git a/BililiveRecorder.WPF/Controls/DeleteRoomConfirmDialog.xaml.cs b/BililiveRecorder.WPF/Controls/DeleteRoomConfirmDialog.xaml.cs index f6eee6a..9ac7a67 100644 --- a/BililiveRecorder.WPF/Controls/DeleteRoomConfirmDialog.xaml.cs +++ b/BililiveRecorder.WPF/Controls/DeleteRoomConfirmDialog.xaml.cs @@ -7,7 +7,7 @@ namespace BililiveRecorder.WPF.Controls { public DeleteRoomConfirmDialog() { - InitializeComponent(); + this.InitializeComponent(); } } } diff --git a/BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml b/BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml new file mode 100644 index 0000000..21e5e9c --- /dev/null +++ b/BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml.cs b/BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml.cs new file mode 100644 index 0000000..300d014 --- /dev/null +++ b/BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace BililiveRecorder.WPF.Controls +{ + /// + /// Interaction logic for PerRoomSettingsDialog.xaml + /// + public partial class PerRoomSettingsDialog + { + public PerRoomSettingsDialog() + { + this.InitializeComponent(); + } + } +} diff --git a/BililiveRecorder.WPF/Controls/RoomCard.xaml b/BililiveRecorder.WPF/Controls/RoomCard.xaml index 29a1c40..9099d07 100644 --- a/BililiveRecorder.WPF/Controls/RoomCard.xaml +++ b/BililiveRecorder.WPF/Controls/RoomCard.xaml @@ -63,6 +63,12 @@ + + + + + + @@ -123,7 +129,7 @@ diff --git a/BililiveRecorder.WPF/Controls/RoomCard.xaml.cs b/BililiveRecorder.WPF/Controls/RoomCard.xaml.cs index 3a0dbf1..bb2dfc0 100644 --- a/BililiveRecorder.WPF/Controls/RoomCard.xaml.cs +++ b/BililiveRecorder.WPF/Controls/RoomCard.xaml.cs @@ -13,49 +13,56 @@ namespace BililiveRecorder.WPF.Controls { public RoomCard() { - InitializeComponent(); + this.InitializeComponent(); } public event EventHandler DeleteRequested; + public event EventHandler ShowSettingsRequested; + private void MenuItem_StartRecording_Click(object sender, RoutedEventArgs e) { - (DataContext as IRecordedRoom)?.StartRecord(); + (this.DataContext as IRecordedRoom)?.StartRecord(); } private void MenuItem_StopRecording_Click(object sender, RoutedEventArgs e) { - (DataContext as IRecordedRoom)?.StopRecord(); + (this.DataContext as IRecordedRoom)?.StopRecord(); } private void MenuItem_RefreshInfo_Click(object sender, RoutedEventArgs e) { - (DataContext as IRecordedRoom)?.RefreshRoomInfo(); + (this.DataContext as IRecordedRoom)?.RefreshRoomInfo(); } private void MenuItem_StartMonitor_Click(object sender, RoutedEventArgs e) { - (DataContext as IRecordedRoom)?.Start(); + (this.DataContext as IRecordedRoom)?.Start(); } private void MenuItem_StopMonitor_Click(object sender, RoutedEventArgs e) { - (DataContext as IRecordedRoom)?.Stop(); + (this.DataContext as IRecordedRoom)?.Stop(); } private void MenuItem_DeleteRoom_Click(object sender, RoutedEventArgs e) { - DeleteRequested?.Invoke(DataContext, EventArgs.Empty); + DeleteRequested?.Invoke(this.DataContext, EventArgs.Empty); + } + + private void MenuItem_ShowSettings_Click(object sender, RoutedEventArgs e) + { + ShowSettingsRequested?.Invoke(this.DataContext, EventArgs.Empty); } private void Button_Clip_Click(object sender, RoutedEventArgs e) { - (DataContext as IRecordedRoom)?.Clip(); + (this.DataContext as IRecordedRoom)?.Clip(); } private void MenuItem_OpenInBrowser_Click(object sender, RoutedEventArgs e) { - if (DataContext is IRecordedRoom r && r is not null) + if (this.DataContext is IRecordedRoom r && r is not null) { try { diff --git a/BililiveRecorder.WPF/Controls/SettingWithDefault.xaml b/BililiveRecorder.WPF/Controls/SettingWithDefault.xaml new file mode 100644 index 0000000..625baad --- /dev/null +++ b/BililiveRecorder.WPF/Controls/SettingWithDefault.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + diff --git a/BililiveRecorder.WPF/Controls/SettingWithDefault.xaml.cs b/BililiveRecorder.WPF/Controls/SettingWithDefault.xaml.cs new file mode 100644 index 0000000..c89c096 --- /dev/null +++ b/BililiveRecorder.WPF/Controls/SettingWithDefault.xaml.cs @@ -0,0 +1,49 @@ +using System.Windows; +using System.Windows.Markup; + +namespace BililiveRecorder.WPF.Controls +{ + /// + /// Interaction logic for SettingWithDefault.xaml + /// + [ContentProperty("InnerContent")] + public partial class SettingWithDefault + { + public SettingWithDefault() + { + this.InitializeComponent(); + } + + public static readonly DependencyProperty HeaderProperty + = DependencyProperty.Register("Header", + typeof(string), + typeof(SettingWithDefault), + new FrameworkPropertyMetadata(string.Empty)); + + public string Header + { + get => (string)this.GetValue(HeaderProperty); + set => this.SetValue(HeaderProperty, value); + } + + public static readonly DependencyProperty IsSettingNotUsingDefaultProperty + = DependencyProperty.Register("IsSettingNotUsingDefault", + typeof(bool), + typeof(SettingWithDefault), + new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + + public bool IsSettingNotUsingDefault + { + get => (bool)this.GetValue(IsSettingNotUsingDefaultProperty); + set => this.SetValue(IsSettingNotUsingDefaultProperty, value); + } + + public static readonly DependencyProperty InnerContentProperty = DependencyProperty.Register("InnerContent", typeof(object), typeof(SettingWithDefault)); + + public object InnerContent + { + get => this.GetValue(InnerContentProperty); + set => this.SetValue(InnerContentProperty, value); + } + } +} diff --git a/BililiveRecorder.WPF/Controls/TaskbarIconControl.xaml b/BililiveRecorder.WPF/Controls/TaskbarIconControl.xaml index 63fdd04..870a17b 100644 --- a/BililiveRecorder.WPF/Controls/TaskbarIconControl.xaml +++ b/BililiveRecorder.WPF/Controls/TaskbarIconControl.xaml @@ -35,7 +35,7 @@ - + diff --git a/BililiveRecorder.WPF/Controls/TaskbarIconControl.xaml.cs b/BililiveRecorder.WPF/Controls/TaskbarIconControl.xaml.cs index 83b7b5f..d4deabb 100644 --- a/BililiveRecorder.WPF/Controls/TaskbarIconControl.xaml.cs +++ b/BililiveRecorder.WPF/Controls/TaskbarIconControl.xaml.cs @@ -1,6 +1,5 @@ using System.Windows; using System.Windows.Controls; -using Hardcodet.Wpf.TaskbarNotification; namespace BililiveRecorder.WPF.Controls { @@ -11,14 +10,14 @@ namespace BililiveRecorder.WPF.Controls { public TaskbarIconControl() { - InitializeComponent(); + this.InitializeComponent(); // AddHandler(NewMainWindow.ShowBalloonTipEvent, (RoutedEventHandler)UserControl_ShowBalloonTip); if (Application.Current.MainWindow is NewMainWindow nmw) { nmw.ShowBalloonTipCallback = (title, msg, sym) => { - TaskbarIcon.ShowBalloonTip(title, msg, sym); + this.TaskbarIcon.ShowBalloonTip(title, msg, sym); }; } } diff --git a/BililiveRecorder.WPF/Controls/WorkDirectorySelectorDialog.xaml.cs b/BililiveRecorder.WPF/Controls/WorkDirectorySelectorDialog.xaml.cs index 315b6a9..3cdba84 100644 --- a/BililiveRecorder.WPF/Controls/WorkDirectorySelectorDialog.xaml.cs +++ b/BililiveRecorder.WPF/Controls/WorkDirectorySelectorDialog.xaml.cs @@ -13,14 +13,14 @@ namespace BililiveRecorder.WPF.Controls private string error = string.Empty; private string path = string.Empty; - public string Error { get => error; set => SetField(ref error, value); } + public string Error { get => this.error; set => this.SetField(ref this.error, value); } - public string Path { get => path; set => SetField(ref path, value); } + public string Path { get => this.path; set => this.SetField(ref this.path, value); } public WorkDirectorySelectorDialog() { - DataContext = this; - InitializeComponent(); + this.DataContext = this; + this.InitializeComponent(); } public event PropertyChangedEventHandler PropertyChanged; @@ -28,7 +28,7 @@ namespace BililiveRecorder.WPF.Controls protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = "") { if (EqualityComparer.Default.Equals(field, value)) { return false; } - field = value; OnPropertyChanged(propertyName); return true; + field = value; this.OnPropertyChanged(propertyName); return true; } private void Button_Click(object sender, System.Windows.RoutedEventArgs e) @@ -45,7 +45,7 @@ namespace BililiveRecorder.WPF.Controls }; if (fileDialog.ShowDialog() == CommonFileDialogResult.Ok) { - Path = fileDialog.FileName; + this.Path = fileDialog.FileName; } } } diff --git a/BililiveRecorder.WPF/Converters/BoolToValueConverter.cs b/BililiveRecorder.WPF/Converters/BoolToValueConverter.cs index acd4f2a..ae7c891 100644 --- a/BililiveRecorder.WPF/Converters/BoolToValueConverter.cs +++ b/BililiveRecorder.WPF/Converters/BoolToValueConverter.cs @@ -10,24 +10,24 @@ namespace BililiveRecorder.WPF.Converters public static readonly DependencyProperty TrueValueProperty = DependencyProperty.Register(nameof(TrueValue), typeof(object), typeof(BoolToValueConverter), new PropertyMetadata(null)); public static readonly DependencyProperty FalseValueProperty = DependencyProperty.Register(nameof(FalseValue), typeof(object), typeof(BoolToValueConverter), new PropertyMetadata(null)); - public object TrueValue { get => GetValue(TrueValueProperty); set => SetValue(TrueValueProperty, value); } - public object FalseValue { get => GetValue(FalseValueProperty); set => SetValue(FalseValueProperty, value); } + public object TrueValue { get => this.GetValue(TrueValueProperty); set => this.SetValue(TrueValueProperty, value); } + public object FalseValue { get => this.GetValue(FalseValueProperty); set => this.SetValue(FalseValueProperty, value); } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value == null) { - return FalseValue; + return this.FalseValue; } else { - return (bool)value ? TrueValue : FalseValue; + return (bool)value ? this.TrueValue : this.FalseValue; } } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - return value != null ? value.Equals(TrueValue) : false; + return value != null ? value.Equals(this.TrueValue) : false; } } } diff --git a/BililiveRecorder.WPF/Converters/MultiBoolToValueConverter.cs b/BililiveRecorder.WPF/Converters/MultiBoolToValueConverter.cs index 13857db..6f848aa 100644 --- a/BililiveRecorder.WPF/Converters/MultiBoolToValueConverter.cs +++ b/BililiveRecorder.WPF/Converters/MultiBoolToValueConverter.cs @@ -15,10 +15,10 @@ namespace BililiveRecorder.WPF.Converters { if ((value is bool boolean) && boolean == false) { - return FalseValue; + return this.FalseValue; } } - return TrueValue; + return this.TrueValue; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) diff --git a/BililiveRecorder.WPF/Converters/NullValueTemplateSelector.cs b/BililiveRecorder.WPF/Converters/NullValueTemplateSelector.cs index 62eb27e..eac1918 100644 --- a/BililiveRecorder.WPF/Converters/NullValueTemplateSelector.cs +++ b/BililiveRecorder.WPF/Converters/NullValueTemplateSelector.cs @@ -8,6 +8,6 @@ namespace BililiveRecorder.WPF.Converters public DataTemplate Normal { get; set; } public DataTemplate Null { get; set; } - public override DataTemplate SelectTemplate(object item, DependencyObject container) => item is null ? Null : Normal; + public override DataTemplate SelectTemplate(object item, DependencyObject container) => item is null ? this.Null : this.Normal; } } diff --git a/BililiveRecorder.WPF/Converters/RoomListInterceptConverter.cs b/BililiveRecorder.WPF/Converters/RoomListInterceptConverter.cs index b51d342..5a3dc14 100644 --- a/BililiveRecorder.WPF/Converters/RoomListInterceptConverter.cs +++ b/BililiveRecorder.WPF/Converters/RoomListInterceptConverter.cs @@ -4,7 +4,6 @@ using System.Collections.Specialized; using System.Globalization; using System.Windows.Data; using BililiveRecorder.Core; -using BililiveRecorder.Core.Config; namespace BililiveRecorder.WPF.Converters { @@ -27,7 +26,7 @@ namespace BililiveRecorder.WPF.Converters public RecorderWrapper(IRecorder recorder) : base(recorder) { this.recorder = recorder; - Add(null); + this.Add(null); recorder.CollectionChanged += (sender, e) => { @@ -35,19 +34,19 @@ namespace BililiveRecorder.WPF.Converters { case NotifyCollectionChangedAction.Add: if (e.NewItems.Count != 1) throw new NotImplementedException("Wrapper Add Item Count != 1"); - InsertItem(e.NewStartingIndex, e.NewItems[0] as IRecordedRoom); + this.InsertItem(e.NewStartingIndex, e.NewItems[0] as IRecordedRoom); break; case NotifyCollectionChangedAction.Remove: if (e.OldItems.Count != 1) throw new NotImplementedException("Wrapper Remove Item Count != 1"); - if (!Remove(e.OldItems[0] as IRecordedRoom)) throw new NotImplementedException("Wrapper Remove Item Sync Fail"); + if (!this.Remove(e.OldItems[0] as IRecordedRoom)) throw new NotImplementedException("Wrapper Remove Item Sync Fail"); break; case NotifyCollectionChangedAction.Replace: throw new NotImplementedException("Wrapper Replace Item"); case NotifyCollectionChangedAction.Move: throw new NotImplementedException("Wrapper Move Item"); case NotifyCollectionChangedAction.Reset: - ClearItems(); - Add(null); + this.ClearItems(); + this.Add(null); break; default: break; diff --git a/BililiveRecorder.WPF/MockData/MockRecordedRoom.cs b/BililiveRecorder.WPF/MockData/MockRecordedRoom.cs index 04bfc09..048297b 100644 --- a/BililiveRecorder.WPF/MockData/MockRecordedRoom.cs +++ b/BililiveRecorder.WPF/MockData/MockRecordedRoom.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using BililiveRecorder.Core; using BililiveRecorder.Core.Callback; +using BililiveRecorder.Core.Config.V2; using BililiveRecorder.FlvProcessor; #nullable enable @@ -14,14 +15,14 @@ namespace BililiveRecorder.WPF.MockData public MockRecordedRoom() { - RoomId = 123456789; - ShortRoomId = 1234; - StreamerName = "Mock主播名Mock主播名Mock主播名Mock主播名"; - IsMonitoring = false; - IsRecording = true; - IsStreaming = true; - DownloadSpeedPersentage = 100d; - DownloadSpeedMegaBitps = 2.45d; + this.RoomId = 123456789; + this.ShortRoomId = 1234; + this.StreamerName = "Mock主播名Mock主播名Mock主播名Mock主播名"; + this.IsMonitoring = false; + this.IsRecording = true; + this.IsStreaming = true; + this.DownloadSpeedPersentage = 100d; + this.DownloadSpeedMegaBitps = 2.45d; } public int ShortRoomId { get; set; } @@ -52,6 +53,8 @@ namespace BililiveRecorder.WPF.MockData public Guid Guid { get; } = Guid.NewGuid(); + public RoomConfig RoomConfig => new RoomConfig(); + public event PropertyChangedEventHandler? PropertyChanged; public event EventHandler? RecordEnded; @@ -70,32 +73,32 @@ namespace BililiveRecorder.WPF.MockData public bool Start() { - IsMonitoring = true; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMonitoring))); + this.IsMonitoring = true; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsMonitoring))); return true; } public void StartRecord() { - IsRecording = true; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRecording))); + this.IsRecording = true; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsRecording))); } public void Stop() { - IsMonitoring = false; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMonitoring))); + this.IsMonitoring = false; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsMonitoring))); } public void StopRecord() { - IsRecording = false; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRecording))); + this.IsRecording = false; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsRecording))); } protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!this.disposedValue) { if (disposing) { @@ -104,7 +107,7 @@ namespace BililiveRecorder.WPF.MockData // TODO: free unmanaged resources (unmanaged objects) and override finalizer // TODO: set large fields to null - disposedValue = true; + this.disposedValue = true; } } @@ -118,7 +121,7 @@ namespace BililiveRecorder.WPF.MockData public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + this.Dispose(disposing: true); GC.SuppressFinalize(this); } } diff --git a/BililiveRecorder.WPF/MockData/MockRecorder.cs b/BililiveRecorder.WPF/MockData/MockRecorder.cs index 63c5534..faa7a01 100644 --- a/BililiveRecorder.WPF/MockData/MockRecorder.cs +++ b/BililiveRecorder.WPF/MockData/MockRecorder.cs @@ -5,7 +5,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using BililiveRecorder.Core; -using BililiveRecorder.Core.Config; +using BililiveRecorder.Core.Config.V2; namespace BililiveRecorder.WPF.MockData { @@ -16,47 +16,47 @@ namespace BililiveRecorder.WPF.MockData public MockRecorder() { - Rooms.Add(new MockRecordedRoom + this.Rooms.Add(new MockRecordedRoom { IsMonitoring = false, IsRecording = false }); - Rooms.Add(new MockRecordedRoom + this.Rooms.Add(new MockRecordedRoom { IsMonitoring = true, IsRecording = false }); - Rooms.Add(new MockRecordedRoom + this.Rooms.Add(new MockRecordedRoom { DownloadSpeedPersentage = 100, DownloadSpeedMegaBitps = 12.45 }); - Rooms.Add(new MockRecordedRoom + this.Rooms.Add(new MockRecordedRoom { DownloadSpeedPersentage = 95, DownloadSpeedMegaBitps = 789.45 }); - Rooms.Add(new MockRecordedRoom + this.Rooms.Add(new MockRecordedRoom { DownloadSpeedPersentage = 90 }); - Rooms.Add(new MockRecordedRoom + this.Rooms.Add(new MockRecordedRoom { DownloadSpeedPersentage = 85 }); - Rooms.Add(new MockRecordedRoom + this.Rooms.Add(new MockRecordedRoom { DownloadSpeedPersentage = 80 }); - Rooms.Add(new MockRecordedRoom + this.Rooms.Add(new MockRecordedRoom { DownloadSpeedPersentage = 75 }); - Rooms.Add(new MockRecordedRoom + this.Rooms.Add(new MockRecordedRoom { DownloadSpeedPersentage = 70 }); - Rooms.Add(new MockRecordedRoom + this.Rooms.Add(new MockRecordedRoom { DownloadSpeedPersentage = 109 }); @@ -64,28 +64,28 @@ namespace BililiveRecorder.WPF.MockData private ObservableCollection Rooms { get; } = new ObservableCollection(); - public ConfigV1 Config { get; } = new ConfigV1(); + public ConfigV2 Config { get; } = new ConfigV2(); - public int Count => Rooms.Count; + public int Count => this.Rooms.Count; public bool IsReadOnly => true; - int ICollection.Count => Rooms.Count; + int ICollection.Count => this.Rooms.Count; bool ICollection.IsReadOnly => true; - public IRecordedRoom this[int index] => Rooms[index]; + public IRecordedRoom this[int index] => this.Rooms[index]; public event PropertyChangedEventHandler PropertyChanged { - add => (Rooms as INotifyPropertyChanged).PropertyChanged += value; - remove => (Rooms as INotifyPropertyChanged).PropertyChanged -= value; + add => (this.Rooms as INotifyPropertyChanged).PropertyChanged += value; + remove => (this.Rooms as INotifyPropertyChanged).PropertyChanged -= value; } public event NotifyCollectionChangedEventHandler CollectionChanged { - add => (Rooms as INotifyCollectionChanged).CollectionChanged += value; - remove => (Rooms as INotifyCollectionChanged).CollectionChanged -= value; + add => (this.Rooms as INotifyCollectionChanged).CollectionChanged += value; + remove => (this.Rooms as INotifyCollectionChanged).CollectionChanged -= value; } void ICollection.Add(IRecordedRoom item) => throw new NotSupportedException("Collection is readonly"); @@ -94,35 +94,35 @@ namespace BililiveRecorder.WPF.MockData bool ICollection.Remove(IRecordedRoom item) => throw new NotSupportedException("Collection is readonly"); - bool ICollection.Contains(IRecordedRoom item) => Rooms.Contains(item); + bool ICollection.Contains(IRecordedRoom item) => this.Rooms.Contains(item); - void ICollection.CopyTo(IRecordedRoom[] array, int arrayIndex) => Rooms.CopyTo(array, arrayIndex); + void ICollection.CopyTo(IRecordedRoom[] array, int arrayIndex) => this.Rooms.CopyTo(array, arrayIndex); - public IEnumerator GetEnumerator() => Rooms.GetEnumerator(); + public IEnumerator GetEnumerator() => this.Rooms.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => Rooms.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => this.Rooms.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => Rooms.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => this.Rooms.GetEnumerator(); public bool Initialize(string workdir) { - Config.WorkDirectory = workdir; + this.Config.Global.WorkDirectory = workdir; return true; } public void AddRoom(int roomid) { - AddRoom(roomid, false); + this.AddRoom(roomid, false); } public void AddRoom(int roomid, bool enabled) { - Rooms.Add(new MockRecordedRoom { RoomId = roomid, IsMonitoring = enabled }); + this.Rooms.Add(new MockRecordedRoom { RoomId = roomid, IsMonitoring = enabled }); } public void RemoveRoom(IRecordedRoom rr) { - Rooms.Remove(rr); + this.Rooms.Remove(rr); } public void SaveConfigToFile() @@ -132,17 +132,17 @@ namespace BililiveRecorder.WPF.MockData protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!this.disposedValue) { if (disposing) { // dispose managed state (managed objects) - Rooms.Clear(); + this.Rooms.Clear(); } // free unmanaged resources (unmanaged objects) and override finalizer // set large fields to null - disposedValue = true; + this.disposedValue = true; } } @@ -156,7 +156,7 @@ namespace BililiveRecorder.WPF.MockData public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + this.Dispose(disposing: true); GC.SuppressFinalize(this); } } diff --git a/BililiveRecorder.WPF/Models/Commands.cs b/BililiveRecorder.WPF/Models/Commands.cs index d5b265c..23bc27d 100644 --- a/BililiveRecorder.WPF/Models/Commands.cs +++ b/BililiveRecorder.WPF/Models/Commands.cs @@ -32,12 +32,12 @@ namespace BililiveRecorder.WPF.Models #region ICommand Members - public void Execute(object parameter) => ExecuteDelegate?.Invoke(parameter); + public void Execute(object parameter) => this.ExecuteDelegate?.Invoke(parameter); - public bool CanExecute(object parameter) => CanExecuteDelegate switch + public bool CanExecute(object parameter) => this.CanExecuteDelegate switch { null => true, - _ => CanExecuteDelegate(parameter), + _ => this.CanExecuteDelegate(parameter), }; public event EventHandler CanExecuteChanged diff --git a/BililiveRecorder.WPF/Models/LogModel.cs b/BililiveRecorder.WPF/Models/LogModel.cs index 19b4e62..297a695 100644 --- a/BililiveRecorder.WPF/Models/LogModel.cs +++ b/BililiveRecorder.WPF/Models/LogModel.cs @@ -16,37 +16,37 @@ namespace BililiveRecorder.WPF.Models public LogModel() : base(new[] { "" }) { - LogReceived += LogModel_LogReceived; + LogReceived += this.LogModel_LogReceived; } private void LogModel_LogReceived(object sender, string e) { - _ = Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.DataBind, (Action)AddLogToCollection, e); + _ = Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.DataBind, (Action)this.AddLogToCollection, e); } private void AddLogToCollection(string e) { - Add(e); - while (Count > MAX_LINE) + this.Add(e); + while (this.Count > MAX_LINE) { - RemoveItem(0); + this.RemoveItem(0); } } protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!this.disposedValue) { if (disposing) { // dispose managed state (managed objects) - LogReceived -= LogModel_LogReceived; - ClearItems(); + LogReceived -= this.LogModel_LogReceived; + this.ClearItems(); } // free unmanaged resources (unmanaged objects) and override finalizer // set large fields to null - disposedValue = true; + this.disposedValue = true; } } @@ -60,7 +60,7 @@ namespace BililiveRecorder.WPF.Models public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + this.Dispose(disposing: true); GC.SuppressFinalize(this); } } diff --git a/BililiveRecorder.WPF/Models/RootModel.cs b/BililiveRecorder.WPF/Models/RootModel.cs index 98696e4..01e7737 100644 --- a/BililiveRecorder.WPF/Models/RootModel.cs +++ b/BililiveRecorder.WPF/Models/RootModel.cs @@ -15,7 +15,7 @@ namespace BililiveRecorder.WPF.Models public LogModel Logs { get; } = new LogModel(); - public IRecorder Recorder { get => recorder; internal set => SetField(ref recorder, value); } + public IRecorder Recorder { get => this.recorder; internal set => this.SetField(ref this.recorder, value); } public RootModel() { @@ -26,23 +26,23 @@ namespace BililiveRecorder.WPF.Models protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = "") { if (EqualityComparer.Default.Equals(field, value)) { return false; } - field = value; OnPropertyChanged(propertyName); return true; + field = value; this.OnPropertyChanged(propertyName); return true; } protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!this.disposedValue) { if (disposing) { // dispose managed state (managed objects) - Recorder?.Dispose(); - Logs.Dispose(); + this.Recorder?.Dispose(); + this.Logs.Dispose(); } // free unmanaged resources (unmanaged objects) and override finalizer // set large fields to null - disposedValue = true; + this.disposedValue = true; } } @@ -56,7 +56,7 @@ namespace BililiveRecorder.WPF.Models public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + this.Dispose(disposing: true); GC.SuppressFinalize(this); } } diff --git a/BililiveRecorder.WPF/NewMainWindow.xaml.cs b/BililiveRecorder.WPF/NewMainWindow.xaml.cs index 6a1a164..7df4bf1 100644 --- a/BililiveRecorder.WPF/NewMainWindow.xaml.cs +++ b/BililiveRecorder.WPF/NewMainWindow.xaml.cs @@ -14,14 +14,14 @@ namespace BililiveRecorder.WPF { public NewMainWindow() { - InitializeComponent(); + this.InitializeComponent(); - Title = "B站录播姬 " + BuildInfo.Version + " " + BuildInfo.HeadShaShort; + this.Title = "B站录播姬 " + BuildInfo.Version + " " + BuildInfo.HeadShaShort; - SingleInstance.NotificationReceived += SingleInstance_NotificationReceived; + SingleInstance.NotificationReceived += this.SingleInstance_NotificationReceived; } - private void SingleInstance_NotificationReceived(object sender, EventArgs e) => SuperActivateAction(); + private void SingleInstance_NotificationReceived(object sender, EventArgs e) => this.SuperActivateAction(); public event EventHandler NativeBeforeWindowClose; @@ -29,20 +29,20 @@ namespace BililiveRecorder.WPF internal void CloseWithoutConfirmAction() { - CloseConfirmed = true; - Close(); + this.CloseConfirmed = true; + this.Close(); } internal void SuperActivateAction() { try { - Show(); - WindowState = WindowState.Normal; - Topmost = true; - Activate(); - Topmost = false; - Focus(); + this.Show(); + this.WindowState = WindowState.Normal; + this.Topmost = true; + this.Activate(); + this.Topmost = false; + this.Focus(); } catch (Exception) { } @@ -50,10 +50,10 @@ namespace BililiveRecorder.WPF private void Window_StateChanged(object sender, EventArgs e) { - if (WindowState == WindowState.Minimized) + if (this.WindowState == WindowState.Minimized) { - Hide(); - ShowBalloonTipCallback?.Invoke("B站录播姬", "录播姬已最小化到托盘,左键单击图标恢复界面", BalloonIcon.Info); + this.Hide(); + this.ShowBalloonTipCallback?.Invoke("B站录播姬", "录播姬已最小化到托盘,左键单击图标恢复界面", BalloonIcon.Info); } } @@ -67,28 +67,28 @@ namespace BililiveRecorder.WPF private async void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { - if (PromptCloseConfirm && !CloseConfirmed) + if (this.PromptCloseConfirm && !this.CloseConfirmed) { e.Cancel = true; - if (CloseWindowSemaphoreSlim.Wait(0)) + if (this.CloseWindowSemaphoreSlim.Wait(0)) { try { if (await new CloseWindowConfirmDialog().ShowAsync() == ContentDialogResult.Primary) { - CloseConfirmed = true; - CloseWindowSemaphoreSlim.Release(); - Close(); + this.CloseConfirmed = true; + this.CloseWindowSemaphoreSlim.Release(); + this.Close(); return; } } catch (Exception) { } - CloseWindowSemaphoreSlim.Release(); + this.CloseWindowSemaphoreSlim.Release(); } } else { - SingleInstance.NotificationReceived -= SingleInstance_NotificationReceived; + SingleInstance.NotificationReceived -= this.SingleInstance_NotificationReceived; NativeBeforeWindowClose?.Invoke(this, EventArgs.Empty); return; } diff --git a/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml b/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml index a616f6a..f4dbf63 100644 --- a/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml +++ b/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml @@ -5,78 +5,87 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:ui="http://schemas.modernwpf.com/2019" + xmlns:c="clr-namespace:BililiveRecorder.WPF.Controls" xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages" - xmlns:config="clr-namespace:BililiveRecorder.Core.Config;assembly=BililiveRecorder.Core" + xmlns:config="clr-namespace:BililiveRecorder.Core.Config.V2;assembly=BililiveRecorder.Core" mc:Ignorable="d" d:DesignHeight="1500" d:DesignWidth="500" - DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:RootPage},Path=DataContext.Recorder.Config}" + DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:RootPage},Path=DataContext.Recorder.Config.Global}" Title="SettingsPage"> - + - + - + - - - - + + + - + + - - + + 录制断开后等待多长时间再尝试开始录制 - - - - + + + + + 发出连接直播服务器的请求后等待多长时间 防止直播服务器长时间不返回数据导致卡住 - - - - + + + + + 弹幕服务器被断开后等待多长时间再尝试连接 监控开播的主要途径是通过弹幕服务器发送的信息 - - - - + + + + + 在一定时间没有收到直播服务器发送的数据后断开重连 用于防止因为玄学网络问题导致卡住 - - - - + + + + + 此项影响的时间间隔是定时请求HTTP接口的间隔, 主要目的是防止没有从弹幕服务器收到开播消息, 所以此项不需要设置太短。 时间间隔设置太短会被B站服务器屏蔽,导致无法录制。 - - + + + diff --git a/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml.cs b/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml.cs index 91adcfa..3a1b75a 100644 --- a/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml.cs +++ b/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml.cs @@ -11,7 +11,7 @@ namespace BililiveRecorder.WPF.Pages { public AdvancedSettingsPage() { - InitializeComponent(); + this.InitializeComponent(); } private void Crash_Click(object sender, RoutedEventArgs e) diff --git a/BililiveRecorder.WPF/Pages/AnnouncementPage.xaml.cs b/BililiveRecorder.WPF/Pages/AnnouncementPage.xaml.cs index 31c3897..d2b765d 100644 --- a/BililiveRecorder.WPF/Pages/AnnouncementPage.xaml.cs +++ b/BililiveRecorder.WPF/Pages/AnnouncementPage.xaml.cs @@ -28,20 +28,20 @@ namespace BililiveRecorder.WPF.Pages public AnnouncementPage() { - InitializeComponent(); - Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(async () => await LoadAnnouncementAsync(ignore_cache: false, show_error: false))); + this.InitializeComponent(); + this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(async () => await this.LoadAnnouncementAsync(ignore_cache: false, show_error: false))); } - private async void Button_Click(object sender, RoutedEventArgs e) => await LoadAnnouncementAsync(ignore_cache: true, show_error: Keyboard.Modifiers.HasFlag(ModifierKeys.Control)); + private async void Button_Click(object sender, RoutedEventArgs e) => await this.LoadAnnouncementAsync(ignore_cache: true, show_error: Keyboard.Modifiers.HasFlag(ModifierKeys.Control)); private async Task LoadAnnouncementAsync(bool ignore_cache, bool show_error) { MemoryStream data; bool success; - Container.Child = null; - Error.Visibility = Visibility.Collapsed; - Loading.Visibility = Visibility.Visible; + this.Container.Child = null; + this.Error.Visibility = Visibility.Collapsed; + this.Loading.Visibility = Visibility.Visible; if (AnnouncementCache is not null && !ignore_cache) { @@ -83,7 +83,7 @@ namespace BililiveRecorder.WPF.Pages using var reader = new XamlXmlReader(stream, System.Windows.Markup.XamlReader.GetWpfSchemaContext()); var obj = System.Windows.Markup.XamlReader.Load(reader); if (obj is UIElement elem) - Container.Child = elem; + this.Container.Child = elem; } catch (Exception ex) { @@ -93,16 +93,16 @@ namespace BililiveRecorder.WPF.Pages } } - Loading.Visibility = Visibility.Collapsed; + this.Loading.Visibility = Visibility.Collapsed; if (success) { - RefreshButton.ToolTip = "当前公告获取时间: " + AnnouncementCacheTime.ToString("F"); + this.RefreshButton.ToolTip = "当前公告获取时间: " + AnnouncementCacheTime.ToString("F"); AnnouncementCache = data; } else { - RefreshButton.ToolTip = null; - Error.Visibility = Visibility.Visible; + this.RefreshButton.ToolTip = null; + this.Error.Visibility = Visibility.Visible; } } @@ -135,7 +135,7 @@ namespace BililiveRecorder.WPF.Pages { MessageBox.Show(ex.ToString(), "加载发生错误"); } - await LoadAnnouncementAsync(ignore_cache: false, show_error: true); + await this.LoadAnnouncementAsync(ignore_cache: false, show_error: true); } } } diff --git a/BililiveRecorder.WPF/Pages/LogPage.xaml.cs b/BililiveRecorder.WPF/Pages/LogPage.xaml.cs index 2352432..d8fe150 100644 --- a/BililiveRecorder.WPF/Pages/LogPage.xaml.cs +++ b/BililiveRecorder.WPF/Pages/LogPage.xaml.cs @@ -12,8 +12,8 @@ namespace BililiveRecorder.WPF.Pages { public LogPage() { - InitializeComponent(); - VersionTextBlock.Text = BuildInfo.Version + " " + BuildInfo.HeadShaShort; + this.InitializeComponent(); + this.VersionTextBlock.Text = BuildInfo.Version + " " + BuildInfo.HeadShaShort; } private void TextBlock_MouseRightButtonUp(object sender, MouseButtonEventArgs e) diff --git a/BililiveRecorder.WPF/Pages/RoomListPage.xaml b/BililiveRecorder.WPF/Pages/RoomListPage.xaml index 9d61b1b..fac094a 100644 --- a/BililiveRecorder.WPF/Pages/RoomListPage.xaml +++ b/BililiveRecorder.WPF/Pages/RoomListPage.xaml @@ -7,7 +7,7 @@ xmlns:ui="http://schemas.modernwpf.com/2019" xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages" xmlns:mock="clr-namespace:BililiveRecorder.WPF.MockData" - xmlns:controls="clr-namespace:BililiveRecorder.WPF.Controls" + xmlns:c="clr-namespace:BililiveRecorder.WPF.Controls" xmlns:converters="clr-namespace:BililiveRecorder.WPF.Converters" mc:Ignorable="d" d:DesignHeight="1000" d:DesignWidth="960" @@ -17,12 +17,12 @@ - + - + ; + (this.SortedRoomList as SortedItemsSourceView).Data = e.NewValue as ICollection; } public static readonly DependencyProperty SortedRoomListProperty = @@ -49,8 +49,8 @@ namespace BililiveRecorder.WPF.Pages public object SortedRoomList { - get => GetValue(SortedRoomListProperty); - set => SetValue(SortedRoomListProperty, value); + get => this.GetValue(SortedRoomListProperty); + set => this.SetValue(SortedRoomListProperty, value); } private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) @@ -60,7 +60,7 @@ namespace BililiveRecorder.WPF.Pages private async void RoomCard_DeleteRequested(object sender, EventArgs e) { - if (DataContext is IRecorder rec && sender is IRecordedRoom room) + if (this.DataContext is IRecorder rec && sender is IRecordedRoom room) { var dialog = new DeleteRoomConfirmDialog { @@ -77,10 +77,19 @@ namespace BililiveRecorder.WPF.Pages } } + private async void RoomCard_ShowSettingsRequested(object sender, EventArgs e) + { + try + { + await new PerRoomSettingsDialog { DataContext = sender }.ShowAsync(); + } + catch (Exception) { } + } + private async void AddRoomCard_AddRoomRequested(object sender, string e) { var input = e.Trim(); - if (string.IsNullOrWhiteSpace(input) || DataContext is not IRecorder rec) return; + if (string.IsNullOrWhiteSpace(input) || this.DataContext is not IRecorder rec) return; if (!int.TryParse(input, out var roomid)) { @@ -119,7 +128,7 @@ namespace BililiveRecorder.WPF.Pages private async void MenuItem_EnableAutoRecAll_Click(object sender, RoutedEventArgs e) { - if (!(DataContext is IRecorder rec)) return; + if (!(this.DataContext is IRecorder rec)) return; await Task.WhenAll(rec.ToList().Select(rr => Task.Run(() => rr.Start()))); rec.SaveConfigToFile(); @@ -127,7 +136,7 @@ namespace BililiveRecorder.WPF.Pages private async void MenuItem_DisableAutoRecAll_Click(object sender, RoutedEventArgs e) { - if (!(DataContext is IRecorder rec)) return; + if (!(this.DataContext is IRecorder rec)) return; await Task.WhenAll(rec.ToList().Select(rr => Task.Run(() => rr.Stop()))); rec.SaveConfigToFile(); @@ -135,23 +144,23 @@ namespace BililiveRecorder.WPF.Pages private void MenuItem_SortBy_Click(object sender, RoutedEventArgs e) { - (SortedRoomList as SortedItemsSourceView).SortedBy = (SortedBy)((MenuItem)sender).Tag; + (this.SortedRoomList as SortedItemsSourceView).SortedBy = (SortedBy)((MenuItem)sender).Tag; } private void MenuItem_ShowLog_Click(object sender, RoutedEventArgs e) { - Splitter.Visibility = Visibility.Visible; - LogElement.Visibility = Visibility.Visible; - RoomListRowDefinition.Height = new GridLength(1, GridUnitType.Star); - LogRowDefinition.Height = new GridLength(1, GridUnitType.Star); + this.Splitter.Visibility = Visibility.Visible; + this.LogElement.Visibility = Visibility.Visible; + this.RoomListRowDefinition.Height = new GridLength(1, GridUnitType.Star); + this.LogRowDefinition.Height = new GridLength(1, GridUnitType.Star); } private void MenuItem_HideLog_Click(object sender, RoutedEventArgs e) { - Splitter.Visibility = Visibility.Collapsed; - LogElement.Visibility = Visibility.Collapsed; - RoomListRowDefinition.Height = new GridLength(1, GridUnitType.Star); - LogRowDefinition.Height = new GridLength(0); + this.Splitter.Visibility = Visibility.Collapsed; + this.LogElement.Visibility = Visibility.Collapsed; + this.RoomListRowDefinition.Height = new GridLength(1, GridUnitType.Star); + this.LogRowDefinition.Height = new GridLength(0); } private void Log_ScrollViewer_Loaded(object sender, RoutedEventArgs e) @@ -177,8 +186,8 @@ namespace BililiveRecorder.WPF.Pages { try { - if (DataContext is IRecorder rec) - Process.Start("explorer.exe", rec.Config.WorkDirectory); + if (this.DataContext is IRecorder rec) + Process.Start("explorer.exe", rec.Config.Global.WorkDirectory); } catch (Exception) { @@ -195,7 +204,7 @@ namespace BililiveRecorder.WPF.Pages internal class SortedItemsSourceView : IList, IReadOnlyList, IKeyIndexMapping, INotifyCollectionChanged { - private static Logger logger = LogManager.GetCurrentClassLogger(); + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private ICollection _data; private SortedBy sortedBy; @@ -210,73 +219,73 @@ namespace BililiveRecorder.WPF.Pages { if (data is IList list) { - if (list is INotifyCollectionChanged n) n.CollectionChanged += Data_CollectionChanged; - _data = list; + if (list is INotifyCollectionChanged n) n.CollectionChanged += this.Data_CollectionChanged; + this._data = list; } else { throw new ArgumentException("Type not supported.", nameof(data)); } } - Sort(); + this.Sort(); } - private void Data_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => Sort(); + private void Data_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => this.Sort(); public ICollection Data { - get => _data; + get => this._data; set { - if (_data is INotifyCollectionChanged n1) n1.CollectionChanged -= Data_CollectionChanged; - if (value is INotifyCollectionChanged n2) n2.CollectionChanged += Data_CollectionChanged; - _data = value; - Sort(); + if (this._data is INotifyCollectionChanged n1) n1.CollectionChanged -= this.Data_CollectionChanged; + if (value is INotifyCollectionChanged n2) n2.CollectionChanged += this.Data_CollectionChanged; + this._data = value; + this.Sort(); } } - public SortedBy SortedBy { get => sortedBy; set { sortedBy = value; Sort(); } } + public SortedBy SortedBy { get => this.sortedBy; set { this.sortedBy = value; this.Sort(); } } public List Sorted { get; private set; } private int sortSeboucneCount = int.MinValue; - private SemaphoreSlim sortSemaphoreSlim = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim sortSemaphoreSlim = new SemaphoreSlim(1, 1); private async void Sort() { // debounce && lock logger.Debug("Sort called."); - var callCount = Interlocked.Increment(ref sortSeboucneCount); + var callCount = Interlocked.Increment(ref this.sortSeboucneCount); await Task.Delay(200); - if (sortSeboucneCount != callCount) + if (this.sortSeboucneCount != callCount) { logger.Debug("Sort cancelled by debounce."); return; } - await sortSemaphoreSlim.WaitAsync(); - try { SortImpl(); } - finally { sortSemaphoreSlim.Release(); } + await this.sortSemaphoreSlim.WaitAsync(); + try { this.SortImpl(); } + finally { this.sortSemaphoreSlim.Release(); } } private void SortImpl() { - logger.Debug("SortImpl called with {sortedBy} and {count} rooms.", SortedBy, Data?.Count ?? -1); + logger.Debug("SortImpl called with {sortedBy} and {count} rooms.", this.SortedBy, this.Data?.Count ?? -1); - if (Data is null) + if (this.Data is null) { - Sorted = NullRoom.ToList(); + this.Sorted = this.NullRoom.ToList(); logger.Debug("SortImpl returned NullRoom."); } else { - IEnumerable orderedData = SortedBy switch + IEnumerable orderedData = this.SortedBy switch { - SortedBy.RoomId => Data.OrderBy(x => x.ShortRoomId == 0 ? x.RoomId : x.ShortRoomId), - SortedBy.Status => Data.OrderByDescending(x => x.IsRecording).ThenByDescending(x => x.IsMonitoring), - _ => Data, + SortedBy.RoomId => this.Data.OrderBy(x => x.ShortRoomId == 0 ? x.RoomId : x.ShortRoomId), + SortedBy.Status => this.Data.OrderByDescending(x => x.IsRecording).ThenByDescending(x => x.IsMonitoring), + _ => this.Data, }; - var result = orderedData.Concat(NullRoom).ToList(); + var result = orderedData.Concat(this.NullRoom).ToList(); logger.Debug("SortImpl sorted with {count} items.", result.Count); { // 崩溃问题信息收集。。虽然不觉得是这里的问题 @@ -310,7 +319,7 @@ namespace BililiveRecorder.WPF.Pages } } - Sorted = result; + this.Sorted = result; } // Instead of tossing out existing elements and re-creating them, @@ -319,21 +328,21 @@ namespace BililiveRecorder.WPF.Pages CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } - public IRecordedRoom this[int index] => Sorted != null ? Sorted[index] : throw new IndexOutOfRangeException(); - public int Count => Sorted != null ? Sorted.Count : 0; + public IRecordedRoom this[int index] => this.Sorted != null ? this.Sorted[index] : throw new IndexOutOfRangeException(); + public int Count => this.Sorted != null ? this.Sorted.Count : 0; - public bool IsReadOnly => ((IList)Sorted).IsReadOnly; + public bool IsReadOnly => ((IList)this.Sorted).IsReadOnly; - public bool IsFixedSize => ((IList)Sorted).IsFixedSize; + public bool IsFixedSize => ((IList)this.Sorted).IsFixedSize; - public object SyncRoot => ((ICollection)Sorted).SyncRoot; + public object SyncRoot => ((ICollection)this.Sorted).SyncRoot; - public bool IsSynchronized => ((ICollection)Sorted).IsSynchronized; + public bool IsSynchronized => ((ICollection)this.Sorted).IsSynchronized; - object IList.this[int index] { get => ((IList)Sorted)[index]; set => ((IList)Sorted)[index] = value; } + object IList.this[int index] { get => ((IList)this.Sorted)[index]; set => ((IList)this.Sorted)[index] = value; } - public IEnumerator GetEnumerator() => Sorted.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public IEnumerator GetEnumerator() => this.Sorted.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); #region IKeyIndexMapping @@ -353,15 +362,15 @@ namespace BililiveRecorder.WPF.Pages { // We'll try to increase our odds of finding a match sooner by starting from the // position that we know was last requested and search forward. - var start = lastRequestedIndex; - for (var i = start; i < Count; i++) + var start = this.lastRequestedIndex; + for (var i = start; i < this.Count; i++) { if ((this[i]?.Guid ?? Guid.Empty).Equals(uniqueId)) return i; } // Then try searching backward. - start = Math.Min(Count - 1, lastRequestedIndex); + start = Math.Min(this.Count - 1, this.lastRequestedIndex); for (var i = start; i >= 0; i--) { if ((this[i]?.Guid ?? Guid.Empty).Equals(uniqueId)) @@ -374,48 +383,48 @@ namespace BililiveRecorder.WPF.Pages public string KeyFromIndex(int index) { var key = this[index]?.Guid ?? Guid.Empty; - lastRequestedIndex = index; + this.lastRequestedIndex = index; return key.ToString(); } public int Add(object value) { - return ((IList)Sorted).Add(value); + return ((IList)this.Sorted).Add(value); } public bool Contains(object value) { - return ((IList)Sorted).Contains(value); + return ((IList)this.Sorted).Contains(value); } public void Clear() { - ((IList)Sorted).Clear(); + ((IList)this.Sorted).Clear(); } public int IndexOf(object value) { - return ((IList)Sorted).IndexOf(value); + return ((IList)this.Sorted).IndexOf(value); } public void Insert(int index, object value) { - ((IList)Sorted).Insert(index, value); + ((IList)this.Sorted).Insert(index, value); } public void Remove(object value) { - ((IList)Sorted).Remove(value); + ((IList)this.Sorted).Remove(value); } public void RemoveAt(int index) { - ((IList)Sorted).RemoveAt(index); + ((IList)this.Sorted).RemoveAt(index); } public void CopyTo(Array array, int index) { - ((ICollection)Sorted).CopyTo(array, index); + ((ICollection)this.Sorted).CopyTo(array, index); } #endregion diff --git a/BililiveRecorder.WPF/Pages/RootPage.xaml.cs b/BililiveRecorder.WPF/Pages/RootPage.xaml.cs index a4fa2d8..29e695e 100644 --- a/BililiveRecorder.WPF/Pages/RootPage.xaml.cs +++ b/BililiveRecorder.WPF/Pages/RootPage.xaml.cs @@ -40,38 +40,38 @@ namespace BililiveRecorder.WPF.Pages public RootPage() { - void AddType(Type t) => PageMap.Add(t.Name, t); + void AddType(Type t) => this.PageMap.Add(t.Name, t); AddType(typeof(RoomListPage)); AddType(typeof(LogPage)); AddType(typeof(SettingsPage)); AddType(typeof(AdvancedSettingsPage)); AddType(typeof(AnnouncementPage)); - Model = new RootModel(); - DataContext = Model; + this.Model = new RootModel(); + this.DataContext = this.Model; var builder = new ContainerBuilder(); builder.RegisterModule(); builder.RegisterModule(); - Container = builder.Build(); - RootScope = Container.BeginLifetimeScope("recorder_root"); + this.Container = builder.Build(); + this.RootScope = this.Container.BeginLifetimeScope("recorder_root"); - InitializeComponent(); - AdvancedSettingsPageItem.Visibility = Visibility.Hidden; + this.InitializeComponent(); + this.AdvancedSettingsPageItem.Visibility = Visibility.Hidden; - (Application.Current.MainWindow as NewMainWindow).NativeBeforeWindowClose += RootPage_NativeBeforeWindowClose; - Loaded += RootPage_Loaded; + (Application.Current.MainWindow as NewMainWindow).NativeBeforeWindowClose += this.RootPage_NativeBeforeWindowClose; + Loaded += this.RootPage_Loaded; } private void RootPage_NativeBeforeWindowClose(object sender, EventArgs e) { - Model.Dispose(); + this.Model.Dispose(); SingleInstance.Cleanup(); } private async void RootPage_Loaded(object sender, RoutedEventArgs e) { - var recorder = RootScope.Resolve(); + var recorder = this.RootScope.Resolve(); var first_time = true; var error = string.Empty; string path; @@ -113,8 +113,8 @@ namespace BililiveRecorder.WPF.Pages var lastdir = string.Empty; try { - if (File.Exists(lastdir_path)) - lastdir = File.ReadAllText(lastdir_path).Replace("\r", "").Replace("\n", "").Trim(); + if (File.Exists(this.lastdir_path)) + lastdir = File.ReadAllText(this.lastdir_path).Replace("\r", "").Replace("\n", "").Trim(); } catch (Exception) { } @@ -161,7 +161,7 @@ namespace BililiveRecorder.WPF.Pages // 如果不是从命令行参数传入的路径,写入 lastdir_path 记录 try - { if (string.IsNullOrWhiteSpace(commandLineOption?.WorkDirectory)) File.WriteAllText(lastdir_path, path); } + { if (string.IsNullOrWhiteSpace(commandLineOption?.WorkDirectory)) File.WriteAllText(this.lastdir_path, path); } catch (Exception) { } // 检查已经在同目录运行的其他进程 @@ -170,14 +170,14 @@ namespace BililiveRecorder.WPF.Pages // 无已经在同目录运行的进程 if (recorder.Initialize(path)) { - Model.Recorder = recorder; + this.Model.Recorder = recorder; _ = Task.Run(async () => { await Task.Delay(100); - _ = Dispatcher.BeginInvoke(DispatcherPriority.Normal, method: new Action(() => + _ = this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, method: new Action(() => { - RoomListPageNavigationViewItem.IsSelected = true; + this.RoomListPageNavigationViewItem.IsSelected = true; })); }); break; @@ -206,10 +206,10 @@ namespace BililiveRecorder.WPF.Pages private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) { - SettingsClickCount = 0; + this.SettingsClickCount = 0; if (args.IsSettingsSelected) { - MainFrame.Navigate(typeof(SettingsPage), null, transitionInfo); + this.MainFrame.Navigate(typeof(SettingsPage), null, this.transitionInfo); } else { @@ -219,26 +219,26 @@ namespace BililiveRecorder.WPF.Pages { try { - MainFrame.Navigate(new Uri(selectedItemTag), null, transitionInfo); + this.MainFrame.Navigate(new Uri(selectedItemTag), null, this.transitionInfo); } catch (Exception) { } } - else if (PageMap.ContainsKey(selectedItemTag)) + else if (this.PageMap.ContainsKey(selectedItemTag)) { - var pageType = PageMap[selectedItemTag]; - MainFrame.Navigate(pageType, null, transitionInfo); + var pageType = this.PageMap[selectedItemTag]; + this.MainFrame.Navigate(pageType, null, this.transitionInfo); } } } private void NavigationViewItem_MouseRightButtonUp(object sender, MouseButtonEventArgs e) { - if (++SettingsClickCount > 3) + if (++this.SettingsClickCount > 3) { - SettingsClickCount = 0; - AdvancedSettingsPageItem.Visibility = AdvancedSettingsPageItem.Visibility != Visibility.Visible ? Visibility.Visible : Visibility.Hidden; + this.SettingsClickCount = 0; + this.AdvancedSettingsPageItem.Visibility = this.AdvancedSettingsPageItem.Visibility != Visibility.Visible ? Visibility.Visible : Visibility.Hidden; } } diff --git a/BililiveRecorder.WPF/Pages/SettingsPage.xaml b/BililiveRecorder.WPF/Pages/SettingsPage.xaml index 2126496..27bee5a 100644 --- a/BililiveRecorder.WPF/Pages/SettingsPage.xaml +++ b/BililiveRecorder.WPF/Pages/SettingsPage.xaml @@ -5,12 +5,13 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:ui="http://schemas.modernwpf.com/2019" + xmlns:c="clr-namespace:BililiveRecorder.WPF.Controls" xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages" - xmlns:config="clr-namespace:BililiveRecorder.Core.Config;assembly=BililiveRecorder.Core" + xmlns:config="clr-namespace:BililiveRecorder.Core.Config.V2;assembly=BililiveRecorder.Core" xmlns:flv="clr-namespace:BililiveRecorder.FlvProcessor;assembly=BililiveRecorder.FlvProcessor" mc:Ignorable="d" d:DesignHeight="1500" d:DesignWidth="500" - DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:RootPage},Path=DataContext.Recorder.Config}" + DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:RootPage},Path=DataContext.Recorder.Config.Global}" Title="SettingsPage"> - + @@ -86,7 +87,7 @@ - + @@ -106,11 +107,13 @@ - - - - + + + + + + diff --git a/BililiveRecorder.WPF/Pages/SettingsPage.xaml.cs b/BililiveRecorder.WPF/Pages/SettingsPage.xaml.cs index 194f6d3..5ab7e5d 100644 --- a/BililiveRecorder.WPF/Pages/SettingsPage.xaml.cs +++ b/BililiveRecorder.WPF/Pages/SettingsPage.xaml.cs @@ -1,18 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; - namespace BililiveRecorder.WPF.Pages { /// @@ -22,7 +7,7 @@ namespace BililiveRecorder.WPF.Pages { public SettingsPage() { - InitializeComponent(); + this.InitializeComponent(); } } } diff --git a/BililiveRecorder.WPF/Properties/AssemblyInfo.cs b/BililiveRecorder.WPF/Properties/AssemblyInfo.cs index 703c96c..1f3b04d 100644 --- a/BililiveRecorder.WPF/Properties/AssemblyInfo.cs +++ b/BililiveRecorder.WPF/Properties/AssemblyInfo.cs @@ -1,6 +1,4 @@ using System.Reflection; -using System.Resources; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Windows; diff --git a/BililiveRecorder.sln b/BililiveRecorder.sln index dd46da1..1902d91 100644 --- a/BililiveRecorder.sln +++ b/BililiveRecorder.sln @@ -19,6 +19,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.UnitTest.Core", "test\BililiveRecorder.UnitTest.Core\BililiveRecorder.UnitTest.Core.csproj", "{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,10 +45,17 @@ Global {1B626335-283F-4313-9045-B5B96FAAB2DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {1B626335-283F-4313-9045-B5B96FAAB2DF}.Release|Any CPU.ActiveCfg = Release|Any CPU {1B626335-283F-4313-9045-B5B96FAAB2DF}.Release|Any CPU.Build.0 = Release|Any CPU + {521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {521EC763-5694-45A8-B87F-6E6B7F2A3BD4} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170} EndGlobalSection diff --git a/test/BililiveRecorder.UnitTest.Core/BililiveRecorder.UnitTest.Core.csproj b/test/BililiveRecorder.UnitTest.Core/BililiveRecorder.UnitTest.Core.csproj new file mode 100644 index 0000000..838dc17 --- /dev/null +++ b/test/BililiveRecorder.UnitTest.Core/BililiveRecorder.UnitTest.Core.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp3.1 + 9.0 + enable + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/test/BililiveRecorder.UnitTest.Core/ConfigTests.cs b/test/BililiveRecorder.UnitTest.Core/ConfigTests.cs new file mode 100644 index 0000000..28899ce --- /dev/null +++ b/test/BililiveRecorder.UnitTest.Core/ConfigTests.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BililiveRecorder.Core.Config; +using BililiveRecorder.Core.Config.V1; +using BililiveRecorder.Core.Config.V2; +using Newtonsoft.Json; +using Xunit; + +namespace BililiveRecorder.UnitTest.Core +{ + public class ConfigTests + { + private const string V2TestString1 = "{\"version\":2,\"global\":{\"EnabledFeature\":{\"HasValue\":false,\"Value\":0},\"ClipLengthPast\":{\"HasValue\":false,\"Value\":0},\"ClipLengthFuture\":{\"HasValue\":false,\"Value\":0},\"TimingStreamRetry\":{\"HasValue\":false,\"Value\":0},\"TimingStreamConnect\":{\"HasValue\":false,\"Value\":0},\"TimingDanmakuRetry\":{\"HasValue\":false,\"Value\":0},\"TimingCheckInterval\":{\"HasValue\":false,\"Value\":0},\"TimingWatchdogTimeout\":{\"HasValue\":false,\"Value\":0},\"RecordDanmakuFlushInterval\":{\"HasValue\":false,\"Value\":0},\"Cookie\":{\"HasValue\":false,\"Value\":null},\"WebHookUrls\":{\"HasValue\":false,\"Value\":null},\"LiveApiHost\":{\"HasValue\":false,\"Value\":null},\"RecordFilenameFormat\":{\"HasValue\":false,\"Value\":null},\"ClipFilenameFormat\":{\"HasValue\":false,\"Value\":null},\"CuttingMode\":{\"HasValue\":false,\"Value\":0},\"CuttingNumber\":{\"HasValue\":false,\"Value\":0},\"RecordDanmaku\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuRaw\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuSuperChat\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuGift\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuGuard\":{\"HasValue\":false,\"Value\":false}},\"rooms\":[{\"RoomId\":{\"HasValue\":true,\"Value\":1},\"AutoRecord\":{\"HasValue\":false,\"Value\":false},\"CuttingMode\":{\"HasValue\":false,\"Value\":0},\"CuttingNumber\":{\"HasValue\":false,\"Value\":0},\"RecordDanmaku\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuRaw\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuSuperChat\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuGift\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuGuard\":{\"HasValue\":false,\"Value\":false}},{\"RoomId\":{\"HasValue\":true,\"Value\":2},\"AutoRecord\":{\"HasValue\":false,\"Value\":false},\"CuttingMode\":{\"HasValue\":false,\"Value\":0},\"CuttingNumber\":{\"HasValue\":false,\"Value\":0},\"RecordDanmaku\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuRaw\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuSuperChat\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuGift\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuGuard\":{\"HasValue\":false,\"Value\":false}}]}"; + + private static readonly List V1 = new() + { + new object[] { "{\"version\":1}", 1 }, + new object[] { "{\"version\":1,\"data\":\"\"}", 1 }, + }; + + private static readonly List V2 = new() + { + new object[] { "{\"version\":2}", 2 }, + new object[] { "{\"version\":2,\"data\":{}}", 2 }, + new object[] { V2TestString1, 2 }, + }; + + public static IEnumerable GetTestData(int version) + => version switch + { + 0 => V1.Concat(V2).AsEnumerable(), + 1 => V1.AsEnumerable(), + 2 => V2.AsEnumerable(), + _ => throw new ArgumentException() + }; + + [Theory, MemberData(nameof(GetTestData), 0)] + public void Parse(string data, int ver) + { + var result = JsonConvert.DeserializeObject(data); + + var type = ver switch + { + 1 => typeof(ConfigV1Wrapper), + 2 => typeof(ConfigV2), + _ => throw new Exception("not supported") + }; + + Assert.Equal(type, result.GetType()); + } + + [Fact] + public void V2Test1() + { + var obj = JsonConvert.DeserializeObject(V2TestString1); + + var v2 = Assert.IsType(obj); + Assert.Equal(2, v2.Rooms.Count); + Assert.Equal(1, v2.Rooms[0].RoomId); + Assert.Equal(2, v2.Rooms[1].RoomId); + } + + [Fact] + public void Save() + { + ConfigBase config = new ConfigV2() + { + Rooms = new List + { + new RoomConfig { RoomId = 1 }, + new RoomConfig { RoomId = 2 } + }, + Global = new GlobalConfig() + }; + + var json = JsonConvert.SerializeObject(config); + } + } +}