Improve config code generation

This commit is contained in:
Genteure 2021-07-15 21:41:21 +08:00
parent 2521ed228e
commit ca3dfd06bf
13 changed files with 964 additions and 180 deletions

12
.config/dotnet-tools.json Normal file
View File

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-format": {
"version": "5.1.225507",
"commands": [
"dotnet-format"
]
}
}
}

81
.tools/build_config.js Normal file
View File

@ -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());
});

View File

@ -1,4 +1,4 @@
module.exports = { export default {
"global": [{ "global": [{
"name": "TimingStreamRetry", "name": "TimingStreamRetry",
"type": "uint", "type": "uint",
@ -70,13 +70,14 @@ module.exports = {
"type": "int", "type": "int",
"desc": "房间号", "desc": "房间号",
"default": "default", "default": "default",
"without_global": true "without_global": true,
"web_readonly": true
}, { }, {
"name": "AutoRecord", "name": "AutoRecord",
"type": "bool", "type": "bool",
"desc": "是否启用自动录制", "desc": "是否启用自动录制",
"default": "default", "default": "default",
"without_global": true "without_global": true,
}, { }, {
"name": "RecordMode", "name": "RecordMode",
"type": "RecordMode", "type": "RecordMode",

View File

@ -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 += `/// <summary>\n/// ${r.desc}\n/// </summary>\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 += `/// <summary>\n/// ${r.desc}\n/// </summary>\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<GlobalConfig, RoomConfig>\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<DefaultConfig, GlobalConfig>\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;
}

View File

@ -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)
}

View File

@ -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<RoomConfig>\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<GlobalConfig>\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<DefaultConfig>\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<SetRoomConfig>\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<SetGlobalConfig>\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;
}

3
.tools/package.json Normal file
View File

@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@ -135,7 +135,7 @@ namespace BililiveRecorder.Cli.Configure
switch (selection) switch (selection)
{ {
case JsonSchemaSelection.Default: 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; break;
case JsonSchemaSelection.Custom: case JsonSchemaSelection.Custom:
config.DollarSignSchema = AnsiConsole.Prompt(new TextPrompt<string>("[green]JSON Schema[/]:").AllowEmpty()); config.DollarSignSchema = AnsiConsole.Prompt(new TextPrompt<string>("[green]JSON Schema[/]:").AllowEmpty());

View File

@ -4,7 +4,7 @@ namespace BililiveRecorder.Cli.Configure
{ {
public enum JsonSchemaSelection 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, Default,
[Description("Custom")] [Description("Custom")]

View File

@ -1,7 +1,8 @@
// ****************************** // ******************************
// GENERATED CODE, DO NOT EDIT. // GENERATED CODE, DO NOT EDIT MANUALLY.
// RUN FORMATTER AFTER GENERATE // SEE .tools/build_config.js
// ****************************** // ******************************
using System.ComponentModel; using System.ComponentModel;
using HierarchicalPropertyDefault; using HierarchicalPropertyDefault;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -337,7 +338,7 @@ namespace BililiveRecorder.Core.Config.V2
public sealed partial class DefaultConfig public sealed partial class DefaultConfig
{ {
internal static readonly DefaultConfig Instance = new DefaultConfig(); public static readonly DefaultConfig Instance = new DefaultConfig();
private DefaultConfig() { } private DefaultConfig() { }
public uint TimingStreamRetry => 6 * 1000; public uint TimingStreamRetry => 6 * 1000;

View File

@ -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 += `/// <summary>\n/// ${r.desc}\n/// </summary>\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 += `/// <summary>\n/// ${r.desc}\n/// </summary>\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<GlobalConfig, RoomConfig>\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<DefaultConfig, GlobalConfig>\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));

View File

@ -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", "$schema": "http://json-schema.org/schema",
"definitions": { "definitions": {
"global-config": { "global-config": {
@ -542,8 +542,7 @@
], ],
"properties": { "properties": {
"$schema": { "$schema": {
"type": "string", "const": "https://raw.githubusercontent.com/Bililive/BililiveRecorder/dev-1.3/configV2.schema.json"
"default": "https://raw.githubusercontent.com/Bililive/BililiveRecorder/dev-1.3/BililiveRecorder.Core/Config/V2/config.schema.json"
}, },
"version": { "version": {
"const": 2 "const": 2

561
configV2.schema.json Normal file
View File

@ -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": "触发 <see cref=\"System.Xml.XmlWriter.Flush\"/> 的弹幕个数",
"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"
}
}
}
}