diff --git a/.editorconfig b/.editorconfig index 00dc79c..dff06f8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,4 +6,151 @@ root = true # all files [*] indent_style = space -indent_size = 4 \ No newline at end of file +indent_size = 4 +charset = utf-8 +insert_final_newline = true + +[*.cs] + +#Core editorconfig formatting - indentation + +#use soft tabs (spaces) for indentation +indent_style = space + +#Formatting - indentation options + +#indent switch case contents. +csharp_indent_case_contents = true +#csharp_indent_case_contents_when_block +csharp_indent_case_contents_when_block = true +#indent switch labels +csharp_indent_switch_labels = true + +#Formatting - new line options + +#place catch statements on a new line +csharp_new_line_before_catch = true +#place else statements on a new line +csharp_new_line_before_else = true +#require finally statements to be on a new line after the closing brace +csharp_new_line_before_finally = true +#require members of anonymous types to be on separate lines +csharp_new_line_before_members_in_anonymous_types = true +#require members of object intializers to be on separate lines +csharp_new_line_before_members_in_object_initializers = true +#require braces to be on a new line for anonymous_types, lambdas, properties, accessors, methods, object_collection_array_initializers, control_blocks, and types (also known as "Allman" style) +csharp_new_line_before_open_brace = anonymous_types, lambdas, properties, accessors, methods, object_collection_array_initializers, control_blocks, types + +#Formatting - organize using options + +#place System.* using directives before other using directives +dotnet_sort_system_directives_first = true + +#Formatting - spacing options + +#require NO space between a cast and the value +csharp_space_after_cast = false +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_after_colon_in_inheritance_clause = true +#require a space after a keyword in a control flow statement such as a for loop +csharp_space_after_keywords_in_control_flow_statements = true +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_before_colon_in_inheritance_clause = true +#remove space within empty argument list parentheses +csharp_space_between_method_call_empty_parameter_list_parentheses = false +#remove space between method call name and opening parenthesis +csharp_space_between_method_call_name_and_opening_parenthesis = false +#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call +csharp_space_between_method_call_parameter_list_parentheses = false +#remove space within empty parameter list parentheses for a method declaration +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. +csharp_space_between_method_declaration_parameter_list_parentheses = false + +#Formatting - wrapping options + +#leave code block on single line +csharp_preserve_single_line_blocks = true +#leave statements and member declarations on the same line +csharp_preserve_single_line_statements = true + +#Style - Code block preferences + +#prefer curly braces even for one line of code +csharp_prefer_braces = true:suggestion + +#Style - expression bodied member options + +#prefer expression-bodied members for accessors +csharp_style_expression_bodied_accessors = true:suggestion +#prefer block bodies for constructors +csharp_style_expression_bodied_constructors = false:suggestion +#prefer expression-bodied members for indexers +csharp_style_expression_bodied_indexers = true:suggestion +#prefer block bodies for methods +csharp_style_expression_bodied_methods = false:suggestion +#prefer expression-bodied members for properties +csharp_style_expression_bodied_properties = true:suggestion + +#Style - expression level options + +#prefer out variables to be declared inline in the argument list of a method call when possible +csharp_style_inlined_variable_declaration = true:suggestion +#prefer tuple names to ItemX properties +dotnet_style_explicit_tuple_names = true:suggestion +#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_member_access = true:suggestion + +#Style - Expression-level preferences + +#prefer objects to be initialized using object initializers when possible +dotnet_style_object_initializer = true:suggestion +#prefer inferred anonymous type member names +dotnet_style_prefer_inferred_anonymous_type_member_names = false:suggestion +#prefer inferred tuple element names +dotnet_style_prefer_inferred_tuple_names = true:suggestion + +#Style - implicit and explicit types + +#prefer var over explicit type in all cases, unless overridden by another code style rule +csharp_style_var_elsewhere = true:suggestion +#prefer var is used to declare variables with built-in system types such as int +csharp_style_var_for_built_in_types = true:suggestion +#prefer var when the type is already mentioned on the right-hand side of a declaration expression +csharp_style_var_when_type_is_apparent = true:suggestion + +#Style - language keyword and framework type options + +#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion + +#Style - Miscellaneous preferences + +#prefer local functions over anonymous functions +csharp_style_pattern_local_over_anonymous_function = true:suggestion + +#Style - modifier options + +#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +#Style - Modifier preferences + +#when this rule is set to a list of modifiers, prefer the specified ordering. +csharp_preferred_modifier_order = public,private,internal,protected,static,readonly,async,virtual,unsafe,override:suggestion + +#Style - Pattern matching + +#prefer pattern matching instead of is expression with type casts +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + +#Style - qualification options + +#prefer events not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_event = false:suggestion +#prefer fields not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_field = false:suggestion +#prefer methods not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_method = false:suggestion +#prefer properties not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_property = false:suggestion diff --git a/BililiveRecorder.Core/BasicDanmakuWriter.cs b/BililiveRecorder.Core/BasicDanmakuWriter.cs index 2639c5a..28524b8 100644 --- a/BililiveRecorder.Core/BasicDanmakuWriter.cs +++ b/BililiveRecorder.Core/BasicDanmakuWriter.cs @@ -1,4 +1,4 @@ -using BililiveRecorder.Core.Config; +using BililiveRecorder.Core.Config; using System; using System.IO; using System.Text; @@ -84,8 +84,6 @@ namespace BililiveRecorder.Core { if (xmlWriter != null) { - // TimeSpan diff = DateTimeOffset.UtcNow - offset; - switch (danmakuModel.MsgType) { case MsgTypeEnum.Comment: @@ -93,13 +91,14 @@ namespace BililiveRecorder.Core var type = danmakuModel.RawObj?["info"]?[0]?[1]?.ToObject() ?? 1; var size = danmakuModel.RawObj?["info"]?[0]?[2]?.ToObject() ?? 25; var color = danmakuModel.RawObj?["info"]?[0]?[3]?.ToObject() ?? 0XFFFFFF; - long st = danmakuModel.RawObj?["info"]?[0]?[4]?.ToObject() ?? 0L; + var st = danmakuModel.RawObj?["info"]?[0]?[4]?.ToObject() ?? 0L; var ts = Math.Max((DateTimeOffset.FromUnixTimeMilliseconds(st) - offset).TotalSeconds, 0d); xmlWriter.WriteStartElement("d"); xmlWriter.WriteAttributeString("p", $"{ts},{type},{size},{color},{st},0,{danmakuModel.UserID},0"); xmlWriter.WriteAttributeString("user", danmakuModel.UserName); - xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["info"]?.ToString(Newtonsoft.Json.Formatting.None)); + if (config.RecordDanmakuRaw) + xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["info"]?.ToString(Newtonsoft.Json.Formatting.None)); xmlWriter.WriteValue(danmakuModel.CommentText); xmlWriter.WriteEndElement(); } @@ -108,10 +107,13 @@ namespace BililiveRecorder.Core if (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()); - xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); + if (config.RecordDanmakuRaw) + xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); xmlWriter.WriteValue(danmakuModel.CommentText); xmlWriter.WriteEndElement(); } @@ -120,10 +122,13 @@ namespace BililiveRecorder.Core if (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()); - xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); + if (config.RecordDanmakuRaw) + xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); xmlWriter.WriteEndElement(); } break; @@ -131,10 +136,13 @@ namespace BililiveRecorder.Core if (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()); - xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); + if (config.RecordDanmakuRaw) + xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); xmlWriter.WriteEndElement(); } break; diff --git a/BililiveRecorder.Core/BililiveAPI.cs b/BililiveRecorder.Core/BililiveAPI.cs index 658beb5..858566f 100644 --- a/BililiveRecorder.Core/BililiveAPI.cs +++ b/BililiveRecorder.Core/BililiveAPI.cs @@ -1,30 +1,40 @@ -using Newtonsoft.Json.Linq; -using NLog; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; -using System.Threading; using System.Threading.Tasks; +using BililiveRecorder.Core.Config; +using Newtonsoft.Json.Linq; +using NLog; namespace BililiveRecorder.Core { - internal static class BililiveAPI + public class BililiveAPI { private const string HTTP_HEADER_ACCEPT = "application/json, text/javascript, */*; q=0.01"; private const string HTTP_HEADER_REFERER = "https://live.bilibili.com/"; private const string DEFAULT_SERVER_HOST = "broadcastlv.chat.bilibili.com"; private const int DEFAULT_SERVER_PORT = 2243; + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private static readonly Random random = new Random(); - private static readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); - private static HttpClient httpclient; - private static HttpClient danmakuhttpclient; - internal static Config.ConfigV1 Config = null; // TODO: 以后有空把整个 class 改成非 static 的然后用 DI 获取 config - static BililiveAPI() + private readonly ConfigV1 Config; + private readonly HttpClient danmakuhttpclient; + private HttpClient httpclient; + + public BililiveAPI(ConfigV1 config) { + Config = config; + Config.PropertyChanged += (sender, e) => + { + if (e.PropertyName == nameof(Config.Cookie)) + { + ApplyCookieSettings(Config.Cookie); + } + }; + httpclient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; httpclient.DefaultRequestHeaders.Add("Accept", HTTP_HEADER_ACCEPT); httpclient.DefaultRequestHeaders.Add("Referer", HTTP_HEADER_REFERER); @@ -36,11 +46,11 @@ namespace BililiveRecorder.Core danmakuhttpclient.DefaultRequestHeaders.Add("User-Agent", Utils.UserAgent); } - public static async Task ApplyCookieSettings(string cookie_string) + public void ApplyCookieSettings(string cookie_string) { - await semaphoreSlim.WaitAsync(); try { + logger.Trace("设置 Cookie 信息..."); if (!string.IsNullOrWhiteSpace(cookie_string)) { var pclient = new HttpClient(handler: new HttpClientHandler @@ -65,15 +75,12 @@ namespace BililiveRecorder.Core cleanclient.DefaultRequestHeaders.Add("User-Agent", Utils.UserAgent); httpclient = cleanclient; } + logger.Debug("设置 Cookie 成功"); } catch (Exception ex) { logger.Error(ex, "设置 Cookie 时发生错误"); } - finally - { - semaphoreSlim.Release(); - } } /// @@ -83,9 +90,8 @@ namespace BililiveRecorder.Core /// 数据 /// /// - private static async Task HttpGetJsonAsync(HttpClient client, string url) + private async Task HttpGetJsonAsync(HttpClient client, string url) { - await semaphoreSlim.WaitAsync(); try { var s = await client.GetStringAsync(url); @@ -96,10 +102,6 @@ namespace BililiveRecorder.Core { return null; } - finally - { - semaphoreSlim.Release(); - } } /// @@ -109,49 +111,20 @@ namespace BililiveRecorder.Core /// FLV播放地址 /// /// - public static async Task GetPlayUrlAsync(int roomid) + public async Task GetPlayUrlAsync(int roomid) { - string url = $@"{Config.LiveApiHost}/room/v1/Room/playUrl?cid={roomid}&quality=4&platform=web"; - if (Config.AvoidTxy) + var url = $@"{Config.LiveApiHost}/room/v1/Room/playUrl?cid={roomid}&quality=4&platform=web"; + // 随机选择一个 url + if ((await HttpGetJsonAsync(httpclient, url))?["data"]?["durl"] is JArray array) { - // 尽量避开腾讯云 - int attempt_left = 2; - while (true) + var urls = array.Select(t => t?["url"]?.ToObject()); + var distinct = urls.Distinct().ToArray(); + if (distinct.Length > 0) { - attempt_left--; - if ((await HttpGetJsonAsync(httpclient, url))?["data"]?["durl"] is JArray all_jtoken && all_jtoken.Count > 0) - { - var all = all_jtoken.Select(x => x["url"].ToObject()).ToArray(); - var withoutTxy = all.Where(x => !x.Contains("txy.")).ToArray(); - if (withoutTxy.Length > 0) - { - return withoutTxy[random.Next(withoutTxy.Length)]; - } - else if (attempt_left <= 0) - { - return all[random.Next(all.Length)]; - } - } - else - { - throw new Exception("没有直播播放地址"); - } + return distinct[random.Next(distinct.Length)]; } } - else - { - // 随机选择一个 url - if ((await HttpGetJsonAsync(httpclient, url))?["data"]?["durl"] is JArray array) - { - var urls = array.Select(t => t?["url"]?.ToObject()); - var distinct = urls.Distinct().ToArray(); - if (distinct.Length > 0) - { - return distinct[random.Next(distinct.Length)]; - } - } - throw new Exception("没有直播播放地址"); - } + throw new Exception("没有直播播放地址"); } /// @@ -161,7 +134,7 @@ namespace BililiveRecorder.Core /// 直播间信息 /// /// - public static async Task GetRoomInfoAsync(int roomid) + public async Task GetRoomInfoAsync(int roomid) { try { @@ -201,7 +174,7 @@ namespace BililiveRecorder.Core /// /// /// - public static async Task<(string token, string host, int port)> GetDanmuConf(int roomid) + public async Task<(string token, string host, int port)> GetDanmuConf(int roomid) { try { diff --git a/BililiveRecorder.Core/BililiveRecorder.Core.csproj b/BililiveRecorder.Core/BililiveRecorder.Core.csproj index 9a516df..1ef5460 100644 --- a/BililiveRecorder.Core/BililiveRecorder.Core.csproj +++ b/BililiveRecorder.Core/BililiveRecorder.Core.csproj @@ -21,7 +21,7 @@ - + diff --git a/BililiveRecorder.Core/Config/ConfigV1.cs b/BililiveRecorder.Core/Config/ConfigV1.cs index 9c08412..45c5cdf 100644 --- a/BililiveRecorder.Core/Config/ConfigV1.cs +++ b/BililiveRecorder.Core/Config/ConfigV1.cs @@ -1,9 +1,9 @@ -using BililiveRecorder.FlvProcessor; -using Newtonsoft.Json; -using NLog; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; +using BililiveRecorder.FlvProcessor; +using Newtonsoft.Json; +using NLog; namespace BililiveRecorder.Core.Config { @@ -98,6 +98,12 @@ namespace BililiveRecorder.Core.Config [JsonProperty("record_danmaku")] public bool RecordDanmaku { get => _recordDanmaku; set => SetField(ref _recordDanmaku, value); } + /// + /// 是否记录弹幕原始数据 + /// + [JsonProperty("record_danmaku_raw")] + public bool RecordDanmakuRaw { get => _recordDanmakuRaw; set => SetField(ref _recordDanmakuRaw, value); } + /// /// 是否同时录制 SuperChat /// @@ -116,12 +122,6 @@ namespace BililiveRecorder.Core.Config [JsonProperty("record_danmaku_guard")] public bool RecordDanmakuGuard { get => _recordDanmakuGuard; set => SetField(ref _recordDanmakuGuard, value); } - /// - /// 尽量避开腾讯云服务器,可有效提升录制文件能正常播放的概率。(垃圾腾讯云直播服务) - /// - [JsonProperty("avoidtxy")] - public bool AvoidTxy { get => _avoidTxy; set => SetField(ref _avoidTxy, value); } - /// /// 替换api.live.bilibili.com服务器为其他反代,可以支持在云服务器上录制 /// @@ -153,12 +153,13 @@ namespace BililiveRecorder.Core.Config } #endregion + private string _workDirectory; + private uint _clipLengthPast = 20; private uint _clipLengthFuture = 10; private uint _cuttingNumber = 10; private EnabledFeature _enabledFeature = EnabledFeature.RecordOnly; private AutoCuttingMode _cuttingMode = AutoCuttingMode.Disabled; - private string _workDirectory; private uint _timingWatchdogTimeout = 10 * 1000; private uint _timingStreamRetry = 6 * 1000; @@ -172,12 +173,11 @@ namespace BililiveRecorder.Core.Config private string _clip_filename_format = @"{roomid}-{name}/剪辑片段-{roomid}-{date}-{time}-{title}.flv"; private bool _recordDanmaku = false; + private bool _recordDanmakuRaw = false; private bool _recordDanmakuSuperChat = false; private bool _recordDanmakuGift = false; private bool _recordDanmakuGuard = false; - private bool _avoidTxy = false; - private string _liveApiHost = "https://api.live.bilibili.com"; } } diff --git a/BililiveRecorder.Core/CoreModule.cs b/BililiveRecorder.Core/CoreModule.cs index 33e4152..3047495 100644 --- a/BililiveRecorder.Core/CoreModule.cs +++ b/BililiveRecorder.Core/CoreModule.cs @@ -1,6 +1,6 @@ -using Autofac; -using BililiveRecorder.Core.Config; using System.Net.Sockets; +using Autofac; +using BililiveRecorder.Core.Config; namespace BililiveRecorder.Core { @@ -14,6 +14,7 @@ namespace BililiveRecorder.Core protected override void Load(ContainerBuilder builder) { 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/IRecorder.cs b/BililiveRecorder.Core/IRecorder.cs index d0d84e7..1282c2b 100644 --- a/BililiveRecorder.Core/IRecorder.cs +++ b/BililiveRecorder.Core/IRecorder.cs @@ -1,11 +1,12 @@ -using BililiveRecorder.Core.Config; +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; +using BililiveRecorder.Core.Config; namespace BililiveRecorder.Core { - public interface IRecorder : INotifyPropertyChanged, INotifyCollectionChanged, IEnumerable, ICollection + public interface IRecorder : INotifyPropertyChanged, INotifyCollectionChanged, IEnumerable, ICollection, IDisposable { ConfigV1 Config { get; } @@ -19,6 +20,6 @@ namespace BililiveRecorder.Core void SaveConfigToFile(); - void Shutdown(); + // void Shutdown(); } } diff --git a/BililiveRecorder.Core/RecordedRoom.cs b/BililiveRecorder.Core/RecordedRoom.cs index 27e5e7c..bebb879 100644 --- a/BililiveRecorder.Core/RecordedRoom.cs +++ b/BililiveRecorder.Core/RecordedRoom.cs @@ -1,4 +1,4 @@ -using BililiveRecorder.Core.Config; +using BililiveRecorder.Core.Config; using BililiveRecorder.FlvProcessor; using NLog; using System; @@ -85,6 +85,7 @@ namespace BililiveRecorder.Core } private ConfigV1 _config { get; } + private BililiveAPI BililiveAPI { get; } public IStreamMonitor StreamMonitor { get; } private bool _retry = true; @@ -115,11 +116,13 @@ namespace BililiveRecorder.Core IBasicDanmakuWriter basicDanmakuWriter, Func newIStreamMonitor, Func newIFlvStreamProcessor, + BililiveAPI bililiveAPI, int roomid) { this.newIFlvStreamProcessor = newIFlvStreamProcessor; _config = config; + BililiveAPI = bililiveAPI; this.basicDanmakuWriter = basicDanmakuWriter; diff --git a/BililiveRecorder.Core/Recorder.cs b/BililiveRecorder.Core/Recorder.cs index eaaf663..530cd64 100644 --- a/BililiveRecorder.Core/Recorder.cs +++ b/BililiveRecorder.Core/Recorder.cs @@ -1,5 +1,3 @@ -using BililiveRecorder.Core.Config; -using NLog; using System; using System.Collections; using System.Collections.Generic; @@ -9,6 +7,8 @@ using System.ComponentModel; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BililiveRecorder.Core.Config; +using NLog; namespace BililiveRecorder.Core { @@ -16,24 +16,18 @@ namespace BililiveRecorder.Core { private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - private ObservableCollection Rooms { get; } = new ObservableCollection(); - public ConfigV1 Config { get; } - - ConfigV1 IRecorder.Config => Config; - - public int Count => Rooms.Count; - - public bool IsReadOnly => true; - - int ICollection.Count => Rooms.Count; - - bool ICollection.IsReadOnly => true; - private readonly Func newIRecordedRoom; - private CancellationTokenSource tokenSource; + private readonly CancellationTokenSource tokenSource; private bool _valid = false; + private bool disposedValue; + private ObservableCollection Rooms { get; } = new ObservableCollection(); + + public ConfigV1 Config { get; } + + public int Count => Rooms.Count; + public bool IsReadOnly => true; public IRecordedRoom this[int index] => Rooms[index]; public Recorder(ConfigV1 config, Func iRecordedRoom) @@ -41,8 +35,6 @@ namespace BililiveRecorder.Core newIRecordedRoom = iRecordedRoom; Config = config; - BililiveAPI.Config = config; - tokenSource = new CancellationTokenSource(); Repeat.Interval(TimeSpan.FromSeconds(3), DownloadWatchdog, tokenSource.Token); @@ -52,57 +44,8 @@ namespace BililiveRecorder.Core $"O:{e.OldItems?.Cast()?.Select(rr => rr.RoomId.ToString())?.Aggregate((current, next) => current + "," + next)};" + $"N:{e.NewItems?.Cast()?.Select(rr => rr.RoomId.ToString())?.Aggregate((current, next) => current + "," + next)}"); }; - - var debouncing = new SemaphoreSlim(1, 1); - Config.PropertyChanged += async (sender, e) => - { - if (e.PropertyName == nameof(Config.Cookie)) - { - if (await debouncing.WaitAsync(0)) - { - try - { - logger.Trace("设置 Cookie 等待..."); - await Task.Delay(100); - logger.Trace("设置 Cookie 信息..."); - await BililiveAPI.ApplyCookieSettings(Config.Cookie); - logger.Debug("设置 Cookie 成功"); - } - finally - { - debouncing.Release(); - } - } - } - }; } - public event PropertyChangedEventHandler PropertyChanged - { - add => (Rooms as INotifyPropertyChanged).PropertyChanged += value; - remove => (Rooms as INotifyPropertyChanged).PropertyChanged -= value; - } - - public event NotifyCollectionChangedEventHandler CollectionChanged - { - add => (Rooms as INotifyCollectionChanged).CollectionChanged += value; - remove => (Rooms as INotifyCollectionChanged).CollectionChanged -= value; - } - - event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged - { - add => (Rooms as INotifyPropertyChanged).PropertyChanged += value; - remove => (Rooms as INotifyPropertyChanged).PropertyChanged -= value; - } - - event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged - { - add => (Rooms as INotifyCollectionChanged).CollectionChanged += value; - remove => (Rooms as INotifyCollectionChanged).CollectionChanged -= value; - } - - bool IRecorder.Initialize(string workdir) => Initialize(workdir); - public bool Initialize(string workdir) { logger.Debug("Initialize: " + workdir); @@ -167,15 +110,16 @@ namespace BililiveRecorder.Core /// 直播间 public void RemoveRoom(IRecordedRoom rr) { + if (rr is null) return; if (!_valid) { throw new InvalidOperationException("Not Initialized"); } rr.Shutdown(); logger.Debug("RemoveRoom 移除了直播间 {roomid}", rr.RoomId); Rooms.Remove(rr); } - public void Shutdown() + private void Shutdown() { - if (!_valid) { throw new InvalidOperationException("Not Initialized"); } + if (!_valid) { return; } logger.Debug("Shutdown called."); tokenSource.Cancel(); @@ -185,6 +129,8 @@ namespace BililiveRecorder.Core { rr.Shutdown(); }); + + Rooms.Clear(); } public void SaveConfigToFile() @@ -227,19 +173,54 @@ 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(); + public event PropertyChangedEventHandler PropertyChanged + { + add => (Rooms as INotifyPropertyChanged).PropertyChanged += value; + remove => (Rooms as INotifyPropertyChanged).PropertyChanged -= value; + } + + public event NotifyCollectionChangedEventHandler CollectionChanged + { + add => (Rooms as INotifyCollectionChanged).CollectionChanged += value; + remove => (Rooms as INotifyCollectionChanged).CollectionChanged -= value; + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + Shutdown(); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // set large fields to null + disposedValue = true; + } + } + + // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~Recorder() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/BililiveRecorder.Core/StreamMonitor.cs b/BililiveRecorder.Core/StreamMonitor.cs index 4e231f3..5cd7bfa 100644 --- a/BililiveRecorder.Core/StreamMonitor.cs +++ b/BililiveRecorder.Core/StreamMonitor.cs @@ -1,15 +1,14 @@ -using BililiveRecorder.Core.Config; -using Newtonsoft.Json; -using NLog; using System; using System.IO; using System.IO.Compression; -using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; +using BililiveRecorder.Core.Config; +using Newtonsoft.Json; +using NLog; using Timer = System.Timers.Timer; namespace BililiveRecorder.Core @@ -32,6 +31,7 @@ namespace BililiveRecorder.Core private readonly Func funcTcpClient; private readonly ConfigV1 config; + private readonly BililiveAPI bililiveAPI; #pragma warning disable IDE1006 // 命名样式 private bool dmTcpConnected => dmClient?.Connected ?? false; @@ -40,7 +40,7 @@ namespace BililiveRecorder.Core private TcpClient dmClient; private NetworkStream dmNetStream; private Thread dmReceiveMessageLoopThread; - private CancellationTokenSource dmTokenSource = null; + private readonly CancellationTokenSource dmTokenSource = null; private bool dmConnectionTriggered = false; private readonly Timer httpTimer; @@ -50,10 +50,11 @@ namespace BililiveRecorder.Core public event StreamStartedEvent StreamStarted; public event ReceivedDanmakuEvt ReceivedDanmaku; - public StreamMonitor(int roomid, Func funcTcpClient, ConfigV1 config) + public StreamMonitor(int roomid, Func funcTcpClient, ConfigV1 config, BililiveAPI bililiveAPI) { this.funcTcpClient = funcTcpClient; this.config = config; + this.bililiveAPI = bililiveAPI; Roomid = roomid; @@ -178,7 +179,7 @@ namespace BililiveRecorder.Core public async Task FetchRoomInfoAsync() { - RoomInfo roomInfo = await BililiveAPI.GetRoomInfoAsync(Roomid).ConfigureAwait(false); + RoomInfo roomInfo = await bililiveAPI.GetRoomInfoAsync(Roomid).ConfigureAwait(false); RoomInfoUpdated?.Invoke(this, new RoomInfoUpdatedArgs { RoomInfo = roomInfo }); return roomInfo; } @@ -208,7 +209,7 @@ namespace BililiveRecorder.Core try { - var (token, host, port) = await BililiveAPI.GetDanmuConf(Roomid); + var (token, host, port) = await bililiveAPI.GetDanmuConf(Roomid); logger.Log(Roomid, LogLevel.Debug, $"连接弹幕服务器 {host}:{port} {(string.IsNullOrWhiteSpace(token) ? "无" : "有")} token"); diff --git a/BililiveRecorder.FlvProcessor/BililiveRecorder.FlvProcessor.csproj b/BililiveRecorder.FlvProcessor/BililiveRecorder.FlvProcessor.csproj index 9d94af4..3638434 100644 --- a/BililiveRecorder.FlvProcessor/BililiveRecorder.FlvProcessor.csproj +++ b/BililiveRecorder.FlvProcessor/BililiveRecorder.FlvProcessor.csproj @@ -20,7 +20,7 @@ - + diff --git a/BililiveRecorder.WPF/App.xaml b/BililiveRecorder.WPF/App.xaml index 8f3366a..0b0ef8a 100644 --- a/BililiveRecorder.WPF/App.xaml +++ b/BililiveRecorder.WPF/App.xaml @@ -1,6 +1,19 @@ - + StartupUri="NewMainWindow.xaml"> + + + + + + + + + + + diff --git a/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj b/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj index bee2cca..7fe0b1f 100644 --- a/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj +++ b/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj @@ -13,6 +13,8 @@ {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 4 true + + AnyCPU @@ -23,6 +25,7 @@ DEBUG;TRACE prompt 4 + false - @@ -50,65 +53,12 @@ Properties\app.manifest - - ..\packages\Autofac.4.8.1\lib\net45\Autofac.dll - - - ..\packages\CommandLineParser.2.4.3\lib\netstandard2.0\CommandLine.dll - - - ..\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.dll - - - ..\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.MsDelta.dll - - - ..\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.PatchApi.dll - - - ..\packages\Hardcodet.NotifyIcon.Wpf.1.0.8\lib\net451\Hardcodet.Wpf.TaskbarNotification.dll - - - ..\packages\WindowsAPICodePack-Core.1.1.2\lib\Microsoft.WindowsAPICodePack.dll - - - ..\packages\WindowsAPICodePack-Shell.1.1.1\lib\Microsoft.WindowsAPICodePack.Shell.dll - - - ..\packages\Mono.Cecil.0.9.6.1\lib\net45\Mono.Cecil.dll - - - ..\packages\Mono.Cecil.0.9.6.1\lib\net45\Mono.Cecil.Mdb.dll - - - ..\packages\Mono.Cecil.0.9.6.1\lib\net45\Mono.Cecil.Pdb.dll - - - ..\packages\Mono.Cecil.0.9.6.1\lib\net45\Mono.Cecil.Rocks.dll - - - ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll - - - ..\packages\NLog.4.5.10\lib\net45\NLog.dll - - - ..\packages\squirrel.windows.1.9.0\lib\Net45\NuGet.Squirrel.dll - - - ..\packages\SharpCompress.0.17.1\lib\net45\SharpCompress.dll - - - ..\packages\Splat.1.6.2\lib\Net45\Splat.dll - - - ..\packages\squirrel.windows.1.9.0\lib\Net45\Squirrel.dll - + @@ -132,29 +82,95 @@ Designer - + + AddRoomCard.xaml + + + AddRoomFailedDialog.xaml + + + CloseWindowConfirmDialog.xaml + + + DeleteRoomConfirmDialog.xaml + + + RoomCard.xaml + + + WorkDirectorySelectorDialog.xaml + + + + + + + + + RootPage.xaml + + + + + + + + + ClickSelectTextBox.xaml - - + TimedMessageBox.xaml - - + + SettingsWindow.xaml - + UpdateBarUserControl.xaml - + WorkDirectoryWindow.xaml - + + Designer MSBuild:Compile - + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + MSBuild:Compile Designer @@ -162,28 +178,65 @@ App.xaml Code - + MainWindow.xaml Code - + Designer MSBuild:Compile - + Designer MSBuild:Compile - + Designer MSBuild:Compile - + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + Designer MSBuild:Compile + + + + NewMainWindow.xaml + + + LogPage.xaml + + + RoomListPage.xaml + + + AdvancedSettingsPage.xaml + + + SettingsPage.xaml + Code @@ -203,10 +256,6 @@ - - Designer - - @@ -221,6 +270,41 @@ BililiveRecorder.FlvProcessor + + + 4.9.4 + + + 2.4.3 + + + 1.0.8 + + + 0.9.2 + + + 11.0.2 + + + 4.5.10 + + + 4.7.1 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + 2.0.1 + + + 1.1.2 + + + 1.1.1 + + + cd $(SolutionDir) diff --git a/BililiveRecorder.WPF/Controls/AddRoomCard.xaml b/BililiveRecorder.WPF/Controls/AddRoomCard.xaml new file mode 100644 index 0000000..919a8df --- /dev/null +++ b/BililiveRecorder.WPF/Controls/AddRoomCard.xaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/BililiveRecorder.WPF/Controls/RoomCard.xaml.cs b/BililiveRecorder.WPF/Controls/RoomCard.xaml.cs new file mode 100644 index 0000000..bf16eda --- /dev/null +++ b/BililiveRecorder.WPF/Controls/RoomCard.xaml.cs @@ -0,0 +1,55 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using BililiveRecorder.Core; + +namespace BililiveRecorder.WPF.Controls +{ + /// + /// Interaction logic for RoomCard.xaml + /// + public partial class RoomCard : UserControl + { + public RoomCard() + { + InitializeComponent(); + } + + public event EventHandler DeleteRequested; + + private void MenuItem_StartRecording_Click(object sender, RoutedEventArgs e) + { + (DataContext as IRecordedRoom)?.StartRecord(); + } + + private void MenuItem_StopRecording_Click(object sender, RoutedEventArgs e) + { + (DataContext as IRecordedRoom)?.StopRecord(); + } + + private void MenuItem_RefreshInfo_Click(object sender, RoutedEventArgs e) + { + (DataContext as IRecordedRoom)?.RefreshRoomInfo(); + } + + private void MenuItem_StartMonitor_Click(object sender, RoutedEventArgs e) + { + (DataContext as IRecordedRoom)?.Start(); + } + + private void MenuItem_StopMonitor_Click(object sender, RoutedEventArgs e) + { + (DataContext as IRecordedRoom)?.Stop(); + } + + private void MenuItem_DeleteRoom_Click(object sender, RoutedEventArgs e) + { + DeleteRequested?.Invoke(DataContext, EventArgs.Empty); + } + + private void Button_Clip_Click(object sender, RoutedEventArgs e) + { + (DataContext as IRecordedRoom)?.Clip(); + } + } +} diff --git a/BililiveRecorder.WPF/Controls/WorkDirectorySelectorDialog.xaml b/BililiveRecorder.WPF/Controls/WorkDirectorySelectorDialog.xaml new file mode 100644 index 0000000..f30911b --- /dev/null +++ b/BililiveRecorder.WPF/Controls/WorkDirectorySelectorDialog.xaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/BililiveRecorder.WPF/Controls/WorkDirectorySelectorDialog.xaml.cs b/BililiveRecorder.WPF/Controls/WorkDirectorySelectorDialog.xaml.cs new file mode 100644 index 0000000..315b6a9 --- /dev/null +++ b/BililiveRecorder.WPF/Controls/WorkDirectorySelectorDialog.xaml.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Microsoft.WindowsAPICodePack.Dialogs; + +namespace BililiveRecorder.WPF.Controls +{ + /// + /// Interaction logic for WorkDirectorySelectorDialog.xaml + /// + public partial class WorkDirectorySelectorDialog : INotifyPropertyChanged + { + private string error = string.Empty; + private string path = string.Empty; + + public string Error { get => error; set => SetField(ref error, value); } + + public string Path { get => path; set => SetField(ref path, value); } + + public WorkDirectorySelectorDialog() + { + DataContext = this; + InitializeComponent(); + } + + public event PropertyChangedEventHandler PropertyChanged; + 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; } + field = value; OnPropertyChanged(propertyName); return true; + } + + private void Button_Click(object sender, System.Windows.RoutedEventArgs e) + { + var fileDialog = new CommonOpenFileDialog() + { + IsFolderPicker = true, + Multiselect = false, + Title = "选择录播姬工作目录路径", + AddToMostRecentlyUsedList = false, + EnsurePathExists = true, + NavigateToShortcut = true, + InitialDirectory = Path, + }; + if (fileDialog.ShowDialog() == CommonFileDialogResult.Ok) + { + Path = fileDialog.FileName; + } + } + } +} diff --git a/BililiveRecorder.WPF/Converters/BoolToValueConverter.cs b/BililiveRecorder.WPF/Converters/BoolToValueConverter.cs new file mode 100644 index 0000000..18ea61b --- /dev/null +++ b/BililiveRecorder.WPF/Converters/BoolToValueConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace BililiveRecorder.WPF.Converters +{ + internal class BoolToValueConverter : IValueConverter + { + public object FalseValue { get; set; } + public object TrueValue { get; set; } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + { + return FalseValue; + } + else + { + return (bool)value ? TrueValue : FalseValue; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value != null ? value.Equals(TrueValue) : false; + } + } + +} diff --git a/BililiveRecorder.WPF/Converters/BooleanAndConverter.cs b/BililiveRecorder.WPF/Converters/BooleanAndConverter.cs new file mode 100644 index 0000000..fc77880 --- /dev/null +++ b/BililiveRecorder.WPF/Converters/BooleanAndConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace BililiveRecorder.WPF.Converters +{ + internal class BooleanAndConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + foreach (var value in values) + { + if ((value is bool boolean) && boolean == false) + { + return false; + } + } + return true; + } + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + +} diff --git a/BililiveRecorder.WPF/Converters/BooleanInverterConverter.cs b/BililiveRecorder.WPF/Converters/BooleanInverterConverter.cs new file mode 100644 index 0000000..9818213 --- /dev/null +++ b/BililiveRecorder.WPF/Converters/BooleanInverterConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace BililiveRecorder.WPF.Converters +{ + internal class BooleanInverterConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return !(bool)value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return !(bool)value; + } + } + +} diff --git a/BililiveRecorder.WPF/Converters/ClipEnabledToBooleanConverter.cs b/BililiveRecorder.WPF/Converters/ClipEnabledToBooleanConverter.cs new file mode 100644 index 0000000..d68f7a7 --- /dev/null +++ b/BililiveRecorder.WPF/Converters/ClipEnabledToBooleanConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using BililiveRecorder.FlvProcessor; + +namespace BililiveRecorder.WPF.Converters +{ + internal class ClipEnabledToBooleanConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return !(value is EnabledFeature v) || (EnabledFeature.RecordOnly != v); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/BililiveRecorder.WPF/Converters/EnumToBooleanConverter.cs b/BililiveRecorder.WPF/Converters/EnumToBooleanConverter.cs new file mode 100644 index 0000000..9d2cd0c --- /dev/null +++ b/BililiveRecorder.WPF/Converters/EnumToBooleanConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace BililiveRecorder.WPF.Converters +{ + internal class EnumToBooleanConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value.Equals(parameter); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value.Equals(true) ? parameter : Binding.DoNothing; + } + } +} diff --git a/BililiveRecorder.WPF/Converters/MultiBoolToValueConverter.cs b/BililiveRecorder.WPF/Converters/MultiBoolToValueConverter.cs new file mode 100644 index 0000000..13857db --- /dev/null +++ b/BililiveRecorder.WPF/Converters/MultiBoolToValueConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace BililiveRecorder.WPF.Converters +{ + public class MultiBoolToValueConverter : IMultiValueConverter + { + public object FalseValue { get; set; } + public object TrueValue { get; set; } + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + foreach (var value in values) + { + if ((value is bool boolean) && boolean == false) + { + return FalseValue; + } + } + return TrueValue; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/BililiveRecorder.WPF/Converters/NullValueTemplateSelector.cs b/BililiveRecorder.WPF/Converters/NullValueTemplateSelector.cs new file mode 100644 index 0000000..62eb27e --- /dev/null +++ b/BililiveRecorder.WPF/Converters/NullValueTemplateSelector.cs @@ -0,0 +1,13 @@ +using System.Windows; +using System.Windows.Controls; + +namespace BililiveRecorder.WPF.Converters +{ + internal class NullValueTemplateSelector : DataTemplateSelector + { + public DataTemplate Normal { get; set; } + public DataTemplate Null { get; set; } + + public override DataTemplate SelectTemplate(object item, DependencyObject container) => item is null ? Null : Normal; + } +} diff --git a/BililiveRecorder.WPF/Converters/PercentageToColorBrushConverter.cs b/BililiveRecorder.WPF/Converters/PercentageToColorBrushConverter.cs new file mode 100644 index 0000000..ced2c7b --- /dev/null +++ b/BililiveRecorder.WPF/Converters/PercentageToColorBrushConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; + +namespace BililiveRecorder.WPF.Converters +{ + internal class PercentageToColorBrushConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + const double a = 1d; + const double b = 6d; + const double c = 100d; + const double d = 2.2d; + + var x = (double)value * 100d; + var y = x < (c - a) ? c - Math.Pow(Math.Abs(x - c + a), d) / b : x > (c + a) ? c - Math.Pow(x - c - a, d) / b : c; + return new SolidColorBrush(GradientPick(Math.Max(y, 0d) / 100d, Colors.Red, Colors.Yellow, Colors.Lime)); + Color GradientPick(double percentage, Color c1, Color c2, Color c3) => percentage < 0.5 ? ColorInterp(c1, c2, percentage / 0.5) : percentage == 0.5 ? c2 : ColorInterp(c2, c3, (percentage - 0.5) / 0.5); + Color ColorInterp(Color start, Color end, double percentage) => Color.FromRgb(LinearInterp(start.R, end.R, percentage), LinearInterp(start.G, end.G, percentage), LinearInterp(start.B, end.B, percentage)); + byte LinearInterp(byte start, byte end, double percentage) => (byte)(start + Math.Round(percentage * (end - start))); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/BililiveRecorder.WPF/Converters/RoomListInterceptConverter.cs b/BililiveRecorder.WPF/Converters/RoomListInterceptConverter.cs new file mode 100644 index 0000000..b51d342 --- /dev/null +++ b/BililiveRecorder.WPF/Converters/RoomListInterceptConverter.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Globalization; +using System.Windows.Data; +using BililiveRecorder.Core; +using BililiveRecorder.Core.Config; + +namespace BililiveRecorder.WPF.Converters +{ + internal class RoomListInterceptConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is RecorderWrapper ? value : value is IRecorder recorder ? new RecorderWrapper(recorder) : value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value; + } + + private class RecorderWrapper : ObservableCollection + { + private readonly IRecorder recorder; + + public RecorderWrapper(IRecorder recorder) : base(recorder) + { + this.recorder = recorder; + Add(null); + + recorder.CollectionChanged += (sender, e) => + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.NewItems.Count != 1) throw new NotImplementedException("Wrapper Add Item Count != 1"); + 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"); + 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); + break; + default: + break; + } + }; + } + } + } +} diff --git a/BililiveRecorder.WPF/Converters/ShortRoomIdToVisibilityConverter.cs b/BililiveRecorder.WPF/Converters/ShortRoomIdToVisibilityConverter.cs new file mode 100644 index 0000000..e1ef980 --- /dev/null +++ b/BililiveRecorder.WPF/Converters/ShortRoomIdToVisibilityConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace BililiveRecorder.WPF.Converters +{ + internal class ShortRoomIdToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is int i ? i == 0 ? Visibility.Collapsed : Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/BililiveRecorder.WPF/Converters/ValueConverterGroup.cs b/BililiveRecorder.WPF/Converters/ValueConverterGroup.cs new file mode 100644 index 0000000..f86f5e8 --- /dev/null +++ b/BililiveRecorder.WPF/Converters/ValueConverterGroup.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Windows.Data; + +namespace BililiveRecorder.WPF.Converters +{ + internal class ValueConverterGroup : List, IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture)); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/BililiveRecorder.WPF/DesignTimeResources.xaml b/BililiveRecorder.WPF/DesignTimeResources.xaml new file mode 100644 index 0000000..e8e1c0a --- /dev/null +++ b/BililiveRecorder.WPF/DesignTimeResources.xaml @@ -0,0 +1,8 @@ + + + + + diff --git a/BililiveRecorder.WPF/EnumerableExtensions.cs b/BililiveRecorder.WPF/EnumerableExtensions.cs deleted file mode 100644 index 06d6d0b..0000000 --- a/BililiveRecorder.WPF/EnumerableExtensions.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace BililiveRecorder.WPF -{ - internal static class EnumerableExtensions - { - /// - /// Returns the elements with the maximum key value by using the default comparer to compare key values. - /// - /// Source sequence element type. - /// Key type. - /// Source sequence. - /// Key selector used to extract the key for each element in the sequence. - /// List with the elements that share the same maximum key value. - public static IList MaxBy(this IEnumerable source, Func keySelector) - { - if (source == null) { throw new ArgumentNullException("source"); } - if (keySelector == null) { throw new ArgumentNullException("keySelector"); } - - return MaxBy(source, keySelector, Comparer.Default); - } - - /// - /// Returns the elements with the minimum key value by using the specified comparer to compare key values. - /// - /// Source sequence element type. - /// Key type. - /// Source sequence. - /// Key selector used to extract the key for each element in the sequence. - /// Comparer used to determine the maximum key value. - /// List with the elements that share the same maximum key value. - public static IList MaxBy(this IEnumerable source, Func keySelector, IComparer comparer) - { - if (source == null) { throw new ArgumentNullException("source"); } - if (keySelector == null) { throw new ArgumentNullException("keySelector"); } - if (comparer == null) { throw new ArgumentNullException("comparer"); } - - return ExtremaBy(source, keySelector, (key, minValue) => comparer.Compare(key, minValue)); - } - - private static IList ExtremaBy(IEnumerable source, Func keySelector, Func compare) - { - var result = new List(); - - using (var e = source.GetEnumerator()) - { - if (!e.MoveNext()) { throw new InvalidOperationException("Source sequence doesn't contain any elements."); } - - var current = e.Current; - var resKey = keySelector(current); - result.Add(current); - - while (e.MoveNext()) - { - var cur = e.Current; - var key = keySelector(cur); - - var cmp = compare(key, resKey); - if (cmp == 0) { result.Add(cur); } - else if (cmp > 0) - { - result = new List { cur }; - resKey = key; - } - } - } - return result; - } - } -} diff --git a/BililiveRecorder.WPF/ClickSelectTextBox.xaml b/BililiveRecorder.WPF/Legacy/ClickSelectTextBox.xaml similarity index 100% rename from BililiveRecorder.WPF/ClickSelectTextBox.xaml rename to BililiveRecorder.WPF/Legacy/ClickSelectTextBox.xaml diff --git a/BililiveRecorder.WPF/ClickSelectTextBox.xaml.cs b/BililiveRecorder.WPF/Legacy/ClickSelectTextBox.xaml.cs similarity index 100% rename from BililiveRecorder.WPF/ClickSelectTextBox.xaml.cs rename to BililiveRecorder.WPF/Legacy/ClickSelectTextBox.xaml.cs diff --git a/BililiveRecorder.WPF/MainWindow.xaml b/BililiveRecorder.WPF/Legacy/MainWindow.xaml similarity index 98% rename from BililiveRecorder.WPF/MainWindow.xaml rename to BililiveRecorder.WPF/Legacy/MainWindow.xaml index e5c01f8..ce3df16 100644 --- a/BililiveRecorder.WPF/MainWindow.xaml +++ b/BililiveRecorder.WPF/Legacy/MainWindow.xaml @@ -1,4 +1,4 @@ - - + diff --git a/BililiveRecorder.WPF/MainWindow.xaml.cs b/BililiveRecorder.WPF/Legacy/MainWindow.xaml.cs similarity index 99% rename from BililiveRecorder.WPF/MainWindow.xaml.cs rename to BililiveRecorder.WPF/Legacy/MainWindow.xaml.cs index 95a0fc4..7d80bcc 100644 --- a/BililiveRecorder.WPF/MainWindow.xaml.cs +++ b/BililiveRecorder.WPF/Legacy/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using Autofac; +using Autofac; using BililiveRecorder.Core; using BililiveRecorder.FlvProcessor; using CommandLine; @@ -140,7 +140,8 @@ namespace BililiveRecorder.WPF }.ShowDialog() == true) { _AddLog = null; - Recorder.Shutdown(); + // Recorder.Shutdown(); + Recorder.Dispose(); try { File.WriteAllText(LAST_WORK_DIR_FILE, Recorder.Config.WorkDirectory); diff --git a/BililiveRecorder.WPF/SettingsWindow.xaml b/BililiveRecorder.WPF/Legacy/SettingsWindow.xaml similarity index 99% rename from BililiveRecorder.WPF/SettingsWindow.xaml rename to BililiveRecorder.WPF/Legacy/SettingsWindow.xaml index a7ab5a5..d209514 100644 --- a/BililiveRecorder.WPF/SettingsWindow.xaml +++ b/BililiveRecorder.WPF/Legacy/SettingsWindow.xaml @@ -1,4 +1,4 @@ - + Title="设置 - 录播姬" Height="930" Width="300"> diff --git a/BililiveRecorder.WPF/SettingsWindow.xaml.cs b/BililiveRecorder.WPF/Legacy/SettingsWindow.xaml.cs similarity index 100% rename from BililiveRecorder.WPF/SettingsWindow.xaml.cs rename to BililiveRecorder.WPF/Legacy/SettingsWindow.xaml.cs diff --git a/BililiveRecorder.WPF/TimedMessageBox.xaml b/BililiveRecorder.WPF/Legacy/TimedMessageBox.xaml similarity index 100% rename from BililiveRecorder.WPF/TimedMessageBox.xaml rename to BililiveRecorder.WPF/Legacy/TimedMessageBox.xaml diff --git a/BililiveRecorder.WPF/TimedMessageBox.xaml.cs b/BililiveRecorder.WPF/Legacy/TimedMessageBox.xaml.cs similarity index 100% rename from BililiveRecorder.WPF/TimedMessageBox.xaml.cs rename to BililiveRecorder.WPF/Legacy/TimedMessageBox.xaml.cs diff --git a/BililiveRecorder.WPF/UpdateBarUserControl.xaml b/BililiveRecorder.WPF/Legacy/UpdateBarUserControl.xaml similarity index 100% rename from BililiveRecorder.WPF/UpdateBarUserControl.xaml rename to BililiveRecorder.WPF/Legacy/UpdateBarUserControl.xaml diff --git a/BililiveRecorder.WPF/UpdateBarUserControl.xaml.cs b/BililiveRecorder.WPF/Legacy/UpdateBarUserControl.xaml.cs similarity index 100% rename from BililiveRecorder.WPF/UpdateBarUserControl.xaml.cs rename to BililiveRecorder.WPF/Legacy/UpdateBarUserControl.xaml.cs diff --git a/BililiveRecorder.WPF/ValueConverters.cs b/BililiveRecorder.WPF/Legacy/ValueConverters.cs similarity index 100% rename from BililiveRecorder.WPF/ValueConverters.cs rename to BililiveRecorder.WPF/Legacy/ValueConverters.cs diff --git a/BililiveRecorder.WPF/WorkDirectoryWindow.xaml b/BililiveRecorder.WPF/Legacy/WorkDirectoryWindow.xaml similarity index 100% rename from BililiveRecorder.WPF/WorkDirectoryWindow.xaml rename to BililiveRecorder.WPF/Legacy/WorkDirectoryWindow.xaml diff --git a/BililiveRecorder.WPF/WorkDirectoryWindow.xaml.cs b/BililiveRecorder.WPF/Legacy/WorkDirectoryWindow.xaml.cs similarity index 100% rename from BililiveRecorder.WPF/WorkDirectoryWindow.xaml.cs rename to BililiveRecorder.WPF/Legacy/WorkDirectoryWindow.xaml.cs diff --git a/BililiveRecorder.WPF/MockData/MockRecordedRoom.cs b/BililiveRecorder.WPF/MockData/MockRecordedRoom.cs new file mode 100644 index 0000000..3de56c7 --- /dev/null +++ b/BililiveRecorder.WPF/MockData/MockRecordedRoom.cs @@ -0,0 +1,113 @@ +using System; +using System.ComponentModel; +using BililiveRecorder.Core; +using BililiveRecorder.FlvProcessor; + +namespace BililiveRecorder.WPF.MockData +{ +#if DEBUG + internal class MockRecordedRoom : IRecordedRoom + { + private bool disposedValue; + + public MockRecordedRoom() + { + RoomId = 123456789; + ShortRoomId = 1234; + StreamerName = "Mock主播名Mock主播名Mock主播名Mock主播名"; + IsMonitoring = false; + IsRecording = true; + DownloadSpeedPersentage = 100d; + DownloadSpeedMegaBitps = 2.45d; + } + + public int ShortRoomId { get; set; } + + public int RoomId { get; set; } + + public string StreamerName { get; set; } + + public IStreamMonitor StreamMonitor { get; set; } + + public IFlvStreamProcessor Processor { get; set; } + + public bool IsMonitoring { get; set; } + + public bool IsRecording { get; set; } + + public double DownloadSpeedPersentage { get; set; } + + public double DownloadSpeedMegaBitps { get; set; } + + public DateTime LastUpdateDateTime { get; set; } + + public event PropertyChangedEventHandler PropertyChanged; + + public void Clip() + { + } + + public void RefreshRoomInfo() + { + } + + public void Shutdown() + { + } + + public bool Start() + { + IsMonitoring = true; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMonitoring))); + return true; + } + + public void StartRecord() + { + IsRecording = true; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRecording))); + } + + public void Stop() + { + IsMonitoring = false; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMonitoring))); + } + + public void StopRecord() + { + IsRecording = false; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRecording))); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~MockRecordedRoom() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +#endif +} diff --git a/BililiveRecorder.WPF/MockData/MockRecorder.cs b/BililiveRecorder.WPF/MockData/MockRecorder.cs new file mode 100644 index 0000000..3666cb5 --- /dev/null +++ b/BililiveRecorder.WPF/MockData/MockRecorder.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using BililiveRecorder.Core; +using BililiveRecorder.Core.Config; + +namespace BililiveRecorder.WPF.MockData +{ +#if DEBUG + internal class MockRecorder : IRecorder + { + private bool disposedValue; + + public MockRecorder() + { + Rooms.Add(new MockRecordedRoom + { + IsMonitoring = false, + IsRecording = false + }); + Rooms.Add(new MockRecordedRoom + { + IsMonitoring = true, + IsRecording = false + }); + Rooms.Add(new MockRecordedRoom + { + DownloadSpeedPersentage = 100, + DownloadSpeedMegaBitps = 12.45 + }); + Rooms.Add(new MockRecordedRoom + { + DownloadSpeedPersentage = 95, + DownloadSpeedMegaBitps = 789.45 + }); + Rooms.Add(new MockRecordedRoom + { + DownloadSpeedPersentage = 90 + }); + Rooms.Add(new MockRecordedRoom + { + DownloadSpeedPersentage = 85 + }); + Rooms.Add(new MockRecordedRoom + { + DownloadSpeedPersentage = 80 + }); + Rooms.Add(new MockRecordedRoom + { + DownloadSpeedPersentage = 75 + }); + Rooms.Add(new MockRecordedRoom + { + DownloadSpeedPersentage = 70 + }); + Rooms.Add(new MockRecordedRoom + { + DownloadSpeedPersentage = 109 + }); + + } + + private ObservableCollection Rooms { get; } = new ObservableCollection(); + + public ConfigV1 Config { get; } = new ConfigV1(); + + public int Count => Rooms.Count; + + public bool IsReadOnly => true; + + int ICollection.Count => Rooms.Count; + + bool ICollection.IsReadOnly => true; + + public IRecordedRoom this[int index] => Rooms[index]; + + public event PropertyChangedEventHandler PropertyChanged + { + add => (Rooms as INotifyPropertyChanged).PropertyChanged += value; + remove => (Rooms as INotifyPropertyChanged).PropertyChanged -= value; + } + + public event NotifyCollectionChangedEventHandler CollectionChanged + { + add => (Rooms as INotifyCollectionChanged).CollectionChanged += value; + remove => (Rooms as INotifyCollectionChanged).CollectionChanged -= value; + } + + 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(); + + public bool Initialize(string workdir) + { + Config.WorkDirectory = workdir; + return true; + } + + public void AddRoom(int roomid) + { + AddRoom(roomid, false); + } + + public void AddRoom(int roomid, bool enabled) + { + Rooms.Add(new MockRecordedRoom { RoomId = roomid, IsMonitoring = enabled }); + } + + public void RemoveRoom(IRecordedRoom rr) + { + Rooms.Remove(rr); + } + + public void SaveConfigToFile() + { + + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + Rooms.Clear(); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // set large fields to null + disposedValue = true; + } + } + + // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~MockRecorder() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +#endif +} diff --git a/BililiveRecorder.WPF/Models/LogModel.cs b/BililiveRecorder.WPF/Models/LogModel.cs new file mode 100644 index 0000000..19b4e62 --- /dev/null +++ b/BililiveRecorder.WPF/Models/LogModel.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Threading; + +namespace BililiveRecorder.WPF.Models +{ + internal class LogModel : ObservableCollection, IDisposable + { + private const int MAX_LINE = 50; + + private bool disposedValue; + + public static void AddLog(string log) => LogReceived?.Invoke(null, log); + public static event EventHandler LogReceived; + + public LogModel() : base(new[] { "" }) + { + LogReceived += LogModel_LogReceived; + } + + private void LogModel_LogReceived(object sender, string e) + { + _ = Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.DataBind, (Action)AddLogToCollection, e); + } + + private void AddLogToCollection(string e) + { + Add(e); + while (Count > MAX_LINE) + { + RemoveItem(0); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + LogReceived -= LogModel_LogReceived; + ClearItems(); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // set large fields to null + disposedValue = true; + } + } + + // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~LogModel() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/BililiveRecorder.WPF/Models/RootModel.cs b/BililiveRecorder.WPF/Models/RootModel.cs new file mode 100644 index 0000000..422562b --- /dev/null +++ b/BililiveRecorder.WPF/Models/RootModel.cs @@ -0,0 +1,50 @@ +using System; +using BililiveRecorder.Core; + +namespace BililiveRecorder.WPF.Models +{ + internal class RootModel : IDisposable + { + private bool disposedValue; + + public LogModel Logs { get; } = new LogModel(); + + public IRecorder Recorder { get; internal set; } + + public RootModel() + { + + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + Recorder?.Dispose(); + Logs.Dispose(); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // set large fields to null + disposedValue = true; + } + } + + // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~RootModel() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/BililiveRecorder.WPF/NLog.Debug.config b/BililiveRecorder.WPF/NLog.Debug.config index 7dbf5fd..f1475dc 100644 --- a/BililiveRecorder.WPF/NLog.Debug.config +++ b/BililiveRecorder.WPF/NLog.Debug.config @@ -16,7 +16,7 @@ for information on customizing logging rules and outputs. --> - + diff --git a/BililiveRecorder.WPF/NLog.xsd b/BililiveRecorder.WPF/NLog.xsd deleted file mode 100644 index 075bb91..0000000 --- a/BililiveRecorder.WPF/NLog.xsd +++ /dev/null @@ -1,3168 +0,0 @@ - - - - - - - - - - - - - - - Watch config file for changes and reload automatically. - - - - - Print internal NLog messages to the console. Default value is: false - - - - - Print internal NLog messages to the console error output. Default value is: false - - - - - Write internal NLog messages to the specified file. - - - - - Log level threshold for internal log messages. Default value is: Info. - - - - - Global log level threshold for application log messages. Messages below this level won't be logged.. - - - - - Throw an exception when there is an internal error. Default value is: false. - - - - - Throw an exception when there is a configuration error. If not set, determined by throwExceptions. - - - - - Gets or sets a value indicating whether Variables should be kept on configuration reload. Default value is: false. - - - - - Write internal NLog messages to the System.Diagnostics.Trace. Default value is: false. - - - - - Write timestamps for internal NLog messages. Default value is: true. - - - - - Use InvariantCulture as default culture instead of CurrentCulture. Default value is: false. - - - - - Perform mesage template parsing and formatting of LogEvent messages (true = Always, false = Never, empty = Auto Detect). Default value is: empty. - - - - - - - - - - - - - - Make all targets within this section asynchronous (creates additional threads but the calling thread isn't blocked by any target writes). - - - - - - - - - - - - - - - - - Prefix for targets/layout renderers/filters/conditions loaded from this assembly. - - - - - Load NLog extensions from the specified file (*.dll) - - - - - Load NLog extensions from the specified assembly. Assembly name should be fully qualified. - - - - - - - - - - Name of the logger. May include '*' character which acts like a wildcard. Allowed forms are: *, Name, *Name, Name* and *Name* - - - - - Comma separated list of levels that this rule matches. - - - - - Minimum level that this rule matches. - - - - - Maximum level that this rule matches. - - - - - Level that this rule matches. - - - - - Comma separated list of target names. - - - - - Ignore further rules if this one matches. - - - - - Enable or disable logging rule. Disabled rules are ignored. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Name of the file to be included. You could use * wildcard. The name is relative to the name of the current config file. - - - - - Ignore any errors in the include file. - - - - - - - Variable name. - - - - - Variable value. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Number of log events that should be processed in a batch by the lazy writer thread. - - - - - Limit of full s to write before yielding into Performance is better when writing many small batches, than writing a single large batch - - - - - Action to be taken when the lazy writer thread request queue count exceeds the set limit. - - - - - Limit on the number of requests in the lazy writer thread request queue. - - - - - Time in milliseconds to sleep between batches. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - - - Delay the flush until the LogEvent has been confirmed as written - - - - - Condition expression. Log events who meet this condition will cause a flush on the wrapped target. - - - - - Name of the target. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Number of log events to be buffered. - - - - - Timeout (in milliseconds) after which the contents of buffer will be flushed if there's no write in the specified period of time. Use -1 to disable timed flushes. - - - - - Action to take if the buffer overflows. - - - - - Indicates whether to use sliding timeout. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Encoding to be used. - - - - - Instance of that is used to format log messages. - - - - - End of line value if a newline is appended at the end of log message . - - - - - Maximum message size in bytes. - - - - - Indicates whether to append newline at the end of log message. - - - - - Action that should be taken if the will be more connections than . - - - - - Maximum queue size. - - - - - Maximum current connections. 0 = no maximum. - - - - - Indicates whether to keep connection open whenever possible. - - - - - Size of the connection cache (number of connections which are kept alive). - - - - - Network address. - - - - - Action that should be taken if the message is larger than maxMessageSize. - - - - - NDLC item separator. - - - - - NDC item separator. - - - - - Indicates whether to include NLog-specific extensions to log4j schema. - - - - - Indicates whether to include source info (file name and line number) in the information sent over the network. - - - - - Indicates whether to include contents of the stack. - - - - - Indicates whether to include stack contents. - - - - - Indicates whether to include dictionary contents. - - - - - Indicates whether to include dictionary contents. - - - - - Indicates whether to include call site (class and method name) in the information sent over the network. - - - - - Option to include all properties from the log events - - - - - AppInfo field. By default it's the friendly name of the current AppDomain. - - - - - Renderer for log4j:event logger-xml-attribute (Default ${logger}) - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - - - - - - Layout that should be use to calcuate the value for the parameter. - - - - - Viewer parameter name. - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Text to be rendered. - - - - - Header. - - - - - Footer. - - - - - Indicates whether to auto-check if the console is available. - Disables console writing if Environment.UserInteractive = False (Windows Service) - Disables console writing if Console Standard Input is not available (Non-Console-App) - - - - - The encoding for writing messages to the . - - - - - Indicates whether the error stream (stderr) should be used instead of the output stream (stdout). - - - - - Indicates whether to use default row highlighting rules. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Condition that must be met in order to set the specified foreground and background color. - - - - - Background color. - - - - - Foreground color. - - - - - - - - - - - - - - - - Compile the ? This can improve the performance, but at the costs of more memory usage. If false, the Regex Cache is used. - - - - - Indicates whether to ignore case when comparing texts. - - - - - Regular expression to be matched. You must specify either text or regex. - - - - - Text to be matched. You must specify either text or regex. - - - - - Indicates whether to match whole words only. - - - - - Background color. - - - - - Foreground color. - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Text to be rendered. - - - - - Header. - - - - - Footer. - - - - - Indicates whether to auto-check if the console is available - Disables console writing if Environment.UserInteractive = False (Windows Service) - Disables console writing if Console Standard Input is not available (Non-Console-App) - - - - - The encoding for writing messages to the . - - - - - Indicates whether to send the log messages to the standard error instead of the standard output. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Obsolete - value will be ignored! The logging code always runs outside of transaction. Gets or sets a value indicating whether to use database transactions. Some data providers require this. - - - - - Database user name. If the ConnectionString is not provided this value will be used to construct the "User ID=" part of the connection string. - - - - - Name of the database provider. - - - - - Database password. If the ConnectionString is not provided this value will be used to construct the "Password=" part of the connection string. - - - - - Indicates whether to keep the database connection open between the log events. - - - - - Database name. If the ConnectionString is not provided this value will be used to construct the "Database=" part of the connection string. - - - - - Name of the connection string (as specified in <connectionStrings> configuration section. - - - - - Connection string. When provided, it overrides the values specified in DBHost, DBUserName, DBPassword, DBDatabase. - - - - - Database host name. If the ConnectionString is not provided this value will be used to construct the "Server=" part of the connection string. - - - - - Connection string using for installation and uninstallation. If not provided, regular ConnectionString is being used. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - Text of the SQL command to be run on each log level. - - - - - Type of the SQL command to be run on each log level. - - - - - - - - - - - - - - - - - - - - - - - Type of the command. - - - - - Connection string to run the command against. If not provided, connection string from the target is used. - - - - - Indicates whether to ignore failures. - - - - - Command text. - - - - - - - - - - - - - - Layout that should be use to calcuate the value for the parameter. - - - - - Database parameter name. - - - - - Database parameter precision. - - - - - Database parameter scale. - - - - - Database parameter size. - - - - - - - - - - - - - - - - Name of the target. - - - - - Text to be rendered. - - - - - Header. - - - - - Footer. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - Name of the target. - - - - - Layout used to format log messages. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Layout used to format log messages. - - - - - Layout that renders event Category. - - - - - Optional entrytype. When not set, or when not convertable to then determined by - - - - - Layout that renders event ID. - - - - - Name of the Event Log to write to. This can be System, Application or any user-defined name. - - - - - Name of the machine on which Event Log service is running. - - - - - Maximum Event log size in kilobytes. If null, the value won't be set. Default is 512 Kilobytes as specified by Eventlog API - - - - - Message length limit to write to the Event Log. - - - - - Value to be used as the event Source. - - - - - Action to take if the message is larger than the option. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Indicates whether to return to the first target after any successful write. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Text to be rendered. - - - - - Header. - - - - - Footer. - - - - - File encoding. - - - - - Line ending mode. - - - - - Indicates whether to compress archive files into the zip archive format. - - - - - Way file archives are numbered. - - - - - Name of the file to be used for an archive. - - - - - Is the an absolute or relative path? - - - - - Indicates whether to automatically archive log files every time the specified time passes. - - - - - Size in bytes above which log files will be automatically archived. Warning: combining this with isn't supported. We cannot create multiple archive files, if they should have the same name. Choose: - - - - - Maximum number of archive files that should be kept. - - - - - Indicates whether the footer should be written only when the file is archived. - - - - - Maximum number of log filenames that should be stored as existing. - - - - - Is the an absolute or relative path? - - - - - Gets or set a value indicating whether a managed file stream is forced, instead of using the native implementation. - - - - - Value indicationg whether file creation calls should be synchronized by a system global mutex. - - - - - Indicates whether to replace file contents on each write instead of appending log message at the end. - - - - - Indicates whether to write BOM (byte order mark) in created files - - - - - Indicates whether to enable log file(s) to be deleted. - - - - - Name of the file to write to. - - - - - Value specifying the date format to use when archiving files. - - - - - Indicates whether to archive old log file on startup. - - - - - Cleanup invalid values in a filename, e.g. slashes in a filename. If set to true, this can impact the performance of massive writes. If set to false, nothing gets written when the filename is wrong. - - - - - Indicates whether to create directories if they do not exist. - - - - - Indicates whether to delete old log file on startup. - - - - - File attributes (Windows only). - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - Indicates whether concurrent writes to the log file by multiple processes on different network hosts. - - - - - Maximum number of seconds that files are kept open. If this number is negative the files are not automatically closed after a period of inactivity. - - - - - Number of files to be kept open. Setting this to a higher value may improve performance in a situation where a single File target is writing to many files (such as splitting by level or by logger). - - - - - Indicates whether to keep log file open instead of opening and closing it on each logging event. - - - - - Whether or not this target should just discard all data that its asked to write. Mostly used for when testing NLog Stack except final write - - - - - Indicates whether concurrent writes to the log file by multiple processes on the same host. - - - - - Number of times the write is appended on the file before NLog discards the log message. - - - - - Delay in milliseconds to wait before attempting to write to the file again. - - - - - Log file buffer size in bytes. - - - - - Maximum number of seconds before open files are flushed. If this number is negative or zero the files are not flushed by timer. - - - - - Indicates whether to automatically flush the file buffers after each log message. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Condition expression. Log events who meet this condition will be forwarded to the wrapped target. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Windows domain name to change context to. - - - - - Required impersonation level. - - - - - Type of the logon provider. - - - - - Logon Type. - - - - - User account password. - - - - - Indicates whether to revert to the credentials of the process instead of impersonating another user. - - - - - Username to change context to. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Interval in which messages will be written up to the number of messages. - - - - - Maximum allowed number of messages written per . - - - - - Name of the target. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Endpoint address. - - - - - Name of the endpoint configuration in WCF configuration file. - - - - - Indicates whether to use a WCF service contract that is one way (fire and forget) or two way (request-reply) - - - - - Client ID. - - - - - Indicates whether to include per-event properties in the payload sent to the server. - - - - - Indicates whether to use binary message encoding. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - Layout that should be use to calculate the value for the parameter. - - - - - Name of the parameter. - - - - - Type of the parameter. - - - - - Type of the parameter. Obsolete alias for - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Text to be rendered. - - - - - Header. - - - - - Footer. - - - - - Indicates whether NewLine characters in the body should be replaced with tags. - - - - - Priority used for sending mails. - - - - - Encoding to be used for sending e-mail. - - - - - BCC email addresses separated by semicolons (e.g. john@domain.com;jane@domain.com). - - - - - CC email addresses separated by semicolons (e.g. john@domain.com;jane@domain.com). - - - - - Indicates whether to add new lines between log entries. - - - - - Indicates whether to send message as HTML instead of plain text. - - - - - Sender's email address (e.g. joe@domain.com). - - - - - Mail message body (repeated for each log message send in one mail). - - - - - Mail subject. - - - - - Recipients' email addresses separated by semicolons (e.g. john@domain.com;jane@domain.com). - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - Indicates the SMTP client timeout. - - - - - SMTP Server to be used for sending. - - - - - SMTP Authentication mode. - - - - - Username used to connect to SMTP server (used when SmtpAuthentication is set to "basic"). - - - - - Password used to authenticate against SMTP server (used when SmtpAuthentication is set to "basic"). - - - - - Indicates whether SSL (secure sockets layer) should be used when communicating with SMTP server. - - - - - Port number that SMTP Server is listening on. - - - - - Indicates whether the default Settings from System.Net.MailSettings should be used. - - - - - Folder where applications save mail messages to be processed by the local SMTP server. - - - - - Specifies how outgoing email messages will be handled. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Layout used to format log messages. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Class name. - - - - - Method name. The method must be public and static. Use the AssemblyQualifiedName , https://msdn.microsoft.com/en-us/library/system.type.assemblyqualifiedname(v=vs.110).aspx e.g. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Layout used to format log messages. - - - - - Encoding to be used. - - - - - End of line value if a newline is appended at the end of log message . - - - - - Maximum message size in bytes. - - - - - Indicates whether to append newline at the end of log message. - - - - - Network address. - - - - - Size of the connection cache (number of connections which are kept alive). - - - - - Indicates whether to keep connection open whenever possible. - - - - - Maximum current connections. 0 = no maximum. - - - - - Maximum queue size. - - - - - Action that should be taken if the will be more connections than . - - - - - Action that should be taken if the message is larger than maxMessageSize. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Encoding to be used. - - - - - Instance of that is used to format log messages. - - - - - End of line value if a newline is appended at the end of log message . - - - - - Maximum message size in bytes. - - - - - Indicates whether to append newline at the end of log message. - - - - - Action that should be taken if the will be more connections than . - - - - - Maximum queue size. - - - - - Maximum current connections. 0 = no maximum. - - - - - Indicates whether to keep connection open whenever possible. - - - - - Size of the connection cache (number of connections which are kept alive). - - - - - Network address. - - - - - Action that should be taken if the message is larger than maxMessageSize. - - - - - NDLC item separator. - - - - - NDC item separator. - - - - - Indicates whether to include NLog-specific extensions to log4j schema. - - - - - Indicates whether to include source info (file name and line number) in the information sent over the network. - - - - - Indicates whether to include contents of the stack. - - - - - Indicates whether to include stack contents. - - - - - Indicates whether to include dictionary contents. - - - - - Indicates whether to include dictionary contents. - - - - - Indicates whether to include call site (class and method name) in the information sent over the network. - - - - - Option to include all properties from the log events - - - - - AppInfo field. By default it's the friendly name of the current AppDomain. - - - - - Renderer for log4j:event logger-xml-attribute (Default ${logger}) - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - Name of the target. - - - - - Layout used to format log messages. - - - - - Indicates whether to perform layout calculation. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - Name of the target. - - - - - Layout used to format log messages. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Indicates whether performance counter should be automatically created. - - - - - Name of the performance counter category. - - - - - Counter help text. - - - - - Name of the performance counter. - - - - - Performance counter type. - - - - - The value by which to increment the counter. - - - - - Performance counter instance name. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Default filter to be applied when no specific rule matches. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - Condition to be tested. - - - - - Resulting filter to be applied when the condition matches. - - - - - - - - - - - - - Name of the target. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - Name of the target. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - Number of times to repeat each log message. - - - - - - - - - - - - - - - - - Name of the target. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - Number of retries that should be attempted on the wrapped target in case of a failure. - - - - - Time to wait between retries in milliseconds. - - - - - - - - - - - - - - - Name of the target. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - Name of the target. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - Name of the target. - - - - - Layout used to format log messages. - - - - - Always use independent of - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Name of the target. - - - - - Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit - - - - - Should we include the BOM (Byte-order-mark) for UTF? Influences the property. This will only work for UTF-8. - - - - - Web service method name. Only used with Soap. - - - - - Web service namespace. Only used with Soap. - - - - - Protocol to be used when calling web service. - - - - - Custom proxy address, include port separated by a colon - - - - - Encoding. - - - - - Web service URL. - - - - - Value whether escaping be done according to the old NLog style (Very non-standard) - - - - - Value whether escaping be done according to Rfc3986 (Supports Internationalized Resource Identifiers - IRIs) - - - - - Indicates whether to pre-authenticate the HttpWebRequest (Requires 'Authorization' in parameters) - - - - - Name of the root XML element, if POST of XML document chosen. If so, this property must not be null. (see and ). - - - - - (optional) root namespace of the XML document, if POST of XML document chosen. (see and ). - - - - - Proxy configuration when calling web service - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Footer layout. - - - - - Header layout. - - - - - Body layout (can be repeated multiple times). - - - - - Custom column delimiter value (valid when ColumnDelimiter is set to 'Custom'). - - - - - Column delimiter. - - - - - Quote Character. - - - - - Quoting mode. - - - - - Indicates whether CVS should include header. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Layout of the column. - - - - - Name of the column. - - - - - - - - - - - - - - - - - - - List of property names to exclude when is true - - - - - Option to include all properties from the log event (as JSON) - - - - - Indicates whether to include contents of the dictionary. - - - - - Indicates whether to include contents of the dictionary. - - - - - Option to render the empty object value {} - - - - - Option to suppress the extra spaces in the output json - - - - - How far should the JSON serializer follow object references before backing off - - - - - - - - - - - - - - - - Layout that will be rendered as the attribute's value. - - - - - Name of the attribute. - - - - - Determines wether or not this attribute will be Json encoded. - - - - - Indicates whether to escape non-ascii characters - - - - - Whether an attribute with empty value should be included in the output - - - - - - - - - - - - - - Footer layout. - - - - - Header layout. - - - - - Body layout (can be repeated multiple times). - - - - - - - - - - - - - - - - - - Option to include all properties from the log events - - - - - Indicates whether to include contents of the dictionary. - - - - - Indicates whether to include contents of the dictionary. - - - - - Indicates whether to include contents of the stack. - - - - - Indicates whether to include contents of the stack. - - - - - - - - - - - - - - Layout text. - - - - - - - - - - - - - - - Action to be taken when filter matches. - - - - - Condition expression. - - - - - - - - - - - - - - - - - - - - - - - - - - Action to be taken when filter matches. - - - - - Indicates whether to ignore case when comparing strings. - - - - - Layout to be used to filter log messages. - - - - - Substring to be matched. - - - - - - - - - - - - - - - - - Action to be taken when filter matches. - - - - - String to compare the layout to. - - - - - Indicates whether to ignore case when comparing strings. - - - - - Layout to be used to filter log messages. - - - - - - - - - - - - - - - - - Action to be taken when filter matches. - - - - - Indicates whether to ignore case when comparing strings. - - - - - Layout to be used to filter log messages. - - - - - Substring to be matched. - - - - - - - - - - - - - - - - - Action to be taken when filter matches. - - - - - String to compare the layout to. - - - - - Indicates whether to ignore case when comparing strings. - - - - - Layout to be used to filter log messages. - - - - - - - - - - - - - - - - - - - - - - - - Action to be taken when filter matches. - - - - - Default number of unique filter values to expect, will automatically increase if needed - - - - - Applies the configured action to the initial logevent that starts the timeout period. Used to configure that it should ignore all events until timeout. - - - - - Layout to be used to filter log messages. - - - - - Max number of unique filter values to expect simultaneously - - - - - Max length of filter values, will truncate if above limit - - - - - How long before a filter expires, and logging is accepted again - - - - - Default buffer size for the internal buffers - - - - - Reuse internal buffers, and doesn't have to constantly allocate new buffers - - - - - Append FilterCount to the when an event is no longer filtered - - - - - Insert FilterCount value into when an event is no longer filtered - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/BililiveRecorder.WPF/NewMainWindow.xaml b/BililiveRecorder.WPF/NewMainWindow.xaml new file mode 100644 index 0000000..9ebb783 --- /dev/null +++ b/BililiveRecorder.WPF/NewMainWindow.xaml @@ -0,0 +1,20 @@ + + + diff --git a/BililiveRecorder.WPF/NewMainWindow.xaml.cs b/BililiveRecorder.WPF/NewMainWindow.xaml.cs new file mode 100644 index 0000000..fc52b77 --- /dev/null +++ b/BililiveRecorder.WPF/NewMainWindow.xaml.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading; +using System.Windows; +using BililiveRecorder.WPF.Controls; +using ModernWpf.Controls; + +namespace BililiveRecorder.WPF +{ + /// + /// Interaction logic for NewMainWindow.xaml + /// + public partial class NewMainWindow : Window + { + public NewMainWindow() + { + InitializeComponent(); + + Title = "录播姬 " + BuildInfo.Version + " " + BuildInfo.HeadShaShort; + + BeforeWindowClose += NewMainWindow_BeforeWindowClose; + SingleInstance.NotificationReceived += SingleInstance_NotificationReceived; + } + + private void SingleInstance_NotificationReceived(object sender, EventArgs e) + { + WindowState = WindowState.Normal; + Topmost = true; + Activate(); + Topmost = false; + Focus(); + } + + private bool CloseConfirmed = false; + + private readonly SemaphoreSlim CloseWindowSemaphoreSlim = new SemaphoreSlim(1, 1); + + public event EventHandler BeforeWindowClose; + + public bool PromptCloseConfirm { get; set; } = true; + + private void NewMainWindow_BeforeWindowClose(object sender, EventArgs e) + { + RootPage?.Shutdown(); + SingleInstance.Cleanup(); + } + + private async void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) + { + if (PromptCloseConfirm && !CloseConfirmed) + { + e.Cancel = true; + if (CloseWindowSemaphoreSlim.Wait(0)) + { + try + { + if (await new CloseWindowConfirmDialog().ShowAsync() == ContentDialogResult.Primary) + { + CloseConfirmed = true; + CloseWindowSemaphoreSlim.Release(); + Close(); + return; + } + } + catch (Exception) { } + CloseWindowSemaphoreSlim.Release(); + } + } + else + { + BeforeWindowClose?.Invoke(this, EventArgs.Empty); + return; + } + } + + public void CloseWithoutConfirm() + { + CloseConfirmed = true; + Close(); + } + + private void RootPage_CloseWindowRequested(object sender, EventArgs e) + { + CloseWithoutConfirm(); + } + } +} diff --git a/BililiveRecorder.WPF/Nlog.Release.config b/BililiveRecorder.WPF/Nlog.Release.config index 669e095..ea13a42 100644 --- a/BililiveRecorder.WPF/Nlog.Release.config +++ b/BililiveRecorder.WPF/Nlog.Release.config @@ -6,7 +6,7 @@ throwExceptions="false" internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log"> - + + + + + + + + + + + + + + + + + + + + + + + + 录制断开后等待多长时间再尝试开始录制 + + + + + + 发出连接直播服务器的请求后等待多长时间 + 防止直播服务器长时间不返回数据导致卡住 + + + + + + + 弹幕服务器被断开后等待多长时间再尝试连接 + 监控开播的主要途径是通过弹幕服务器发送的信息 + + + + + + + 在一定时间没有收到直播服务器发送的数据后断开重连 + 用于防止因为玄学网络问题导致卡住 + + + + + + + 此项影响的时间间隔是定时请求HTTP接口的间隔, + 主要目的是防止没有从弹幕服务器收到开播消息, + 所以此项不需要设置太短。 + 时间间隔设置太短会被B站服务器屏蔽,导致无法录制。 + + + + + + + + diff --git a/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml.cs b/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml.cs new file mode 100644 index 0000000..fbecf7d --- /dev/null +++ b/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.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.Pages +{ + /// + /// Interaction logic for AdvancedSettingsPage.xaml + /// + public partial class AdvancedSettingsPage + { + public AdvancedSettingsPage() + { + InitializeComponent(); + } + } +} diff --git a/BililiveRecorder.WPF/Pages/LogPage.xaml b/BililiveRecorder.WPF/Pages/LogPage.xaml new file mode 100644 index 0000000..f54613d --- /dev/null +++ b/BililiveRecorder.WPF/Pages/LogPage.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BililiveRecorder.WPF/Pages/LogPage.xaml.cs b/BililiveRecorder.WPF/Pages/LogPage.xaml.cs new file mode 100644 index 0000000..2352432 --- /dev/null +++ b/BililiveRecorder.WPF/Pages/LogPage.xaml.cs @@ -0,0 +1,38 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace BililiveRecorder.WPF.Pages +{ + /// + /// Interaction logic for LogPage.xaml + /// + public partial class LogPage + { + public LogPage() + { + InitializeComponent(); + VersionTextBlock.Text = BuildInfo.Version + " " + BuildInfo.HeadShaShort; + } + + private void TextBlock_MouseRightButtonUp(object sender, MouseButtonEventArgs e) + { + try + { + if (sender is TextBlock textBlock) + { + Clipboard.SetText(textBlock.Text); + } + } + catch (Exception) + { + } + } + + private void ScrollViewer_Loaded(object sender, RoutedEventArgs e) + { + (sender as ScrollViewer)?.ScrollToEnd(); + } + } +} diff --git a/BililiveRecorder.WPF/Pages/RoomListPage.xaml b/BililiveRecorder.WPF/Pages/RoomListPage.xaml new file mode 100644 index 0000000..86a329d --- /dev/null +++ b/BililiveRecorder.WPF/Pages/RoomListPage.xaml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BililiveRecorder.WPF/Pages/RoomListPage.xaml.cs b/BililiveRecorder.WPF/Pages/RoomListPage.xaml.cs new file mode 100644 index 0000000..f64e5ac --- /dev/null +++ b/BililiveRecorder.WPF/Pages/RoomListPage.xaml.cs @@ -0,0 +1,80 @@ +using System; +using System.Text.RegularExpressions; +using System.Linq; +using BililiveRecorder.Core; +using BililiveRecorder.WPF.Controls; + +namespace BililiveRecorder.WPF.Pages +{ + /// + /// Interaction logic for RoomList.xaml + /// + public partial class RoomListPage + { + private static readonly Regex RoomIdRegex + = new Regex(@"^(?:https?:\/\/)?live\.bilibili\.com\/(?:blanc\/|h5\/)?(\d*)(?:\?.*)?$", + RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); + + public RoomListPage() + { + InitializeComponent(); + } + + private async void RoomCard_DeleteRequested(object sender, EventArgs e) + { + if (DataContext is IRecorder rec && sender is IRecordedRoom room) + { + var dialog = new DeleteRoomConfirmDialog + { + DataContext = room + }; + + var result = await dialog.ShowAsync(); + + if (result == ModernWpf.Controls.ContentDialogResult.Primary) + { + rec.RemoveRoom(room); + } + } + } + + private async void AddRoomCard_AddRoomRequested(object sender, string e) + { + var input = e.Trim(); + if (string.IsNullOrWhiteSpace(input) || !(DataContext is IRecorder rec)) return; + + if (!int.TryParse(input, out var roomid)) + { + var m = RoomIdRegex.Match(input); + if (m.Success && m.Groups.Count > 1 && int.TryParse(m.Groups[1].Value, out var result2)) + { + roomid = result2; + } + else + { + await new AddRoomFailedDialog { DataContext = "请输入B站直播房间号或直播间链接" }.ShowAsync(); + return; + } + } + + if (roomid < 0) + { + await new AddRoomFailedDialog { DataContext = "房间号不能是负数" }.ShowAsync(); + return; + } + else if (roomid == 0) + { + await new AddRoomFailedDialog { DataContext = "房间号不能是 0" }.ShowAsync(); + return; + } + + if (rec.Any(x => x.RoomId == roomid || x.ShortRoomId == roomid)) + { + await new AddRoomFailedDialog { DataContext = "这个直播间已经被添加过了" }.ShowAsync(); + return; + } + + rec.AddRoom(roomid); + } + } +} diff --git a/BililiveRecorder.WPF/Pages/RootPage.xaml b/BililiveRecorder.WPF/Pages/RootPage.xaml new file mode 100644 index 0000000..20ac0ff --- /dev/null +++ b/BililiveRecorder.WPF/Pages/RootPage.xaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BililiveRecorder.WPF/Pages/RootPage.xaml.cs b/BililiveRecorder.WPF/Pages/RootPage.xaml.cs new file mode 100644 index 0000000..4f8c58b --- /dev/null +++ b/BililiveRecorder.WPF/Pages/RootPage.xaml.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Autofac; +using BililiveRecorder.Core; +using BililiveRecorder.FlvProcessor; +using BililiveRecorder.WPF.Controls; +using BililiveRecorder.WPF.Models; +using CommandLine; +using ModernWpf.Controls; +using Path = System.IO.Path; + +namespace BililiveRecorder.WPF.Pages +{ + /// + /// Interaction logic for RootPage.xaml + /// + public partial class RootPage : UserControl + { + private readonly Dictionary PageMap = new Dictionary(); + private readonly string lastdir_path = Path.Combine(Path.GetDirectoryName(typeof(RootPage).Assembly.Location), "lastdir.txt"); + + private IContainer Container { get; set; } + private ILifetimeScope RootScope { get; set; } + + private int SettingsClickCount = 0; + + internal RootModel Model { get; private set; } + + public event EventHandler CloseWindowRequested; + + public RootPage() + { + void AddType(Type t) => PageMap.Add(t.Name, t); + AddType(typeof(RoomListPage)); + AddType(typeof(LogPage)); + AddType(typeof(SettingsPage)); + AddType(typeof(AdvancedSettingsPage)); + + Model = new RootModel(); + DataContext = Model; + + var builder = new ContainerBuilder(); + builder.RegisterModule(); + builder.RegisterModule(); + Container = builder.Build(); + RootScope = Container.BeginLifetimeScope("recorder_root"); + + InitializeComponent(); + AdvancedSettingsPageItem.Visibility = Visibility.Hidden; + + Loaded += RootPage_Loaded; + } + + public void Shutdown() + { + Model.Dispose(); + } + + private async void RootPage_Loaded(object sender, RoutedEventArgs e) + { + bool first_time = true; + + var recorder = RootScope.Resolve(); + var error = string.Empty; + string path; + while (true) + { + CommandLineOption commandLineOption = null; + if (first_time) + { + first_time = false; + Parser.Default + .ParseArguments(Environment.GetCommandLineArgs()) + .WithParsed(x => commandLineOption = x); + + if (!string.IsNullOrWhiteSpace(commandLineOption.WorkDirectory)) + { + path = Path.GetFullPath(commandLineOption.WorkDirectory); + goto check_path; + } + } + + string lastdir = string.Empty; + try + { + if (File.Exists(lastdir_path)) + { + lastdir = File.ReadAllText(lastdir_path).Replace("\r", "").Replace("\n", "").Trim(); + } + } + catch (Exception) { } + var w = new WorkDirectorySelectorDialog + { + Error = error, + Path = lastdir + }; + var result = await w.ShowAsync(); + if (result != ContentDialogResult.Primary) + { + CloseWindowRequested?.Invoke(this, EventArgs.Empty); + return; + } + + path = Path.GetFullPath(w.Path); + check_path: + var config = Path.Combine(path, "config.json"); + + if (!Directory.Exists(path)) + { + error = "目录不存在"; + continue; + } + else if (!Directory.EnumerateFiles(path).Any()) + { + // 可用的空文件夹 + } + else if (!File.Exists(config)) + { + error = "目录已有其他文件"; + continue; + } + + try + { + if (string.IsNullOrWhiteSpace(commandLineOption?.WorkDirectory)) + { + File.WriteAllText(lastdir_path, path); + } + } + catch (Exception) { } + + // 检查已经在同目录运行的其他进程 + if (SingleInstance.CheckMutex(path)) + { + if (recorder.Initialize(path)) + { + Model.Recorder = recorder; + RoomListPageNavigationViewItem.IsSelected = true; + break; + } + else + { + error = "配置文件加载失败"; + continue; + } + } + else + { + CloseWindowRequested?.Invoke(this, EventArgs.Empty); + return; + } + } + } + + private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) + { + SettingsClickCount = 0; + if (args.IsSettingsSelected) + { + MainFrame.Navigate(typeof(SettingsPage)); + } + else + { + var selectedItem = (NavigationViewItem)args.SelectedItem; + var selectedItemTag = (string)selectedItem.Tag; + if (PageMap.ContainsKey(selectedItemTag)) + { + var pageType = PageMap[selectedItemTag]; + MainFrame.Navigate(pageType); + } + } + } + + private void NavigationViewItem_MouseRightButtonUp(object sender, MouseButtonEventArgs e) + { + if (++SettingsClickCount > 3) + { + SettingsClickCount = 0; + AdvancedSettingsPageItem.Visibility = AdvancedSettingsPageItem.Visibility != Visibility.Visible ? Visibility.Visible : Visibility.Hidden; + } + } + } +} diff --git a/BililiveRecorder.WPF/Pages/SettingsPage.xaml b/BililiveRecorder.WPF/Pages/SettingsPage.xaml new file mode 100644 index 0000000..2bc326f --- /dev/null +++ b/BililiveRecorder.WPF/Pages/SettingsPage.xaml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 文件名变量说明 + + 日期: {date} + 时间: {time} + 房间号: {roomid} + 标题: {title} + 主播名: {name} + 随机数字: {random} + + 所有 { } 大括号均为英文半角括号 + 只支持 flv 格式 + + + + + + + + + + + + + + diff --git a/BililiveRecorder.WPF/Pages/SettingsPage.xaml.cs b/BililiveRecorder.WPF/Pages/SettingsPage.xaml.cs new file mode 100644 index 0000000..194f6d3 --- /dev/null +++ b/BililiveRecorder.WPF/Pages/SettingsPage.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.Pages +{ + /// + /// Interaction logic for SettingsPage.xaml + /// + public partial class SettingsPage + { + public SettingsPage() + { + InitializeComponent(); + } + } +} diff --git a/BililiveRecorder.WPF/SingleInstance.cs b/BililiveRecorder.WPF/SingleInstance.cs new file mode 100644 index 0000000..8d4d7d8 --- /dev/null +++ b/BililiveRecorder.WPF/SingleInstance.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Remoting; +using System.Runtime.Remoting.Channels; +using System.Runtime.Remoting.Channels.Ipc; +using System.Runtime.Serialization.Formatters; +using System.Text; +using System.Threading; +using System.Windows; +using System.Windows.Threading; + +namespace BililiveRecorder.WPF +{ + public static class SingleInstance + { + private static Mutex singleInstanceMutex; + private static IpcServerChannel channel; + + public static event EventHandler NotificationReceived; + + public static bool CheckMutex(string path) + { + const string RemoteServiceName = "SingleInstanceApplicationService"; + + var b64path = Convert.ToBase64String(Encoding.UTF8.GetBytes(path)).Replace('+', '_').Replace('/', '-'); + var identifier = "BililiveRecorder:SingeInstance:" + b64path; + + singleInstanceMutex = new Mutex(true, identifier, out var createdNew); + if (createdNew) + { + channel = new IpcServerChannel(new Dictionary + { + ["name"] = identifier, + ["portName"] = identifier, + ["exclusiveAddressUse"] = "false" + }, new BinaryServerFormatterSinkProvider + { + TypeFilterLevel = TypeFilterLevel.Full + }); + + ChannelServices.RegisterChannel(channel, true); + RemotingServices.Marshal(new IPCRemoteService(), RemoteServiceName); + } + else + { + ChannelServices.RegisterChannel(new IpcClientChannel(), true); + var remote = (IPCRemoteService)RemotingServices.Connect(typeof(IPCRemoteService), $"ipc://{identifier}/{RemoteServiceName}"); + remote?.Notify(); + } + + return createdNew; + } + + public static void Cleanup() + { + singleInstanceMutex?.Close(); + singleInstanceMutex = null; + + if (channel != null) + { + ChannelServices.UnregisterChannel(channel); + channel = null; + } + } + + private static void ActivateFirstInstanceCallback() => NotificationReceived?.Invoke(null, EventArgs.Empty); + + private class IPCRemoteService : MarshalByRefObject + { + public void Notify() => Application.Current?.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)ActivateFirstInstanceCallback); + public override object InitializeLifetimeService() => null; + } + } +} diff --git a/BililiveRecorder.WPF/packages.config b/BililiveRecorder.WPF/packages.config deleted file mode 100644 index 30daf64..0000000 --- a/BililiveRecorder.WPF/packages.config +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/BililiveRecorder.sln b/BililiveRecorder.sln index 68ac2bf..dd46da1 100644 --- a/BililiveRecorder.sln +++ b/BililiveRecorder.sln @@ -14,6 +14,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.FlvProcess EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Cli", "BililiveRecorder.Cli\BililiveRecorder.Cli.csproj", "{1B626335-283F-4313-9045-B5B96FAAB2DF}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FD13D9F6-94CD-4CBA-AEA8-EF71002EAC6B}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU