diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..c31beb8 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-format": { + "version": "5.1.225507", + "commands": [ + "dotnet-format" + ] + } + } +} \ No newline at end of file diff --git a/.tools/build_config.js b/.tools/build_config.js new file mode 100644 index 0000000..9700694 --- /dev/null +++ b/.tools/build_config.js @@ -0,0 +1,81 @@ +"use strict"; +import { spawn } from "child_process"; +import { stdout, stderr } from "process"; +import { writeFileSync } from "fs"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from 'url'; + +import data from "./config_data.js" + +import generate_json_schema from "./generate_json_schema.js" +import generate_core_config from "./generate_core_config.js" +import generate_web_config from "./generate_web_config.js" + +const baseDirectory = dirname(fileURLToPath(import.meta.url)); + +const DO_NOT_EDIT_COMMENT = `// ****************************** +// GENERATED CODE, DO NOT EDIT MANUALLY. +// SEE .tools/build_config.js +// ******************************\n\n` + +// --------------------------------------------- +// SCHEMA +// --------------------------------------------- + +console.log("[node] writing json schema...") + +const json_schema_path = resolve(baseDirectory, '../configV2.schema.json'); + +const json_schema_code = generate_json_schema(data); + +writeFileSync(json_schema_path, json_schema_code, { + encoding: "utf8" +}); + +// --------------------------------------------- +// CORE +// --------------------------------------------- + +console.log("[node] writing core config...") + +const core_config_path = resolve(baseDirectory, '../BililiveRecorder.Core/Config/V2/Config.gen.cs'); + +const core_config_code = generate_core_config(data); + +writeFileSync(core_config_path, DO_NOT_EDIT_COMMENT + core_config_code, { + encoding: "utf8" +}); + +// --------------------------------------------- +// WEB +// --------------------------------------------- +/* disabled +console.log("[node] writing web config...") + +const web_config_path = resolve(baseDirectory, '../BililiveRecorder.Web.Schemas/Types/Config.gen.cs'); + +const web_config_code = generate_web_config(data); + +writeFileSync(web_config_path, DO_NOT_EDIT_COMMENT + web_config_code, { + encoding: "utf8" +}); +*/ +// --------------------------------------------- +// FORMAT +// --------------------------------------------- + +console.log("[node] formatting...") + +let format = spawn('dotnet', ['tool', 'run', 'dotnet-format', '--', '--include', './BililiveRecorder.Core/Config/V2/Config.gen.cs'/*, './BililiveRecorder.Web.Schemas/Types/Config.gen.cs'*/]) + +format.stdout.on('data', function (data) { + stdout.write('[dotnet-format] ' + data.toString()); +}); + +format.stderr.on('data', function (data) { + stderr.write('[dotnet-format] ' + data.toString()); +}); + +format.on('exit', function (code) { + console.log('[node] format done code ' + code.toString()); +}); diff --git a/BililiveRecorder.Core/Config/V2/build_config.data.js b/.tools/config_data.js similarity index 97% rename from BililiveRecorder.Core/Config/V2/build_config.data.js rename to .tools/config_data.js index 5ec9813..e41ee64 100644 --- a/BililiveRecorder.Core/Config/V2/build_config.data.js +++ b/.tools/config_data.js @@ -1,4 +1,4 @@ -module.exports = { +export default { "global": [{ "name": "TimingStreamRetry", "type": "uint", @@ -70,13 +70,14 @@ module.exports = { "type": "int", "desc": "房间号", "default": "default", - "without_global": true + "without_global": true, + "web_readonly": true }, { "name": "AutoRecord", "type": "bool", "desc": "是否启用自动录制", "default": "default", - "without_global": true + "without_global": true, }, { "name": "RecordMode", "type": "RecordMode", diff --git a/.tools/generate_core_config.js b/.tools/generate_core_config.js new file mode 100644 index 0000000..2f3a59f --- /dev/null +++ b/.tools/generate_core_config.js @@ -0,0 +1,66 @@ +"use strict"; + +export default function generate_core_config(data) { + let result = `using System.ComponentModel; +using HierarchicalPropertyDefault; +using Newtonsoft.Json; + +#nullable enable +namespace BililiveRecorder.Core.Config.V2 +{ +`; + + function write_property(r) { + result += `/// \n/// ${r.desc}\n/// \n`; + result += `public ${r.type}${!!r.nullable ? "?" : ""} ${r.name} { get => this.GetPropertyValue<${r.type}>(); set => this.SetPropertyValue(value); }\n`; + result += `public bool Has${r.name} { get => this.GetPropertyHasValue(nameof(this.${r.name})); set => this.SetPropertyHasValue<${r.type}>(value, nameof(this.${r.name})); }\n`; + result += `[JsonProperty(nameof(${r.name})), EditorBrowsable(EditorBrowsableState.Never)]\n`; + result += `public Optional<${r.type}${!!r.nullable ? "?" : ""}> Optional${r.name} { get => this.GetPropertyValueOptional<${r.type}>(nameof(this.${r.name})); set => this.SetPropertyValueOptional(value, nameof(this.${r.name})); }\n\n`; + } + + function write_readonly_property(r) { + result += `/// \n/// ${r.desc}\n/// \n`; + result += `public ${r.type}${!!r.nullable ? "?" : ""} ${r.name} => this.GetPropertyValue<${r.type}>();\n\n`; + } + + { + result += "[JsonObject(MemberSerialization.OptIn)]\n"; + result += "public sealed partial class RoomConfig : HierarchicalObject\n"; + result += "{\n"; + + data.room.forEach(r => write_property(r)); + data.global.forEach(r => write_readonly_property(r)); + + result += "}\n\n"; + } + + { + result += "[JsonObject(MemberSerialization.OptIn)]\n"; + result += "public sealed partial class GlobalConfig : HierarchicalObject\n"; + result += "{\n"; + + data.global + .concat(data.room.filter(x => !x.without_global)) + .forEach(r => write_property(r)); + + result += "}\n\n"; + } + + { + result += `public sealed partial class DefaultConfig + { + public static readonly DefaultConfig Instance = new DefaultConfig(); + private DefaultConfig() {}\n\n`; + + data.global + .concat(data.room.filter(x => !x.without_global)) + .forEach(r => { + result += `public ${r.type} ${r.name} => ${r.default};\n\n`; + }); + + result += "}\n\n"; + } + + result += `}\n`; + return result; +} diff --git a/.tools/generate_json_schema.js b/.tools/generate_json_schema.js new file mode 100644 index 0000000..49ab24e --- /dev/null +++ b/.tools/generate_json_schema.js @@ -0,0 +1,95 @@ +function tryEvalValue(str) { + try { + return eval(str); + } catch { + return str; + } +} + +function mapTypeToJsonSchema(name, type, defVal) { + switch (type) { + case "RecordMode": + return { type: "integer", default: 0, enum: [0, 1], "description": "0: Standard\n1: Raw" }; + case "CuttingMode": + return { type: "integer", default: 0, enum: [0, 1, 2], "description": "0: 禁用\n1: 根据时间切割\n2: 根据文件大小切割" }; + case "uint": + return { type: "integer", minimum: 0, maximum: 4294967295, default: tryEvalValue(defVal) }; + case "int": + return { type: "integer", minimum: -2147483648, maximum: 2147483647, default: tryEvalValue(defVal) }; + case "bool": + return { type: "boolean", default: tryEvalValue(defVal) }; + case "string": + if (name === 'Cookie') { + return { type: "string", pattern: "^(\S+=\S+;? ?)*$", maxLength: 4096, }; + } + return { type: "string", default: defVal === 'string.Empty' ? '' : tryEvalValue(defVal.replace(/^@/, '')) }; + default: + return { type, default: defVal }; + } +} + +function insert(target, { name, type, desc, default: defVal/*, nullable */ }) { + const typeObj = mapTypeToJsonSchema(name, type, defVal); + if (defVal === 'default') delete typeObj['default']; + target[name] = { + "description": desc, + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { "type": "boolean", "default": true }, "Value": typeObj + } + }; +} + +export default function generate_json_schema(data) { + const sharedConfig = {}; + const globalConfig = {}; + const roomConfig = {}; + + data.room.filter(x => !x.without_global).forEach(v => insert(sharedConfig, v)); + data.room.filter(x => x.without_global).forEach(v => insert(roomConfig, v)); + data.global.forEach(v => insert(globalConfig, v)); + + const schema = { + "$comment": "GENERATED CODE, DO NOT EDIT MANUALLY.", + "$schema": "http://json-schema.org/schema", + "definitions": { + "global-config": { + "description": "全局配置", + "additionalProperties": false, + "properties": { ...globalConfig, ...sharedConfig } + }, + "room-config": { + "description": "单个房间配置", + "additionalProperties": false, + "properties": { ...roomConfig, ...sharedConfig } + } + }, + "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" + } + } + } + } + + return JSON.stringify(schema, null, 2) +} diff --git a/.tools/generate_web_config.js b/.tools/generate_web_config.js new file mode 100644 index 0000000..25dc211 --- /dev/null +++ b/.tools/generate_web_config.js @@ -0,0 +1,134 @@ +"use strict"; + +export default function generate_web_config(data) { + let result = `using BililiveRecorder.Core.Config.V2; +using GraphQL.Types; +using HierarchicalPropertyDefault; + +#nullable enable +namespace BililiveRecorder.Web.Schemas.Types +{ +`; + + function write_query_graphType_property(r) { + if (r.without_global) { + result += `this.Field(x => x.${r.name});\n`; + } else { + result += `this.Field(x => x.Optional${r.name}, type: typeof(HierarchicalOptionalType<${r.type}>));\n`; + } + } + + function write_mutation_graphType_property(r) { + if (r.without_global) { + result += `this.Field(x => x.${r.name}, nullable: true);\n`; + } else { + result += `this.Field(x => x.Optional${r.name}, nullable: true, type: typeof(HierarchicalOptionalInputType<${r.type}>));\n`; + } + } + + function write_mutation_dataType_property(r) { + if (r.without_global) { + result += `public ${r.type + (r.nullable ? '?' : '')}? ${r.name} { get; set; }\n`; + } else { + result += `public Optional<${r.type + (r.nullable ? '?' : '')}>? Optional${r.name} { get; set; }\n`; + } + } + + function write_mutation_apply_method(r) { + if (r.without_global) { + result += `if (this.${r.name}.HasValue) config.${r.name} = this.${r.name}.Value;\n`; + } else { + result += `if (this.Optional${r.name}.HasValue) config.Optional${r.name} = this.Optional${r.name}.Value;\n`; + } + } + + { // ====== RoomConfigType ====== + result += "internal class RoomConfigType : ObjectGraphType\n{\n"; + result += "public RoomConfigType()\n{\n" + + data.room.forEach(r => write_query_graphType_property(r)); + + result += "}\n}\n\n"; + } + + { // ====== GlobalConfigType ====== + result += "internal class GlobalConfigType : ObjectGraphType\n{\n" + result += "public GlobalConfigType()\n{\n"; + + data.global + .concat(data.room.filter(x => !x.without_global)) + .forEach(r => write_query_graphType_property(r)); + + result += "}\n}\n\n"; + } + + { // ====== DefaultConfigType ====== + result += "internal class DefaultConfigType : ObjectGraphType\n{\n" + result += "public DefaultConfigType()\n{\n"; + + data.global + .concat(data.room.filter(x => !x.without_global)) + .forEach(r => { + result += `this.Field(x => x.${r.name});\n`; + }); + + result += "}\n}\n\n"; + } + + { // ====== SetRoomConfig ====== + result += "internal class SetRoomConfig\n{\n" + + data.room.filter(x => !x.web_readonly) + .forEach(r => write_mutation_dataType_property(r)); + + result += "\npublic void ApplyTo(RoomConfig config)\n{\n"; + + data.room.filter(x => !x.web_readonly) + .forEach(r => write_mutation_apply_method(r)); + + result += "}\n}\n\n"; + } + + { // ====== SetRoomConfigType ====== + result += "internal class SetRoomConfigType : InputObjectGraphType\n{\n" + result += "public SetRoomConfigType()\n{\n"; + + data.room.filter(x => !x.web_readonly) + .forEach(r => write_mutation_graphType_property(r)); + + result += "}\n}\n\n"; + } + + { // ====== SetGlobalConfig ====== + result += "internal class SetGlobalConfig\n{\n" + + data.global + .concat(data.room.filter(x => !x.without_global)) + .filter(x => !x.web_readonly) + .forEach(r => write_mutation_dataType_property(r)); + + result += "\npublic void ApplyTo(GlobalConfig config)\n{\n"; + + data.global + .concat(data.room.filter(x => !x.without_global)) + .filter(x => !x.web_readonly) + .forEach(r => write_mutation_apply_method(r)); + + result += "}\n}\n\n"; + } + + { // ====== SetGlobalConfigType ====== + result += "internal class SetGlobalConfigType : InputObjectGraphType\n{\n" + result += "public SetGlobalConfigType()\n{\n"; + + data.global + .concat(data.room.filter(x => !x.without_global)) + .filter(x => !x.web_readonly) + .forEach(r => write_mutation_graphType_property(r)); + + result += "}\n}\n\n"; + } + + result += `}\n`; + return result; +} diff --git a/.tools/package.json b/.tools/package.json new file mode 100644 index 0000000..96ae6e5 --- /dev/null +++ b/.tools/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/BililiveRecorder.Cli/Configure/ConfigureCommand.cs b/BililiveRecorder.Cli/Configure/ConfigureCommand.cs index 723c641..436fa4f 100644 --- a/BililiveRecorder.Cli/Configure/ConfigureCommand.cs +++ b/BililiveRecorder.Cli/Configure/ConfigureCommand.cs @@ -135,7 +135,7 @@ namespace BililiveRecorder.Cli.Configure switch (selection) { case JsonSchemaSelection.Default: - config.DollarSignSchema = "https://raw.githubusercontent.com/Bililive/BililiveRecorder/dev-1.3/BililiveRecorder.Core/Config/V2/config.schema.json"; + config.DollarSignSchema = "https://raw.githubusercontent.com/Bililive/BililiveRecorder/dev-1.3/configV2.schema.json"; break; case JsonSchemaSelection.Custom: config.DollarSignSchema = AnsiConsole.Prompt(new TextPrompt("[green]JSON Schema[/]:").AllowEmpty()); diff --git a/BililiveRecorder.Cli/Configure/JsonSchemaSelection.cs b/BililiveRecorder.Cli/Configure/JsonSchemaSelection.cs index a16f975..bc90b83 100644 --- a/BililiveRecorder.Cli/Configure/JsonSchemaSelection.cs +++ b/BililiveRecorder.Cli/Configure/JsonSchemaSelection.cs @@ -4,7 +4,7 @@ namespace BililiveRecorder.Cli.Configure { public enum JsonSchemaSelection { - [Description("https://raw.githubusercontent.com/.../config.schema.json")] + [Description("https://raw.githubusercontent.com/Bililive/BililiveRecorder/dev-1.3/configV2.schema.json")] Default, [Description("Custom")] diff --git a/BililiveRecorder.Core/Config/V2/Config.gen.cs b/BililiveRecorder.Core/Config/V2/Config.gen.cs index 513fbc7..f33bb79 100644 --- a/BililiveRecorder.Core/Config/V2/Config.gen.cs +++ b/BililiveRecorder.Core/Config/V2/Config.gen.cs @@ -1,7 +1,8 @@ // ****************************** -// GENERATED CODE, DO NOT EDIT. -// RUN FORMATTER AFTER GENERATE +// GENERATED CODE, DO NOT EDIT MANUALLY. +// SEE .tools/build_config.js // ****************************** + using System.ComponentModel; using HierarchicalPropertyDefault; using Newtonsoft.Json; @@ -337,7 +338,7 @@ namespace BililiveRecorder.Core.Config.V2 public sealed partial class DefaultConfig { - internal static readonly DefaultConfig Instance = new DefaultConfig(); + public static readonly DefaultConfig Instance = new DefaultConfig(); private DefaultConfig() { } public uint TimingStreamRetry => 6 * 1000; diff --git a/BililiveRecorder.Core/Config/V2/build_config.js b/BililiveRecorder.Core/Config/V2/build_config.js deleted file mode 100644 index 86a9755..0000000 --- a/BililiveRecorder.Core/Config/V2/build_config.js +++ /dev/null @@ -1,169 +0,0 @@ -"use strict"; -const fs = require("fs"); -const data = require("./build_config.data.js"); - -const CODE_HEADER = - `// ****************************** -// GENERATED CODE, DO NOT EDIT. -// RUN FORMATTER AFTER GENERATE -// ****************************** -using System.ComponentModel; -using HierarchicalPropertyDefault; -using Newtonsoft.Json; - -#nullable enable -namespace BililiveRecorder.Core.Config.V2 -{ -`; - -const CODE_FOOTER = `}\n`; - -let result = CODE_HEADER; - -function write_property(r) { - result += `/// \n/// ${r.desc}\n/// \n` - result += `public ${r.type}${!!r.nullable ? "?" : ""} ${r.name} { get => this.GetPropertyValue<${r.type}>(); set => this.SetPropertyValue(value); }\n` - result += `public bool Has${r.name} { get => this.GetPropertyHasValue(nameof(this.${r.name})); set => this.SetPropertyHasValue<${r.type}>(value, nameof(this.${r.name})); }\n` - result += `[JsonProperty(nameof(${r.name})), EditorBrowsable(EditorBrowsableState.Never)]\n` - result += `public Optional<${r.type}${!!r.nullable ? "?" : ""}> Optional${r.name} { get => this.GetPropertyValueOptional<${r.type}>(nameof(this.${r.name})); set => this.SetPropertyValueOptional(value, nameof(this.${r.name})); }\n\n` -} - -function write_readonly_property(r) { - result += `/// \n/// ${r.desc}\n/// \n` - result += `public ${r.type}${!!r.nullable ? "?" : ""} ${r.name} => this.GetPropertyValue<${r.type}>();\n\n` -} - -{ - result += "[JsonObject(MemberSerialization.OptIn)]\n" - result += "public sealed partial class RoomConfig : HierarchicalObject\n" - result += "{\n"; - - data.room.forEach(r => write_property(r)) - data.global.forEach(r => write_readonly_property(r)) - - result += "}\n\n" -} - -{ - result += "[JsonObject(MemberSerialization.OptIn)]\n" - result += "public sealed partial class GlobalConfig : HierarchicalObject\n" - result += "{\n"; - - data.global - .concat(data.room.filter(x => !x.without_global)) - .forEach(r => write_property(r)) - - result += "}\n\n" -} - -{ - result += `public sealed partial class DefaultConfig - { - internal static readonly DefaultConfig Instance = new DefaultConfig(); - private DefaultConfig() {}\n\n`; - - data.global - .concat(data.room.filter(x => !x.without_global)) - .forEach(r => { - result += `public ${r.type} ${r.name} => ${r.default};\n\n` - }) - - result += "}\n\n" -} - -result += CODE_FOOTER; - -fs.writeFileSync("./Config.gen.cs", result, { - encoding: "utf8" -}); - -console.log("记得 format Config.gen.cs") - -/** 进行一个json schema的生成 */ -const sharedConfig = {}; -const globalConfig = {}; -const roomConfig = {}; -function tEval(str) { - try { - return eval(str); - } catch { - return str; - } -} -function switchType(name, type, defVal) { - switch (type) { - case "RecordMode": - return { type: "integer", default: 0, enum: [0, 1], "description": "0: Standard\n1: Raw" }; - case "CuttingMode": - return { type: "integer", default: 0, enum: [0, 1, 2], "description": "0: 禁用\n1: 根据时间切割\n2: 根据文件大小切割" }; - case "uint": - return { type: "integer", minimum: 0, maximum: 4294967295, default: tEval(defVal) }; - case "int": - return { type: "integer", minimum: -2147483648, maximum: 2147483647, default: tEval(defVal) }; - case "bool": - return { type: "boolean", default: tEval(defVal) }; - case "string": - if (name === 'Cookie') { - return { type: "string", pattern: "^(\S+=\S+;? ?)*$", maxLength: 4096, }; - } - return { type: "string", default: defVal === 'string.Empty' ? '' : tEval(defVal.replace(/^@/, '')) }; - default: - return { type, default: defVal }; - } -} -function insert(target, { name, type, desc, default: defVal/*, nullable */ }) { - const typeObj = switchType(name, type, defVal); - if (defVal === 'default') delete typeObj['default']; - target[name] = { - "description": desc, - "type": "object", - "additionalProperties": false, - "properties": { - "HasValue": { "type": "boolean", "default": true }, "Value": typeObj - } - }; -} -data.room.filter(x => !x.without_global).forEach(v => insert(sharedConfig, v)); -data.room.filter(x => x.without_global).forEach(v => insert(roomConfig, v)); -data.global.forEach(v => insert(globalConfig, v)); - - -fs.writeFileSync("./config.schema.json", "// GENERATED CODE, DO NOT EDIT.\n" + JSON.stringify({ - "$schema": "http://json-schema.org/schema", - "definitions": { - "global-config": { - "description": "全局配置", - "additionalProperties": false, - "properties": { ...sharedConfig, ...globalConfig } - }, - "room-config": { - "description": "单个房间配置", - "additionalProperties": false, - "properties": { ...sharedConfig, ...roomConfig } - } - }, - "type": "object", - "additionalProperties": false, - "required": [ - "$schema", - "version" - ], - "properties": { - "$schema": { - "type": "string", - "default": "https://raw.githubusercontent.com/Bililive/BililiveRecorder/dev-1.3/BililiveRecorder.Core/Config/V2/config.schema.json" - }, - "version": { - "const": 2 - }, - "global": { - "$ref": "#/definitions/global-config" - }, - "rooms": { - "type": "array", - "items": { - "$ref": "#/definitions/room-config" - } - } - } -}, null, 4)); diff --git a/BililiveRecorder.Core/Config/V2/config.schema.json b/BililiveRecorder.Core/Config/V2/config.schema.json index 9184a65..23c76fb 100644 --- a/BililiveRecorder.Core/Config/V2/config.schema.json +++ b/BililiveRecorder.Core/Config/V2/config.schema.json @@ -1,5 +1,5 @@ -// GENERATED CODE, DO NOT EDIT. { + "$comment": "Deprecated. Use https://raw.githubusercontent.com/Bililive/BililiveRecorder/dev-1.3/configV2.schema.json", "$schema": "http://json-schema.org/schema", "definitions": { "global-config": { @@ -542,8 +542,7 @@ ], "properties": { "$schema": { - "type": "string", - "default": "https://raw.githubusercontent.com/Bililive/BililiveRecorder/dev-1.3/BililiveRecorder.Core/Config/V2/config.schema.json" + "const": "https://raw.githubusercontent.com/Bililive/BililiveRecorder/dev-1.3/configV2.schema.json" }, "version": { "const": 2 diff --git a/configV2.schema.json b/configV2.schema.json new file mode 100644 index 0000000..bb058a0 --- /dev/null +++ b/configV2.schema.json @@ -0,0 +1,561 @@ +{ + "$comment": "GENERATED CODE, DO NOT EDIT MANUALLY.", + "$schema": "http://json-schema.org/schema", + "definitions": { + "global-config": { + "description": "全局配置", + "additionalProperties": false, + "properties": { + "TimingStreamRetry": { + "description": "录制断开重连时间间隔 毫秒", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "default": 6000 + } + } + }, + "TimingStreamConnect": { + "description": "连接直播服务器超时时间 毫秒", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "default": 5000 + } + } + }, + "TimingDanmakuRetry": { + "description": "弹幕服务器重连时间间隔 毫秒", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "default": 15000 + } + } + }, + "TimingCheckInterval": { + "description": "HTTP API 检查时间间隔 秒", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "default": 600 + } + } + }, + "TimingWatchdogTimeout": { + "description": "最大未收到新直播数据时间 毫秒", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "default": 10000 + } + } + }, + "RecordDanmakuFlushInterval": { + "description": "触发 的弹幕个数", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "default": 20 + } + } + }, + "Cookie": { + "description": "请求 API 时使用的 Cookie", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "string", + "pattern": "^(S+=S+;? ?)*$", + "maxLength": 4096 + } + } + }, + "WebHookUrls": { + "description": "录制文件写入结束 Webhook 地址 每行一个", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "string", + "default": "" + } + } + }, + "WebHookUrlsV2": { + "description": "Webhook v2 地址 每行一个", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "string", + "default": "" + } + } + }, + "LiveApiHost": { + "description": "替换 api.live.bilibili.com 服务器为其他反代,可以支持在云服务器上录制", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "string", + "default": "https://api.live.bilibili.com" + } + } + }, + "RecordFilenameFormat": { + "description": "录制文件名模板", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "string", + "default": "{roomid}-{name}/录制-{roomid}-{date}-{time}-{ms}-{title}.flv" + } + } + }, + "WpfShowTitleAndArea": { + "description": "是否显示直播间标题和分区", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": true + } + } + }, + "RecordMode": { + "description": "录制模式", + "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": "录制文件自动切割模式", + "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": "录制文件自动切割数值(分钟/MiB)", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "default": 100 + } + } + }, + "RecordDanmaku": { + "description": "是否同时录制弹幕", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": false + } + } + }, + "RecordDanmakuRaw": { + "description": "是否记录弹幕原始数据", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": false + } + } + }, + "RecordDanmakuSuperChat": { + "description": "是否同时录制 SuperChat", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": true + } + } + }, + "RecordDanmakuGift": { + "description": "是否同时录制 礼物", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": false + } + } + }, + "RecordDanmakuGuard": { + "description": "是否同时录制 上船", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": true + } + } + }, + "RecordingQuality": { + "description": "录制的直播画质 qn 值,逗号分割,靠前的优先", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "string", + "default": "10000" + } + } + } + } + }, + "room-config": { + "description": "单个房间配置", + "additionalProperties": false, + "properties": { + "RoomId": { + "description": "房间号", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } + }, + "AutoRecord": { + "description": "是否启用自动录制", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean" + } + } + }, + "RecordMode": { + "description": "录制模式", + "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": "录制文件自动切割模式", + "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": "录制文件自动切割数值(分钟/MiB)", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "default": 100 + } + } + }, + "RecordDanmaku": { + "description": "是否同时录制弹幕", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": false + } + } + }, + "RecordDanmakuRaw": { + "description": "是否记录弹幕原始数据", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": false + } + } + }, + "RecordDanmakuSuperChat": { + "description": "是否同时录制 SuperChat", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": true + } + } + }, + "RecordDanmakuGift": { + "description": "是否同时录制 礼物", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": false + } + } + }, + "RecordDanmakuGuard": { + "description": "是否同时录制 上船", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": true + } + } + }, + "RecordingQuality": { + "description": "录制的直播画质 qn 值,逗号分割,靠前的优先", + "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" + } + } + } +} \ No newline at end of file