Core: Create config v3

This commit is contained in:
genteure 2021-12-19 20:58:32 +08:00
parent 82ed148009
commit 8a65c9645d
17 changed files with 1202 additions and 48 deletions

View File

@ -5,7 +5,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using BililiveRecorder.Core.Config.V2;
using BililiveRecorder.Core.Config.V3;
namespace BililiveRecorder.Cli.Configure
{
@ -22,7 +22,7 @@ namespace BililiveRecorder.Cli.Configure
RecordDanmakuGift,
RecordDanmakuGuard,
RecordingQuality,
RecordFilenameFormat,
FileNameRecordTemplate,
WebHookUrls,
WebHookUrlsV2,
WpfShowTitleAndArea,
@ -68,7 +68,7 @@ namespace BililiveRecorder.Cli.Configure
GlobalConfig.Add(GlobalConfigProperties.RecordDanmakuGift, new ConfigInstruction<GlobalConfig, bool>(config => config.HasRecordDanmakuGift = false, (config, value) => config.RecordDanmakuGift = value) { Name = "RecordDanmakuGift", CanBeOptional = true });
GlobalConfig.Add(GlobalConfigProperties.RecordDanmakuGuard, new ConfigInstruction<GlobalConfig, bool>(config => config.HasRecordDanmakuGuard = false, (config, value) => config.RecordDanmakuGuard = value) { Name = "RecordDanmakuGuard", CanBeOptional = true });
GlobalConfig.Add(GlobalConfigProperties.RecordingQuality, new ConfigInstruction<GlobalConfig, string>(config => config.HasRecordingQuality = false, (config, value) => config.RecordingQuality = value) { Name = "RecordingQuality", CanBeOptional = true });
GlobalConfig.Add(GlobalConfigProperties.RecordFilenameFormat, new ConfigInstruction<GlobalConfig, string>(config => config.HasRecordFilenameFormat = false, (config, value) => config.RecordFilenameFormat = value) { Name = "RecordFilenameFormat", CanBeOptional = true });
GlobalConfig.Add(GlobalConfigProperties.FileNameRecordTemplate, new ConfigInstruction<GlobalConfig, string>(config => config.HasFileNameRecordTemplate = false, (config, value) => config.FileNameRecordTemplate = value) { Name = "FileNameRecordTemplate", CanBeOptional = true });
GlobalConfig.Add(GlobalConfigProperties.WebHookUrls, new ConfigInstruction<GlobalConfig, string>(config => config.HasWebHookUrls = false, (config, value) => config.WebHookUrls = value) { Name = "WebHookUrls", CanBeOptional = true });
GlobalConfig.Add(GlobalConfigProperties.WebHookUrlsV2, new ConfigInstruction<GlobalConfig, string>(config => config.HasWebHookUrlsV2 = false, (config, value) => config.WebHookUrlsV2 = value) { Name = "WebHookUrlsV2", CanBeOptional = true });
GlobalConfig.Add(GlobalConfigProperties.WpfShowTitleAndArea, new ConfigInstruction<GlobalConfig, bool>(config => config.HasWpfShowTitleAndArea = false, (config, value) => config.WpfShowTitleAndArea = value) { Name = "WpfShowTitleAndArea", CanBeOptional = true });

View File

@ -4,9 +4,12 @@ using Newtonsoft.Json.Serialization;
namespace BililiveRecorder.Core.Config
{
#pragma warning disable CS0618 // Type or member is obsolete
[JsonConverter(typeof(JsonSubtypes), nameof(Version))]
[JsonSubtypes.KnownSubType(typeof(V1.ConfigV1Wrapper), 1)]
[JsonSubtypes.KnownSubType(typeof(V2.ConfigV2), 2)]
[JsonSubtypes.KnownSubType(typeof(V3.ConfigV3), 3)]
#pragma warning restore CS0618 // Type or member is obsolete
public abstract class ConfigBase
{
[JsonProperty("$schema", Order = -2)]

View File

@ -3,40 +3,59 @@ using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using BililiveRecorder.Core.Config.V2;
#pragma warning disable CS0612 // obsolete
#pragma warning disable CS0618 // Type or member is obsolete
namespace BililiveRecorder.Core.Config
{
internal static class ConfigMapper
{
private static readonly Dictionary<PropertyInfo, PropertyInfo> Map1to2 = new();
private static readonly Dictionary<PropertyInfo, PropertyInfo> Map2to3GlobalConfig = new();
private static readonly Dictionary<PropertyInfo, PropertyInfo> Map2to3RoomConfig = new();
static ConfigMapper()
{
// Map v1 to v2
{
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(Map1to2, x => x.TimingStreamRetry, x => x.TimingStreamRetry);
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(Map1to2, x => x.TimingStreamConnect, x => x.TimingStreamConnect);
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(Map1to2, x => x.TimingDanmakuRetry, x => x.TimingDanmakuRetry);
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(Map1to2, x => x.TimingCheckInterval, x => x.TimingCheckInterval);
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(Map1to2, x => x.TimingWatchdogTimeout, x => x.TimingWatchdogTimeout);
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(Map1to2, x => x.RecordDanmakuFlushInterval, x => x.RecordDanmakuFlushInterval);
AddMap<V1.ConfigV1, V2.GlobalConfig, string?>(Map1to2, x => x.Cookie, x => x.Cookie);
AddMap<V1.ConfigV1, V2.GlobalConfig, string?>(Map1to2, x => x.WebHookUrls, x => x.WebHookUrls);
AddMap<V1.ConfigV1, V2.GlobalConfig, string?>(Map1to2, x => x.LiveApiHost, x => x.LiveApiHost);
AddMap<V1.ConfigV1, V2.GlobalConfig, string?>(Map1to2, x => x.RecordFilenameFormat, x => x.RecordFilenameFormat);
AddMap<V1.ConfigV1, V2.GlobalConfig, CuttingMode>(Map1to2, x => x.CuttingMode, x => x.CuttingMode);
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(Map1to2, x => x.CuttingNumber, x => x.CuttingNumber);
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(Map1to2, x => x.RecordDanmaku, x => x.RecordDanmaku);
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(Map1to2, x => x.RecordDanmakuRaw, x => x.RecordDanmakuRaw);
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(Map1to2, x => x.RecordDanmakuSuperChat, x => x.RecordDanmakuSuperChat);
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(Map1to2, x => x.RecordDanmakuGift, x => x.RecordDanmakuGift);
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(Map1to2, x => x.RecordDanmakuGuard, x => x.RecordDanmakuGuard);
}
// Map v2 to v3
{
Map2to3GlobalConfig = GetPropertyInfoPairs<V2.GlobalConfig, V3.GlobalConfig>()
.Where(x => x.Key.Name.StartsWith("Optional", StringComparison.Ordinal))
.ToDictionary(x => x.Key, x => x.Value);
Map2to3RoomConfig = GetPropertyInfoPairs<V2.RoomConfig, V3.RoomConfig>()
.Where(x => x.Key.Name.StartsWith("Optional", StringComparison.Ordinal))
.ToDictionary(x => x.Key, x => x.Value);
}
}
public static V2.ConfigV2 Map1To2(V1.ConfigV1 v1)
{
var map = new Dictionary<PropertyInfo, PropertyInfo>();
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.TimingStreamRetry, x => x.TimingStreamRetry);
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.TimingStreamConnect, x => x.TimingStreamConnect);
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.TimingDanmakuRetry, x => x.TimingDanmakuRetry);
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.TimingCheckInterval, x => x.TimingCheckInterval);
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.TimingWatchdogTimeout, x => x.TimingWatchdogTimeout);
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.RecordDanmakuFlushInterval, x => x.RecordDanmakuFlushInterval);
AddMap<V1.ConfigV1, V2.GlobalConfig, string?>(map, x => x.Cookie, x => x.Cookie);
AddMap<V1.ConfigV1, V2.GlobalConfig, string?>(map, x => x.WebHookUrls, x => x.WebHookUrls);
AddMap<V1.ConfigV1, V2.GlobalConfig, string?>(map, x => x.LiveApiHost, x => x.LiveApiHost);
AddMap<V1.ConfigV1, V2.GlobalConfig, string?>(map, x => x.RecordFilenameFormat, x => x.RecordFilenameFormat);
AddMap<V1.ConfigV1, V2.GlobalConfig, CuttingMode>(map, x => x.CuttingMode, x => x.CuttingMode);
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.CuttingNumber, x => x.CuttingNumber);
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(map, x => x.RecordDanmaku, x => x.RecordDanmaku);
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(map, x => x.RecordDanmakuRaw, x => x.RecordDanmakuRaw);
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(map, x => x.RecordDanmakuSuperChat, x => x.RecordDanmakuSuperChat);
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(map, x => x.RecordDanmakuGift, x => x.RecordDanmakuGift);
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(map, x => x.RecordDanmakuGuard, x => x.RecordDanmakuGuard);
var def = new V1.ConfigV1(); // old default
var v2 = new V2.ConfigV2();
foreach (var item in map)
foreach (var item in Map1to2)
{
var data = item.Key.GetValue(v1);
if (!(data?.Equals(item.Key.GetValue(def)) ?? true))
@ -48,6 +67,64 @@ namespace BililiveRecorder.Core.Config
return v2;
}
public static V3.ConfigV3 Map2To3(V2.ConfigV2 v2)
{
var v3 = new V3.ConfigV3();
// 复制没有变动的房间独立设置
foreach (var v2room in v2.Rooms)
{
var v3room = new V3.RoomConfig();
CopyValueWithMap(Map2to3RoomConfig, v2room, v3room);
v3.Rooms.Add(v3room);
}
// 复制没有变动的全局设置
CopyValueWithMap(Map2to3GlobalConfig, v2.Global, v3.Global);
// 转换文件名格式
// 如果用户设置了自定义的文件名格式才需要转换,否则使用全局默认
if (v2.Global.HasRecordFilenameFormat && v2.Global.RecordFilenameFormat is not null)
{
v3.Global.FileNameRecordTemplate = v2.Global.RecordFilenameFormat.Replace("", ""); // TODO
}
throw new NotImplementedException("配置转换还未完成");
return v3;
}
private static void CopyValueWithMap(Dictionary<PropertyInfo, PropertyInfo> map, object source, object target)
{
foreach (var item in map)
{
var data = item.Key.GetValue(source);
item.Value.SetValue(target, data);
}
}
private static List<KeyValuePair<PropertyInfo, PropertyInfo>> GetPropertyInfoPairs<T1, T2>()
where T1 : class
where T2 : class
{
var result = new List<KeyValuePair<PropertyInfo, PropertyInfo>>(32);
var t1Type = typeof(T1);
var t2Type = typeof(T2);
var t1 = t1Type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (var p1 in t1)
{
var p2 = t2Type.GetProperty(p1.Name, BindingFlags.Instance | BindingFlags.Public);
if (p2 != null)
result.Add(new KeyValuePair<PropertyInfo, PropertyInfo>(p1, p2));
}
return result;
}
private static void AddMap<T1, T2, T3>(Dictionary<PropertyInfo, PropertyInfo> map, Expression<Func<T1, T3>> keyExpr, Expression<Func<T2, T3>> valueExpr)
{
var key = GetProperty(keyExpr);

View File

@ -18,7 +18,7 @@ namespace BililiveRecorder.Core.Config
NullValueHandling = NullValueHandling.Ignore
};
public static V2.ConfigV2? LoadFrom(string directory)
public static V3.ConfigV3? LoadFrom(string directory)
{
try
{
@ -30,7 +30,7 @@ namespace BililiveRecorder.Core.Config
if (!File.Exists(filepath))
{
logger.Debug("Config file does not exist {Path}", filepath);
return new V2.ConfigV2();
return new V3.ConfigV3();
}
logger.Debug("Loading config from {Path}", filepath);
@ -45,7 +45,7 @@ namespace BililiveRecorder.Core.Config
}
}
public static V2.ConfigV2? LoadJson(string json)
public static V3.ConfigV3? LoadJson(string json)
{
try
{
@ -61,14 +61,22 @@ namespace BililiveRecorder.Core.Config
var v1Data = JsonConvert.DeserializeObject<V1.ConfigV1>(v1.Data ?? string.Empty);
#pragma warning restore CS0612
if (v1Data is null)
return new V2.ConfigV2();
return new V3.ConfigV3();
var newConfig = ConfigMapper.Map1To2(v1Data);
var newConfig = ConfigMapper.Map2To3(ConfigMapper.Map1To2(v1Data));
return newConfig;
}
#pragma warning disable CS0618 // Type or member is obsolete
case V2.ConfigV2 v2:
#pragma warning restore CS0618 // Type or member is obsolete
logger.Debug("读取到 config v2");
return v2;
return ConfigMapper.Map2To3(v2);
case V3.ConfigV3 v3:
logger.Debug("读取到 config v3");
return v3;
default:
logger.Error("读取到不支持的设置版本");
return null;
@ -81,7 +89,7 @@ namespace BililiveRecorder.Core.Config
}
}
public static bool SaveTo(string directory, V2.ConfigV2 config)
public static bool SaveTo(string directory, V3.ConfigV3 config)
{
if (config.DisableConfigSave)
{
@ -109,7 +117,7 @@ namespace BililiveRecorder.Core.Config
}
}
public static string? SaveJson(V2.ConfigV2 config)
public static string? SaveJson(V3.ConfigV3 config)
{
try
{

View File

@ -1,4 +1,4 @@
namespace BililiveRecorder.Core.Config.V2
namespace BililiveRecorder.Core.Config
{
public enum CuttingMode : int
{

View File

@ -1,4 +1,4 @@
namespace BililiveRecorder.Core.Config.V2
namespace BililiveRecorder.Core.Config
{
public enum RecordMode : int
{

View File

@ -41,7 +41,7 @@ namespace BililiveRecorder.Core.Config.V1
/// 自动切割模式
/// </summary>
[JsonProperty("cutting_mode")]
public V2.CuttingMode CuttingMode { get => this._cuttingMode; set => this.SetField(ref this._cuttingMode, value); }
public CuttingMode CuttingMode { get => this._cuttingMode; set => this.SetField(ref this._cuttingMode, value); }
/// <summary>
/// 自动切割数值(分钟/MiB
@ -166,7 +166,7 @@ namespace BililiveRecorder.Core.Config.V1
private uint _clipLengthFuture = 10;
private uint _cuttingNumber = 10;
//private EnabledFeature _enabledFeature = EnabledFeature.RecordOnly;
private V2.CuttingMode _cuttingMode = V2.CuttingMode.Disabled;
private CuttingMode _cuttingMode = CuttingMode.Disabled;
private uint _timingWatchdogTimeout = 10 * 1000;
private uint _timingStreamRetry = 6 * 1000;

View File

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
#nullable enable
namespace BililiveRecorder.Core.Config.V2
{
[Obsolete("Use Config v3")]
public class ConfigV2 : ConfigBase
{
public override int Version => 2;
@ -18,6 +20,7 @@ namespace BililiveRecorder.Core.Config.V2
public bool DisableConfigSave { get; set; } = false; // for CLI
}
[Obsolete("Use Config v3")]
public partial class RoomConfig
{
public RoomConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name }))
@ -28,6 +31,7 @@ namespace BililiveRecorder.Core.Config.V2
public string? WorkDirectory => this.GetPropertyValue<string>();
}
[Obsolete("Use Config v3")]
public partial class GlobalConfig
{
public GlobalConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name }))

View File

@ -0,0 +1,403 @@
// ******************************
// GENERATED CODE, DO NOT EDIT MANUALLY.
// SEE /config_gen/README.md
// ******************************
using System.ComponentModel;
using HierarchicalPropertyDefault;
using Newtonsoft.Json;
#nullable enable
namespace BililiveRecorder.Core.Config.V3
{
[JsonObject(MemberSerialization.OptIn)]
public sealed partial class RoomConfig : HierarchicalObject<GlobalConfig, RoomConfig>
{
/// <summary>
/// 房间号
/// </summary>
public int RoomId { get => this.GetPropertyValue<int>(); set => this.SetPropertyValue(value); }
public bool HasRoomId { get => this.GetPropertyHasValue(nameof(this.RoomId)); set => this.SetPropertyHasValue<int>(value, nameof(this.RoomId)); }
[JsonProperty(nameof(RoomId)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<int> OptionalRoomId { get => this.GetPropertyValueOptional<int>(nameof(this.RoomId)); set => this.SetPropertyValueOptional(value, nameof(this.RoomId)); }
/// <summary>
/// 自动录制
/// </summary>
public bool AutoRecord { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
public bool HasAutoRecord { get => this.GetPropertyHasValue(nameof(this.AutoRecord)); set => this.SetPropertyHasValue<bool>(value, nameof(this.AutoRecord)); }
[JsonProperty(nameof(AutoRecord)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalAutoRecord { get => this.GetPropertyValueOptional<bool>(nameof(this.AutoRecord)); set => this.SetPropertyValueOptional(value, nameof(this.AutoRecord)); }
/// <summary>
/// 录制模式
/// </summary>
public RecordMode RecordMode { get => this.GetPropertyValue<RecordMode>(); set => this.SetPropertyValue(value); }
public bool HasRecordMode { get => this.GetPropertyHasValue(nameof(this.RecordMode)); set => this.SetPropertyHasValue<RecordMode>(value, nameof(this.RecordMode)); }
[JsonProperty(nameof(RecordMode)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<RecordMode> OptionalRecordMode { get => this.GetPropertyValueOptional<RecordMode>(nameof(this.RecordMode)); set => this.SetPropertyValueOptional(value, nameof(this.RecordMode)); }
/// <summary>
/// 自动分段模式
/// </summary>
public CuttingMode CuttingMode { get => this.GetPropertyValue<CuttingMode>(); set => this.SetPropertyValue(value); }
public bool HasCuttingMode { get => this.GetPropertyHasValue(nameof(this.CuttingMode)); set => this.SetPropertyHasValue<CuttingMode>(value, nameof(this.CuttingMode)); }
[JsonProperty(nameof(CuttingMode)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<CuttingMode> OptionalCuttingMode { get => this.GetPropertyValueOptional<CuttingMode>(nameof(this.CuttingMode)); set => this.SetPropertyValueOptional(value, nameof(this.CuttingMode)); }
/// <summary>
/// 自动分段数值
/// </summary>
public uint CuttingNumber { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
public bool HasCuttingNumber { get => this.GetPropertyHasValue(nameof(this.CuttingNumber)); set => this.SetPropertyHasValue<uint>(value, nameof(this.CuttingNumber)); }
[JsonProperty(nameof(CuttingNumber)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<uint> OptionalCuttingNumber { get => this.GetPropertyValueOptional<uint>(nameof(this.CuttingNumber)); set => this.SetPropertyValueOptional(value, nameof(this.CuttingNumber)); }
/// <summary>
/// 弹幕录制
/// </summary>
public bool RecordDanmaku { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
public bool HasRecordDanmaku { get => this.GetPropertyHasValue(nameof(this.RecordDanmaku)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmaku)); }
[JsonProperty(nameof(RecordDanmaku)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalRecordDanmaku { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmaku)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmaku)); }
/// <summary>
/// 弹幕录制-原始数据
/// </summary>
public bool RecordDanmakuRaw { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
public bool HasRecordDanmakuRaw { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuRaw)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuRaw)); }
[JsonProperty(nameof(RecordDanmakuRaw)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalRecordDanmakuRaw { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuRaw)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuRaw)); }
/// <summary>
/// 弹幕录制-SuperChat
/// </summary>
public bool RecordDanmakuSuperChat { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
public bool HasRecordDanmakuSuperChat { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuSuperChat)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuSuperChat)); }
[JsonProperty(nameof(RecordDanmakuSuperChat)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalRecordDanmakuSuperChat { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuSuperChat)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuSuperChat)); }
/// <summary>
/// 弹幕录制-礼物
/// </summary>
public bool RecordDanmakuGift { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
public bool HasRecordDanmakuGift { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuGift)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuGift)); }
[JsonProperty(nameof(RecordDanmakuGift)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalRecordDanmakuGift { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuGift)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGift)); }
/// <summary>
/// 弹幕录制-上船
/// </summary>
public bool RecordDanmakuGuard { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
public bool HasRecordDanmakuGuard { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuGuard)); }
[JsonProperty(nameof(RecordDanmakuGuard)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalRecordDanmakuGuard { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGuard)); }
/// <summary>
/// 直播画质
/// </summary>
public string? RecordingQuality { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
public bool HasRecordingQuality { get => this.GetPropertyHasValue(nameof(this.RecordingQuality)); set => this.SetPropertyHasValue<string>(value, nameof(this.RecordingQuality)); }
[JsonProperty(nameof(RecordingQuality)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<string?> OptionalRecordingQuality { get => this.GetPropertyValueOptional<string>(nameof(this.RecordingQuality)); set => this.SetPropertyValueOptional(value, nameof(this.RecordingQuality)); }
/// <summary>
/// 录制文件名模板
/// </summary>
public string? FileNameRecordTemplate => this.GetPropertyValue<string>();
/// <summary>
/// 录制文件写入结束 Webhook 地址 每行一个
/// </summary>
public string? WebHookUrls => this.GetPropertyValue<string>();
/// <summary>
/// Webhook v2 地址 每行一个
/// </summary>
public string? WebHookUrlsV2 => this.GetPropertyValue<string>();
/// <summary>
/// 在界面显示标题和分区
/// </summary>
public bool WpfShowTitleAndArea => this.GetPropertyValue<bool>();
/// <summary>
/// 请求 API 时使用的 Cookie
/// </summary>
public string? Cookie => this.GetPropertyValue<string>();
/// <summary>
/// 替换 api.live.bilibili.com 服务器为其他反代,可以支持在云服务器上录制
/// </summary>
public string? LiveApiHost => this.GetPropertyValue<string>();
/// <summary>
/// HTTP API 检查时间间隔 秒
/// </summary>
public uint TimingCheckInterval => this.GetPropertyValue<uint>();
/// <summary>
/// 录制断开重连时间间隔 毫秒
/// </summary>
public uint TimingStreamRetry => this.GetPropertyValue<uint>();
/// <summary>
/// 录制无指定画质重连时间间隔 秒
/// </summary>
public uint TimingStreamRetryNoQn => this.GetPropertyValue<uint>();
/// <summary>
/// 连接直播服务器超时时间 毫秒
/// </summary>
public uint TimingStreamConnect => this.GetPropertyValue<uint>();
/// <summary>
/// 弹幕服务器重连时间间隔 毫秒
/// </summary>
public uint TimingDanmakuRetry => this.GetPropertyValue<uint>();
/// <summary>
/// 最大允许未收到直播数据时间 毫秒
/// </summary>
public uint TimingWatchdogTimeout => this.GetPropertyValue<uint>();
/// <summary>
/// 触发 <see cref="System.Xml.XmlWriter.Flush"/> 的弹幕个数
/// </summary>
public uint RecordDanmakuFlushInterval => this.GetPropertyValue<uint>();
}
[JsonObject(MemberSerialization.OptIn)]
public sealed partial class GlobalConfig : HierarchicalObject<DefaultConfig, GlobalConfig>
{
/// <summary>
/// 录制模式
/// </summary>
public RecordMode RecordMode { get => this.GetPropertyValue<RecordMode>(); set => this.SetPropertyValue(value); }
public bool HasRecordMode { get => this.GetPropertyHasValue(nameof(this.RecordMode)); set => this.SetPropertyHasValue<RecordMode>(value, nameof(this.RecordMode)); }
[JsonProperty(nameof(RecordMode)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<RecordMode> OptionalRecordMode { get => this.GetPropertyValueOptional<RecordMode>(nameof(this.RecordMode)); set => this.SetPropertyValueOptional(value, nameof(this.RecordMode)); }
/// <summary>
/// 自动分段模式
/// </summary>
public CuttingMode CuttingMode { get => this.GetPropertyValue<CuttingMode>(); set => this.SetPropertyValue(value); }
public bool HasCuttingMode { get => this.GetPropertyHasValue(nameof(this.CuttingMode)); set => this.SetPropertyHasValue<CuttingMode>(value, nameof(this.CuttingMode)); }
[JsonProperty(nameof(CuttingMode)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<CuttingMode> OptionalCuttingMode { get => this.GetPropertyValueOptional<CuttingMode>(nameof(this.CuttingMode)); set => this.SetPropertyValueOptional(value, nameof(this.CuttingMode)); }
/// <summary>
/// 自动分段数值
/// </summary>
public uint CuttingNumber { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
public bool HasCuttingNumber { get => this.GetPropertyHasValue(nameof(this.CuttingNumber)); set => this.SetPropertyHasValue<uint>(value, nameof(this.CuttingNumber)); }
[JsonProperty(nameof(CuttingNumber)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<uint> OptionalCuttingNumber { get => this.GetPropertyValueOptional<uint>(nameof(this.CuttingNumber)); set => this.SetPropertyValueOptional(value, nameof(this.CuttingNumber)); }
/// <summary>
/// 弹幕录制
/// </summary>
public bool RecordDanmaku { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
public bool HasRecordDanmaku { get => this.GetPropertyHasValue(nameof(this.RecordDanmaku)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmaku)); }
[JsonProperty(nameof(RecordDanmaku)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalRecordDanmaku { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmaku)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmaku)); }
/// <summary>
/// 弹幕录制-原始数据
/// </summary>
public bool RecordDanmakuRaw { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
public bool HasRecordDanmakuRaw { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuRaw)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuRaw)); }
[JsonProperty(nameof(RecordDanmakuRaw)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalRecordDanmakuRaw { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuRaw)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuRaw)); }
/// <summary>
/// 弹幕录制-SuperChat
/// </summary>
public bool RecordDanmakuSuperChat { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
public bool HasRecordDanmakuSuperChat { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuSuperChat)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuSuperChat)); }
[JsonProperty(nameof(RecordDanmakuSuperChat)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalRecordDanmakuSuperChat { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuSuperChat)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuSuperChat)); }
/// <summary>
/// 弹幕录制-礼物
/// </summary>
public bool RecordDanmakuGift { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
public bool HasRecordDanmakuGift { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuGift)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuGift)); }
[JsonProperty(nameof(RecordDanmakuGift)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalRecordDanmakuGift { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuGift)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGift)); }
/// <summary>
/// 弹幕录制-上船
/// </summary>
public bool RecordDanmakuGuard { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
public bool HasRecordDanmakuGuard { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuGuard)); }
[JsonProperty(nameof(RecordDanmakuGuard)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalRecordDanmakuGuard { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGuard)); }
/// <summary>
/// 直播画质
/// </summary>
public string? RecordingQuality { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
public bool HasRecordingQuality { get => this.GetPropertyHasValue(nameof(this.RecordingQuality)); set => this.SetPropertyHasValue<string>(value, nameof(this.RecordingQuality)); }
[JsonProperty(nameof(RecordingQuality)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<string?> OptionalRecordingQuality { get => this.GetPropertyValueOptional<string>(nameof(this.RecordingQuality)); set => this.SetPropertyValueOptional(value, nameof(this.RecordingQuality)); }
/// <summary>
/// 录制文件名模板
/// </summary>
public string? FileNameRecordTemplate { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
public bool HasFileNameRecordTemplate { get => this.GetPropertyHasValue(nameof(this.FileNameRecordTemplate)); set => this.SetPropertyHasValue<string>(value, nameof(this.FileNameRecordTemplate)); }
[JsonProperty(nameof(FileNameRecordTemplate)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<string?> OptionalFileNameRecordTemplate { get => this.GetPropertyValueOptional<string>(nameof(this.FileNameRecordTemplate)); set => this.SetPropertyValueOptional(value, nameof(this.FileNameRecordTemplate)); }
/// <summary>
/// 录制文件写入结束 Webhook 地址 每行一个
/// </summary>
public string? WebHookUrls { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
public bool HasWebHookUrls { get => this.GetPropertyHasValue(nameof(this.WebHookUrls)); set => this.SetPropertyHasValue<string>(value, nameof(this.WebHookUrls)); }
[JsonProperty(nameof(WebHookUrls)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<string?> OptionalWebHookUrls { get => this.GetPropertyValueOptional<string>(nameof(this.WebHookUrls)); set => this.SetPropertyValueOptional(value, nameof(this.WebHookUrls)); }
/// <summary>
/// Webhook v2 地址 每行一个
/// </summary>
public string? WebHookUrlsV2 { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
public bool HasWebHookUrlsV2 { get => this.GetPropertyHasValue(nameof(this.WebHookUrlsV2)); set => this.SetPropertyHasValue<string>(value, nameof(this.WebHookUrlsV2)); }
[JsonProperty(nameof(WebHookUrlsV2)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<string?> OptionalWebHookUrlsV2 { get => this.GetPropertyValueOptional<string>(nameof(this.WebHookUrlsV2)); set => this.SetPropertyValueOptional(value, nameof(this.WebHookUrlsV2)); }
/// <summary>
/// 在界面显示标题和分区
/// </summary>
public bool WpfShowTitleAndArea { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
public bool HasWpfShowTitleAndArea { get => this.GetPropertyHasValue(nameof(this.WpfShowTitleAndArea)); set => this.SetPropertyHasValue<bool>(value, nameof(this.WpfShowTitleAndArea)); }
[JsonProperty(nameof(WpfShowTitleAndArea)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalWpfShowTitleAndArea { get => this.GetPropertyValueOptional<bool>(nameof(this.WpfShowTitleAndArea)); set => this.SetPropertyValueOptional(value, nameof(this.WpfShowTitleAndArea)); }
/// <summary>
/// 请求 API 时使用的 Cookie
/// </summary>
public string? Cookie { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
public bool HasCookie { get => this.GetPropertyHasValue(nameof(this.Cookie)); set => this.SetPropertyHasValue<string>(value, nameof(this.Cookie)); }
[JsonProperty(nameof(Cookie)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<string?> OptionalCookie { get => this.GetPropertyValueOptional<string>(nameof(this.Cookie)); set => this.SetPropertyValueOptional(value, nameof(this.Cookie)); }
/// <summary>
/// 替换 api.live.bilibili.com 服务器为其他反代,可以支持在云服务器上录制
/// </summary>
public string? LiveApiHost { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
public bool HasLiveApiHost { get => this.GetPropertyHasValue(nameof(this.LiveApiHost)); set => this.SetPropertyHasValue<string>(value, nameof(this.LiveApiHost)); }
[JsonProperty(nameof(LiveApiHost)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<string?> OptionalLiveApiHost { get => this.GetPropertyValueOptional<string>(nameof(this.LiveApiHost)); set => this.SetPropertyValueOptional(value, nameof(this.LiveApiHost)); }
/// <summary>
/// HTTP API 检查时间间隔 秒
/// </summary>
public uint TimingCheckInterval { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
public bool HasTimingCheckInterval { get => this.GetPropertyHasValue(nameof(this.TimingCheckInterval)); set => this.SetPropertyHasValue<uint>(value, nameof(this.TimingCheckInterval)); }
[JsonProperty(nameof(TimingCheckInterval)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<uint> OptionalTimingCheckInterval { get => this.GetPropertyValueOptional<uint>(nameof(this.TimingCheckInterval)); set => this.SetPropertyValueOptional(value, nameof(this.TimingCheckInterval)); }
/// <summary>
/// 录制断开重连时间间隔 毫秒
/// </summary>
public uint TimingStreamRetry { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
public bool HasTimingStreamRetry { get => this.GetPropertyHasValue(nameof(this.TimingStreamRetry)); set => this.SetPropertyHasValue<uint>(value, nameof(this.TimingStreamRetry)); }
[JsonProperty(nameof(TimingStreamRetry)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<uint> OptionalTimingStreamRetry { get => this.GetPropertyValueOptional<uint>(nameof(this.TimingStreamRetry)); set => this.SetPropertyValueOptional(value, nameof(this.TimingStreamRetry)); }
/// <summary>
/// 录制无指定画质重连时间间隔 秒
/// </summary>
public uint TimingStreamRetryNoQn { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
public bool HasTimingStreamRetryNoQn { get => this.GetPropertyHasValue(nameof(this.TimingStreamRetryNoQn)); set => this.SetPropertyHasValue<uint>(value, nameof(this.TimingStreamRetryNoQn)); }
[JsonProperty(nameof(TimingStreamRetryNoQn)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<uint> OptionalTimingStreamRetryNoQn { get => this.GetPropertyValueOptional<uint>(nameof(this.TimingStreamRetryNoQn)); set => this.SetPropertyValueOptional(value, nameof(this.TimingStreamRetryNoQn)); }
/// <summary>
/// 连接直播服务器超时时间 毫秒
/// </summary>
public uint TimingStreamConnect { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
public bool HasTimingStreamConnect { get => this.GetPropertyHasValue(nameof(this.TimingStreamConnect)); set => this.SetPropertyHasValue<uint>(value, nameof(this.TimingStreamConnect)); }
[JsonProperty(nameof(TimingStreamConnect)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<uint> OptionalTimingStreamConnect { get => this.GetPropertyValueOptional<uint>(nameof(this.TimingStreamConnect)); set => this.SetPropertyValueOptional(value, nameof(this.TimingStreamConnect)); }
/// <summary>
/// 弹幕服务器重连时间间隔 毫秒
/// </summary>
public uint TimingDanmakuRetry { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
public bool HasTimingDanmakuRetry { get => this.GetPropertyHasValue(nameof(this.TimingDanmakuRetry)); set => this.SetPropertyHasValue<uint>(value, nameof(this.TimingDanmakuRetry)); }
[JsonProperty(nameof(TimingDanmakuRetry)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<uint> OptionalTimingDanmakuRetry { get => this.GetPropertyValueOptional<uint>(nameof(this.TimingDanmakuRetry)); set => this.SetPropertyValueOptional(value, nameof(this.TimingDanmakuRetry)); }
/// <summary>
/// 最大允许未收到直播数据时间 毫秒
/// </summary>
public uint TimingWatchdogTimeout { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
public bool HasTimingWatchdogTimeout { get => this.GetPropertyHasValue(nameof(this.TimingWatchdogTimeout)); set => this.SetPropertyHasValue<uint>(value, nameof(this.TimingWatchdogTimeout)); }
[JsonProperty(nameof(TimingWatchdogTimeout)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<uint> OptionalTimingWatchdogTimeout { get => this.GetPropertyValueOptional<uint>(nameof(this.TimingWatchdogTimeout)); set => this.SetPropertyValueOptional(value, nameof(this.TimingWatchdogTimeout)); }
/// <summary>
/// 触发 <see cref="System.Xml.XmlWriter.Flush"/> 的弹幕个数
/// </summary>
public uint RecordDanmakuFlushInterval { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
public bool HasRecordDanmakuFlushInterval { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuFlushInterval)); set => this.SetPropertyHasValue<uint>(value, nameof(this.RecordDanmakuFlushInterval)); }
[JsonProperty(nameof(RecordDanmakuFlushInterval)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<uint> OptionalRecordDanmakuFlushInterval { get => this.GetPropertyValueOptional<uint>(nameof(this.RecordDanmakuFlushInterval)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuFlushInterval)); }
}
public sealed partial class DefaultConfig
{
public static readonly DefaultConfig Instance = new DefaultConfig();
private DefaultConfig() { }
public RecordMode RecordMode => RecordMode.Standard;
public CuttingMode CuttingMode => CuttingMode.Disabled;
public uint CuttingNumber => 100;
public bool RecordDanmaku => false;
public bool RecordDanmakuRaw => false;
public bool RecordDanmakuSuperChat => true;
public bool RecordDanmakuGift => false;
public bool RecordDanmakuGuard => true;
public string RecordingQuality => "10000";
public string FileNameRecordTemplate => @"{roomid}-{name}/录制-{roomid}-{date}-{time}-{ms}-{title}.flv";
public string WebHookUrls => string.Empty;
public string WebHookUrlsV2 => string.Empty;
public bool WpfShowTitleAndArea => true;
public string Cookie => string.Empty;
public string LiveApiHost => "https://api.live.bilibili.com";
public uint TimingCheckInterval => 10 * 60;
public uint TimingStreamRetry => 6 * 1000;
public uint TimingStreamRetryNoQn => 90;
public uint TimingStreamConnect => 5 * 1000;
public uint TimingDanmakuRetry => 9 * 1000;
public uint TimingWatchdogTimeout => 10 * 1000;
public uint RecordDanmakuFlushInterval => 20;
}
}

View File

@ -0,0 +1,47 @@
using System.Collections.Generic;
using Newtonsoft.Json;
#nullable enable
namespace BililiveRecorder.Core.Config.V3
{
public class ConfigV3 : ConfigBase
{
public override int Version => 3;
[JsonProperty("global")]
public GlobalConfig Global { get; set; } = new GlobalConfig();
[JsonProperty("rooms")]
public List<RoomConfig> Rooms { get; set; } = new List<RoomConfig>();
[JsonIgnore]
public bool DisableConfigSave { get; set; } = false; // for CLI
}
public partial class RoomConfig
{
public RoomConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name }))
{ }
internal void SetParent(GlobalConfig? config) => this.Parent = config;
public string? WorkDirectory => this.GetPropertyValue<string>();
}
public partial class GlobalConfig
{
public GlobalConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name }))
{
this.Parent = DefaultConfig.Instance;
}
/// <summary>
/// 当前工作目录
/// </summary>
public string? WorkDirectory
{
get => this.GetPropertyValue<string>();
set => this.SetPropertyValue(value);
}
}
}

View File

@ -1,5 +1,5 @@
using System;
using BililiveRecorder.Core.Config.V2;
using BililiveRecorder.Core.Config;
using Microsoft.Extensions.DependencyInjection;
using Serilog;

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BililiveRecorder.Core.Api;
using BililiveRecorder.Core.Config;
using BililiveRecorder.Core.Event;
using BililiveRecorder.Core.ProcessingRules;
using BililiveRecorder.Flv;
@ -239,11 +240,11 @@ namespace BililiveRecorder.Core.Recording
{
switch (this.room.RoomConfig.CuttingMode)
{
case Config.V2.CuttingMode.ByTime:
case CuttingMode.ByTime:
if (e.FileMaxTimestamp > this.room.RoomConfig.CuttingNumber * (60u * 1000u))
this.splitFileRule.SetSplitBeforeFlag();
break;
case Config.V2.CuttingMode.BySize:
case CuttingMode.BySize:
if ((e.CurrentFileSize + (e.OutputVideoByteCount * 1.1) + e.OutputAudioByteCount) / (1024d * 1024d) > this.room.RoomConfig.CuttingNumber)
this.splitFileRule.SetSplitBeforeFlag();
break;

611
configV3.schema.json Normal file
View File

@ -0,0 +1,611 @@
{
"$comment": "GENERATED CODE, DO NOT EDIT MANUALLY.",
"$schema": "http://json-schema.org/schema",
"definitions": {
"global-config": {
"description": "全局设置",
"additionalProperties": false,
"properties": {
"FileNameRecordTemplate": {
"description": "录制文件名模板\n默认: {roomid}-{name}/录制-{roomid}-{date}-{time}-{ms}-{title}.flv",
"markdownDescription": "录制文件名模板 \n默认: `{roomid}-{name}/录制-{roomid}-{date}-{time}-{ms}-{title}.flv `\n\nTODO: config v3 新的文件名模板系统的文档还没有写",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "string",
"default": "{roomid}-{name}/录制-{roomid}-{date}-{time}-{ms}-{title}.flv"
}
}
},
"WebHookUrls": {
"description": "WebhookV1\n默认: ",
"markdownDescription": "WebhookV1 \n默认: ` `\n\n具体文档见 [Webhook](/docs/basic/webhook/)",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "string",
"default": ""
}
}
},
"WebHookUrlsV2": {
"description": "WebhookV2\n默认: ",
"markdownDescription": "WebhookV2 \n默认: ` `\n\n具体文档见 [Webhook](/docs/basic/webhook/)",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "string",
"default": ""
}
}
},
"WpfShowTitleAndArea": {
"description": "在界面显示标题和分区\n默认: true",
"markdownDescription": "在界面显示标题和分区 \n默认: `true `\n\n只在桌面版WPF版有效",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "boolean",
"default": true
}
}
},
"Cookie": {
"description": "请求 API 时使用的 Cookie\n默认: (空字符串)",
"markdownDescription": "请求 API 时使用的 Cookie \n默认: `(空字符串) `\n\n",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "string",
"pattern": "^(S+=S+;? ?)*$",
"maxLength": 4096
}
}
},
"LiveApiHost": {
"description": "请求的 API Host\n默认: https://api.live.bilibili.com",
"markdownDescription": "请求的 API Host \n默认: `https://api.live.bilibili.com `\n\n",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "string",
"default": "https://api.live.bilibili.com"
}
}
},
"TimingCheckInterval": {
"description": "HTTP API 检查时间间隔 秒\n默认: 600 (10分)",
"markdownDescription": "HTTP API 检查时间间隔 秒 \n默认: `600 (10分) `\n\n",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"default": 600
}
}
},
"TimingStreamRetry": {
"description": "录制断开重连时间间隔 毫秒\n默认: 6000 (6秒)",
"markdownDescription": "录制断开重连时间间隔 毫秒 \n默认: `6000 (6秒) `\n\n",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"default": 6000
}
}
},
"TimingStreamRetryNoQn": {
"description": "录制无指定画质重连时间间隔 秒\n默认: 90 (1.5分钟)",
"markdownDescription": "录制无指定画质重连时间间隔 秒 \n默认: `90 (1.5分钟) `\n\n",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"default": 90
}
}
},
"TimingStreamConnect": {
"description": "连接直播服务器超时时间 毫秒\n默认: 5000 (5秒)",
"markdownDescription": "连接直播服务器超时时间 毫秒 \n默认: `5000 (5秒) `\n\n",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"default": 5000
}
}
},
"TimingDanmakuRetry": {
"description": "弹幕服务器重连时间间隔 毫秒\n默认: 9000 (9秒)",
"markdownDescription": "弹幕服务器重连时间间隔 毫秒 \n默认: `9000 (9秒) `\n\n",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"default": 9000
}
}
},
"TimingWatchdogTimeout": {
"description": "最大允许未收到直播数据时间 毫秒\n默认: 10000 (10秒)",
"markdownDescription": "最大允许未收到直播数据时间 毫秒 \n默认: `10000 (10秒) `\n\n",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"default": 10000
}
}
},
"RecordDanmakuFlushInterval": {
"description": "触发刷新弹幕写入缓冲的个数\n默认: 20",
"markdownDescription": "触发刷新弹幕写入缓冲的个数 \n默认: `20 `\n\n",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"default": 20
}
}
},
"RecordMode": {
"description": "录制模式\n默认: RecordMode.Standard",
"markdownDescription": "录制模式 \n默认: `RecordMode.Standard `\n\n本设置项是一个 enum键值对应如下\n\n| 键 | 值 |\n|:--:|:--:|\n| RecordMode.Standard | 0 |\n| RecordMode.RawData | 1 |\n\n关于录制模式的说明见 [录制模式](/docs/basic/record_mode/)",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"default": 0,
"enum": [
0,
1
],
"description": "0: Standard\n1: Raw"
}
}
},
"CuttingMode": {
"description": "自动分段模式\n默认: CuttingMode.Disabled",
"markdownDescription": "自动分段模式 \n默认: `CuttingMode.Disabled `\n\n本设置项是一个 enum键值对应如下\n\n| 键 | 值 |\n|:--:|:--:|\n| CuttingMode.Disabled | 0 |\n| CuttingMode.ByTime | 1 |\n| CuttingMode.BySize | 2 |",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"default": 0,
"enum": [
0,
1,
2
],
"description": "0: 禁用\n1: 根据时间切割\n2: 根据文件大小切割"
}
}
},
"CuttingNumber": {
"description": "自动分段数值\n默认: 100",
"markdownDescription": "自动分段数值 \n默认: `100 `\n\n根据 CuttingMode 设置的不同: \n当按时长分段时本设置的单位为分钟。 \n当按大小分段时本设置的单位为MiB。",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"default": 100
}
}
},
"RecordDanmaku": {
"description": "弹幕录制\n默认: false",
"markdownDescription": "弹幕录制 \n默认: `false `\n\n是否录制弹幕`true` 为录制,`false` 为不录制。\n\n本设置同时是所有“弹幕录制”的总开关当本设置为 `false` 时其他所有“弹幕录制”设置无效不会写入弹幕XML文件。",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "boolean",
"default": false
}
}
},
"RecordDanmakuRaw": {
"description": "弹幕录制-原始数据\n默认: false",
"markdownDescription": "弹幕录制-原始数据 \n默认: `false `\n\n是否记录原始 JSON 数据。\n\n弹幕原始数据会保存到 XML 文件每一条弹幕数据的 `raw` attribute 上。\n\n当 `RecordDanmaku` 为 `false` 时本项设置无效。",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "boolean",
"default": false
}
}
},
"RecordDanmakuSuperChat": {
"description": "弹幕录制-SuperChat\n默认: true",
"markdownDescription": "弹幕录制-SuperChat \n默认: `true `\n\n是否记录 SuperChat。\n\n当 `RecordDanmaku` 为 `false` 时本项设置无效。",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "boolean",
"default": true
}
}
},
"RecordDanmakuGift": {
"description": "弹幕录制-礼物\n默认: false",
"markdownDescription": "弹幕录制-礼物 \n默认: `false `\n\n是否记录礼物。\n\n当 `RecordDanmaku` 为 `false` 时本项设置无效。",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "boolean",
"default": false
}
}
},
"RecordDanmakuGuard": {
"description": "弹幕录制-上船\n默认: true",
"markdownDescription": "弹幕录制-上船 \n默认: `true `\n\n是否记录上船购买舰长。\n\n当 `RecordDanmaku` 为 `false` 时本项设置无效。",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "boolean",
"default": true
}
}
},
"RecordingQuality": {
"description": "直播画质\n默认: 10000",
"markdownDescription": "直播画质 \n默认: `10000 `\n\n录制的直播画质 qn 值,以英文逗号分割,靠前的优先。\n\n**注意**(从录播姬 1.3.10 开始):\n\n- 所有主播刚开播时都是只有“原画”的,如果选择不录原画会导致直播开头漏录。\n- 如果设置的录制画质里没有原画,但是主播只有原画画质,会导致不能录制直播。\n- 录播姬不会为了切换录制的画质主动断开录制。\n\n画质 | qn 值\n:--:|:--:\n4K | 20000\n原画 | 10000\n蓝光(杜比) | 401\n蓝光 | 400\n超清 | 250\n高清 | 150\n流畅 | 80",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "string",
"default": "10000"
}
}
}
}
},
"room-config": {
"description": "房间独立设置",
"additionalProperties": false,
"properties": {
"RoomId": {
"description": "房间号\n默认: default",
"markdownDescription": "房间号 \n默认: `default `\n\n",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647
}
}
},
"AutoRecord": {
"description": "自动录制\n默认: default",
"markdownDescription": "自动录制 \n默认: `default `\n\n设为 `true` 为启用自动录制,`false` 为不自动录制。",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "boolean"
}
}
},
"RecordMode": {
"description": "录制模式\n默认: RecordMode.Standard",
"markdownDescription": "录制模式 \n默认: `RecordMode.Standard `\n\n本设置项是一个 enum键值对应如下\n\n| 键 | 值 |\n|:--:|:--:|\n| RecordMode.Standard | 0 |\n| RecordMode.RawData | 1 |\n\n关于录制模式的说明见 [录制模式](/docs/basic/record_mode/)",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"default": 0,
"enum": [
0,
1
],
"description": "0: Standard\n1: Raw"
}
}
},
"CuttingMode": {
"description": "自动分段模式\n默认: CuttingMode.Disabled",
"markdownDescription": "自动分段模式 \n默认: `CuttingMode.Disabled `\n\n本设置项是一个 enum键值对应如下\n\n| 键 | 值 |\n|:--:|:--:|\n| CuttingMode.Disabled | 0 |\n| CuttingMode.ByTime | 1 |\n| CuttingMode.BySize | 2 |",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"default": 0,
"enum": [
0,
1,
2
],
"description": "0: 禁用\n1: 根据时间切割\n2: 根据文件大小切割"
}
}
},
"CuttingNumber": {
"description": "自动分段数值\n默认: 100",
"markdownDescription": "自动分段数值 \n默认: `100 `\n\n根据 CuttingMode 设置的不同: \n当按时长分段时本设置的单位为分钟。 \n当按大小分段时本设置的单位为MiB。",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"default": 100
}
}
},
"RecordDanmaku": {
"description": "弹幕录制\n默认: false",
"markdownDescription": "弹幕录制 \n默认: `false `\n\n是否录制弹幕`true` 为录制,`false` 为不录制。\n\n本设置同时是所有“弹幕录制”的总开关当本设置为 `false` 时其他所有“弹幕录制”设置无效不会写入弹幕XML文件。",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "boolean",
"default": false
}
}
},
"RecordDanmakuRaw": {
"description": "弹幕录制-原始数据\n默认: false",
"markdownDescription": "弹幕录制-原始数据 \n默认: `false `\n\n是否记录原始 JSON 数据。\n\n弹幕原始数据会保存到 XML 文件每一条弹幕数据的 `raw` attribute 上。\n\n当 `RecordDanmaku` 为 `false` 时本项设置无效。",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "boolean",
"default": false
}
}
},
"RecordDanmakuSuperChat": {
"description": "弹幕录制-SuperChat\n默认: true",
"markdownDescription": "弹幕录制-SuperChat \n默认: `true `\n\n是否记录 SuperChat。\n\n当 `RecordDanmaku` 为 `false` 时本项设置无效。",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "boolean",
"default": true
}
}
},
"RecordDanmakuGift": {
"description": "弹幕录制-礼物\n默认: false",
"markdownDescription": "弹幕录制-礼物 \n默认: `false `\n\n是否记录礼物。\n\n当 `RecordDanmaku` 为 `false` 时本项设置无效。",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "boolean",
"default": false
}
}
},
"RecordDanmakuGuard": {
"description": "弹幕录制-上船\n默认: true",
"markdownDescription": "弹幕录制-上船 \n默认: `true `\n\n是否记录上船购买舰长。\n\n当 `RecordDanmaku` 为 `false` 时本项设置无效。",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "boolean",
"default": true
}
}
},
"RecordingQuality": {
"description": "直播画质\n默认: 10000",
"markdownDescription": "直播画质 \n默认: `10000 `\n\n录制的直播画质 qn 值,以英文逗号分割,靠前的优先。\n\n**注意**(从录播姬 1.3.10 开始):\n\n- 所有主播刚开播时都是只有“原画”的,如果选择不录原画会导致直播开头漏录。\n- 如果设置的录制画质里没有原画,但是主播只有原画画质,会导致不能录制直播。\n- 录播姬不会为了切换录制的画质主动断开录制。\n\n画质 | qn 值\n:--:|:--:\n4K | 20000\n原画 | 10000\n蓝光(杜比) | 401\n蓝光 | 400\n超清 | 250\n高清 | 150\n流畅 | 80",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "string",
"default": "10000"
}
}
}
}
}
},
"type": "object",
"additionalProperties": false,
"required": [
"$schema",
"version"
],
"properties": {
"$schema": {
"type": "string",
"default": "https://raw.githubusercontent.com/Bililive/BililiveRecorder/dev-1.3/configV2.schema.json"
},
"version": {
"const": 2
},
"global": {
"$ref": "#/definitions/global-config"
},
"rooms": {
"type": "array",
"items": {
"$ref": "#/definitions/room-config"
}
}
}
}

View File

@ -92,12 +92,12 @@ export const data: Array<ConfigEntry> = [
markdown: "录制的直播画质 qn 值,以英文逗号分割,靠前的优先。\n\n**注意**(从录播姬 1.3.10 开始):\n\n- 所有主播刚开播时都是只有“原画”的,如果选择不录原画会导致直播开头漏录。\n- 如果设置的录制画质里没有原画,但是主播只有原画画质,会导致不能录制直播。\n- 录播姬不会为了切换录制的画质主动断开录制。\n\n画质 | qn 值\n:--:|:--:\n4K | 20000\n原画 | 10000\n蓝光(杜比) | 401\n蓝光 | 400\n超清 | 250\n高清 | 150\n流畅 | 80"
},
{
name: "RecordFilenameFormat",
description: "录制文件名格式",
name: "FileNameRecordTemplate",
description: "录制文件名模板",
type: "string?",
configType: "globalOnly",
defaultValue: "@\"{roomid}-{name}/录制-{roomid}-{date}-{time}-{ms}-{title}.flv\"",
markdown: "- 只支持 FLV 格式\n- 所有大括号均为英文半角括号\n- 录制时如果出现文件名冲突,会使用一个默认文件名\n\n变量 | 含义\n:--:|:--:\n{date} | 当前日期(年月日)\n{time} | 当前时间(时分秒)\n{ms} | 当前时间毫秒\n{roomid} | 房间号\n{title} | 标题\n{name} | 主播名\n{parea} | 大分区\n{area} | 子分区\n{random} | 随机数字\n"
markdown: "TODO: config v3 新的文件名模板系统的文档还没有写"
},
{
name: "WebHookUrls",

View File

@ -22,7 +22,7 @@ interface SectionInfo {
const map: SectionInfoMap = {
core: {
path: './BililiveRecorder.Core/Config/V2/Config.gen.cs',
path: './BililiveRecorder.Core/Config/V3/Config.gen.cs',
format: true,
header: true,
build: builderCore
@ -40,7 +40,7 @@ const map: SectionInfoMap = {
build: builderWeb
},
schema: {
path: './configV2.schema.json',
path: './configV3.schema.json',
format: false,
header: false,
build: builderSchema

View File

@ -4,7 +4,7 @@ import { trimEnd } from "../utils";
export default function (data: ConfigEntry[]): string {
let result = `using System.Collections.Generic;
using System.ComponentModel;
using BililiveRecorder.Core.Config.V2;
using BililiveRecorder.Core.Config.V3;
namespace BililiveRecorder.Cli.Configure
{`;

View File

@ -7,7 +7,7 @@ using HierarchicalPropertyDefault;
using Newtonsoft.Json;
#nullable enable
namespace BililiveRecorder.Core.Config.V2
namespace BililiveRecorder.Core.Config.V3
{
`;