2020-11-24 20:15:21 +08:00
|
|
|
|
using System;
|
2023-06-05 23:45:14 +08:00
|
|
|
|
using System.Diagnostics;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Text;
|
2020-12-20 18:25:01 +08:00
|
|
|
|
using System.Text.RegularExpressions;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
using System.Threading;
|
2021-04-29 18:08:55 +08:00
|
|
|
|
using System.Threading.Tasks;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
using System.Xml;
|
2021-02-23 18:03:37 +08:00
|
|
|
|
using BililiveRecorder.Core.Api.Danmaku;
|
2021-12-19 21:10:34 +08:00
|
|
|
|
using BililiveRecorder.Core.Config.V3;
|
2022-05-18 00:23:01 +08:00
|
|
|
|
using BililiveRecorder.Core.Scripting;
|
2021-04-26 22:45:28 +08:00
|
|
|
|
using Serilog;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
|
2021-01-01 14:46:27 +08:00
|
|
|
|
#nullable enable
|
2021-02-23 18:03:37 +08:00
|
|
|
|
namespace BililiveRecorder.Core.Danmaku
|
2020-11-23 17:35:42 +08:00
|
|
|
|
{
|
2022-05-16 23:28:31 +08:00
|
|
|
|
internal class BasicDanmakuWriter : IBasicDanmakuWriter
|
2020-11-23 17:35:42 +08:00
|
|
|
|
{
|
|
|
|
|
private static readonly XmlWriterSettings xmlWriterSettings = new XmlWriterSettings
|
|
|
|
|
{
|
2021-04-29 18:08:55 +08:00
|
|
|
|
Async = true,
|
2020-11-23 17:35:42 +08:00
|
|
|
|
Indent = true,
|
2020-12-29 07:36:27 +08:00
|
|
|
|
IndentChars = " ",
|
2020-11-23 17:35:42 +08:00
|
|
|
|
Encoding = Encoding.UTF8,
|
|
|
|
|
CloseOutput = true,
|
2021-04-29 18:08:55 +08:00
|
|
|
|
WriteEndDocumentOnClose = true,
|
2020-11-23 17:35:42 +08:00
|
|
|
|
};
|
|
|
|
|
|
2020-12-20 18:25:01 +08:00
|
|
|
|
private static readonly Regex invalidXMLChars = new Regex(@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]", RegexOptions.Compiled);
|
2021-04-16 17:39:38 +08:00
|
|
|
|
private static string RemoveInvalidXMLChars(string? text) => string.IsNullOrWhiteSpace(text) ? string.Empty : invalidXMLChars.Replace(text, string.Empty);
|
2020-12-20 18:25:01 +08:00
|
|
|
|
|
2021-01-01 14:46:27 +08:00
|
|
|
|
private XmlWriter? xmlWriter = null;
|
2023-06-05 23:45:14 +08:00
|
|
|
|
private readonly Stopwatch dmTime = new Stopwatch();
|
2020-12-17 19:32:48 +08:00
|
|
|
|
private uint writeCount = 0;
|
2021-02-23 18:03:37 +08:00
|
|
|
|
private RoomConfig? config;
|
2020-11-24 20:15:21 +08:00
|
|
|
|
|
2020-11-23 17:35:42 +08:00
|
|
|
|
private readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
|
2021-04-26 22:45:28 +08:00
|
|
|
|
private readonly ILogger logger;
|
2022-05-18 00:23:01 +08:00
|
|
|
|
private readonly UserScriptRunner userScriptRunner;
|
2021-04-26 22:45:28 +08:00
|
|
|
|
|
2022-05-18 00:23:01 +08:00
|
|
|
|
public BasicDanmakuWriter(ILogger logger, UserScriptRunner userScriptRunner)
|
2021-04-26 22:45:28 +08:00
|
|
|
|
{
|
|
|
|
|
this.logger = logger?.ForContext<BasicDanmakuWriter>() ?? throw new ArgumentNullException(nameof(logger));
|
2022-05-18 00:23:01 +08:00
|
|
|
|
this.userScriptRunner = userScriptRunner ?? throw new ArgumentNullException(nameof(userScriptRunner));
|
2021-04-26 22:45:28 +08:00
|
|
|
|
}
|
2020-11-23 17:35:42 +08:00
|
|
|
|
|
2021-02-23 18:03:37 +08:00
|
|
|
|
public void EnableWithPath(string path, IRoom room)
|
2020-11-23 17:35:42 +08:00
|
|
|
|
{
|
2021-01-01 14:46:27 +08:00
|
|
|
|
if (this.disposedValue) return;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
|
2021-01-01 14:46:27 +08:00
|
|
|
|
this.semaphoreSlim.Wait();
|
2020-11-23 17:35:42 +08:00
|
|
|
|
try
|
|
|
|
|
{
|
2021-01-01 14:46:27 +08:00
|
|
|
|
if (this.xmlWriter != null)
|
2020-11-23 17:35:42 +08:00
|
|
|
|
{
|
2021-01-01 14:46:27 +08:00
|
|
|
|
this.xmlWriter.Close();
|
|
|
|
|
this.xmlWriter.Dispose();
|
|
|
|
|
this.xmlWriter = null;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try { Directory.CreateDirectory(Path.GetDirectoryName(path)); } catch (Exception) { }
|
|
|
|
|
var stream = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.Read);
|
|
|
|
|
|
2021-02-23 18:03:37 +08:00
|
|
|
|
this.config = room.RoomConfig;
|
|
|
|
|
|
2021-01-01 14:46:27 +08:00
|
|
|
|
this.xmlWriter = XmlWriter.Create(stream, xmlWriterSettings);
|
2021-08-10 18:53:04 +08:00
|
|
|
|
WriteStartDocument(this.xmlWriter, room);
|
2023-06-05 23:45:14 +08:00
|
|
|
|
this.dmTime.Restart();
|
2021-01-01 14:46:27 +08:00
|
|
|
|
this.writeCount = 0;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
2021-01-01 14:46:27 +08:00
|
|
|
|
this.semaphoreSlim.Release();
|
2020-11-23 17:35:42 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Disable()
|
|
|
|
|
{
|
2021-01-01 14:46:27 +08:00
|
|
|
|
if (this.disposedValue) return;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
|
2021-01-01 14:46:27 +08:00
|
|
|
|
this.semaphoreSlim.Wait();
|
2021-04-26 22:45:28 +08:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
this.DisableCore();
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
this.semaphoreSlim.Release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DisableCore()
|
|
|
|
|
{
|
2020-11-23 17:35:42 +08:00
|
|
|
|
try
|
|
|
|
|
{
|
2021-01-01 14:46:27 +08:00
|
|
|
|
if (this.xmlWriter != null)
|
2020-11-23 17:35:42 +08:00
|
|
|
|
{
|
2021-01-01 14:46:27 +08:00
|
|
|
|
this.xmlWriter.Close();
|
|
|
|
|
this.xmlWriter.Dispose();
|
|
|
|
|
this.xmlWriter = null;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-04-26 22:45:28 +08:00
|
|
|
|
catch (Exception ex)
|
2020-11-23 17:35:42 +08:00
|
|
|
|
{
|
2021-04-26 22:45:28 +08:00
|
|
|
|
this.logger.Warning(ex, "关闭弹幕文件时发生错误");
|
|
|
|
|
this.xmlWriter = null;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-29 18:08:55 +08:00
|
|
|
|
public async Task WriteAsync(DanmakuModel danmakuModel)
|
2020-11-23 17:35:42 +08:00
|
|
|
|
{
|
2022-05-18 00:23:01 +08:00
|
|
|
|
if (this.disposedValue)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (this.xmlWriter is null || this.config is null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (danmakuModel.MsgType is not (DanmakuMsgType.Comment or DanmakuMsgType.SuperChat or DanmakuMsgType.GiftSend or DanmakuMsgType.GuardBuy))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (!this.userScriptRunner.CallOnDanmaku(this.logger, danmakuModel.RawString))
|
|
|
|
|
return;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
|
2021-04-29 18:08:55 +08:00
|
|
|
|
await this.semaphoreSlim.WaitAsync();
|
2020-11-23 17:35:42 +08:00
|
|
|
|
try
|
|
|
|
|
{
|
2022-05-18 00:23:01 +08:00
|
|
|
|
if (this.xmlWriter is null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var write = true;
|
|
|
|
|
var recordDanmakuRaw = this.config.RecordDanmakuRaw;
|
|
|
|
|
switch (danmakuModel.MsgType)
|
|
|
|
|
{
|
|
|
|
|
case DanmakuMsgType.Comment:
|
|
|
|
|
{
|
|
|
|
|
var type = danmakuModel.RawObject?["info"]?[0]?[1]?.ToObject<int>() ?? 1;
|
|
|
|
|
var size = danmakuModel.RawObject?["info"]?[0]?[2]?.ToObject<int>() ?? 25;
|
|
|
|
|
var color = danmakuModel.RawObject?["info"]?[0]?[3]?.ToObject<int>() ?? 0XFFFFFF;
|
|
|
|
|
var st = danmakuModel.RawObject?["info"]?[0]?[4]?.ToObject<long>() ?? 0L;
|
|
|
|
|
|
2023-06-05 23:45:14 +08:00
|
|
|
|
var ts = Math.Max(this.dmTime.Elapsed.TotalSeconds, 0d);
|
2022-05-18 00:23:01 +08:00
|
|
|
|
|
|
|
|
|
await this.xmlWriter.WriteStartElementAsync(null, "d", null).ConfigureAwait(false);
|
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "p", null, $"{ts:F3},{type},{size},{color},{st},0,{danmakuModel.UserID},0").ConfigureAwait(false);
|
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "user", null, RemoveInvalidXMLChars(danmakuModel.UserName)).ConfigureAwait(false);
|
|
|
|
|
if (recordDanmakuRaw)
|
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "raw", null, RemoveInvalidXMLChars(danmakuModel.RawObject?["info"]?.ToString(Newtonsoft.Json.Formatting.None))).ConfigureAwait(false);
|
|
|
|
|
this.xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText));
|
|
|
|
|
await this.xmlWriter.WriteEndElementAsync().ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case DanmakuMsgType.SuperChat:
|
|
|
|
|
if (this.config.RecordDanmakuSuperChat)
|
|
|
|
|
{
|
|
|
|
|
await this.xmlWriter.WriteStartElementAsync(null, "sc", null).ConfigureAwait(false);
|
2023-06-05 23:45:14 +08:00
|
|
|
|
var ts = Math.Max(this.dmTime.Elapsed.TotalSeconds, 0d);
|
2022-05-18 00:23:01 +08:00
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "ts", null, ts.ToString("F3")).ConfigureAwait(false);
|
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "user", null, RemoveInvalidXMLChars(danmakuModel.UserName)).ConfigureAwait(false);
|
2022-05-21 22:01:33 +08:00
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "uid", null, danmakuModel.UserID.ToString()).ConfigureAwait(false);
|
2022-05-18 00:23:01 +08:00
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "price", null, danmakuModel.Price.ToString()).ConfigureAwait(false);
|
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "time", null, danmakuModel.SCKeepTime.ToString()).ConfigureAwait(false);
|
|
|
|
|
if (recordDanmakuRaw)
|
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "raw", null, RemoveInvalidXMLChars(danmakuModel.RawObject?["data"]?.ToString(Newtonsoft.Json.Formatting.None))).ConfigureAwait(false);
|
|
|
|
|
this.xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText));
|
|
|
|
|
await this.xmlWriter.WriteEndElementAsync().ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case DanmakuMsgType.GiftSend:
|
|
|
|
|
if (this.config.RecordDanmakuGift)
|
|
|
|
|
{
|
|
|
|
|
await this.xmlWriter.WriteStartElementAsync(null, "gift", null).ConfigureAwait(false);
|
2023-06-05 23:45:14 +08:00
|
|
|
|
var ts = Math.Max(this.dmTime.Elapsed.TotalSeconds, 0d);
|
2022-05-18 00:23:01 +08:00
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "ts", null, ts.ToString("F3")).ConfigureAwait(false);
|
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "user", null, RemoveInvalidXMLChars(danmakuModel.UserName)).ConfigureAwait(false);
|
2022-05-21 22:01:33 +08:00
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "uid", null, danmakuModel.UserID.ToString()).ConfigureAwait(false);
|
2022-05-18 00:23:01 +08:00
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "giftname", null, RemoveInvalidXMLChars(danmakuModel.GiftName)).ConfigureAwait(false);
|
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "giftcount", null, danmakuModel.GiftCount.ToString()).ConfigureAwait(false);
|
|
|
|
|
if (recordDanmakuRaw)
|
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "raw", null, RemoveInvalidXMLChars(danmakuModel.RawObject?["data"]?.ToString(Newtonsoft.Json.Formatting.None))).ConfigureAwait(false);
|
|
|
|
|
await this.xmlWriter.WriteEndElementAsync().ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case DanmakuMsgType.GuardBuy:
|
|
|
|
|
if (this.config.RecordDanmakuGuard)
|
|
|
|
|
{
|
|
|
|
|
await this.xmlWriter.WriteStartElementAsync(null, "guard", null).ConfigureAwait(false);
|
2023-06-05 23:45:14 +08:00
|
|
|
|
var ts = Math.Max(this.dmTime.Elapsed.TotalSeconds, 0d);
|
2022-05-18 00:23:01 +08:00
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "ts", null, ts.ToString("F3")).ConfigureAwait(false);
|
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "user", null, RemoveInvalidXMLChars(danmakuModel.UserName)).ConfigureAwait(false);
|
2022-05-21 22:01:33 +08:00
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "uid", null, danmakuModel.UserID.ToString()).ConfigureAwait(false);
|
2022-05-18 00:23:01 +08:00
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "level", null, danmakuModel.UserGuardLevel.ToString()).ConfigureAwait(false); ;
|
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "count", null, danmakuModel.GiftCount.ToString()).ConfigureAwait(false);
|
|
|
|
|
if (recordDanmakuRaw)
|
|
|
|
|
await this.xmlWriter.WriteAttributeStringAsync(null, "raw", null, RemoveInvalidXMLChars(danmakuModel.RawObject?["data"]?.ToString(Newtonsoft.Json.Formatting.None))).ConfigureAwait(false);
|
|
|
|
|
await this.xmlWriter.WriteEndElementAsync().ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
write = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (write && this.writeCount++ >= this.config.RecordDanmakuFlushInterval)
|
2020-11-23 17:35:42 +08:00
|
|
|
|
{
|
2022-05-18 00:23:01 +08:00
|
|
|
|
await this.xmlWriter.FlushAsync();
|
|
|
|
|
this.writeCount = 0;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-04-26 22:45:28 +08:00
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
this.logger.Warning(ex, "写入弹幕时发生错误");
|
|
|
|
|
this.DisableCore();
|
|
|
|
|
}
|
2020-11-23 17:35:42 +08:00
|
|
|
|
finally
|
|
|
|
|
{
|
2021-01-01 14:46:27 +08:00
|
|
|
|
this.semaphoreSlim.Release();
|
2020-11-23 17:35:42 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-10 18:53:04 +08:00
|
|
|
|
private static void WriteStartDocument(XmlWriter writer, IRoom room)
|
2020-11-23 17:35:42 +08:00
|
|
|
|
{
|
|
|
|
|
writer.WriteStartDocument();
|
2020-12-15 18:53:52 +08:00
|
|
|
|
writer.WriteProcessingInstruction("xml-stylesheet", "type=\"text/xsl\" href=\"#s\"");
|
2020-11-23 17:35:42 +08:00
|
|
|
|
writer.WriteStartElement("i");
|
2022-05-19 12:57:32 +08:00
|
|
|
|
writer.WriteComment("\nB站录播姬 " + GitVersionInformation.InformationalVersion + "\nhttps://rec.danmuji.org/user/danmaku/\n本文件的弹幕信息兼容B站主站视频弹幕XML格式\n本XML自带样式可以在浏览器里打开(推荐使用Chrome)\n\nsc 为SuperChat\ngift为礼物\nguard为上船\n\nattribute \"raw\" 为原始数据\n");
|
2020-11-23 17:35:42 +08:00
|
|
|
|
writer.WriteElementString("chatserver", "chat.bilibili.com");
|
|
|
|
|
writer.WriteElementString("chatid", "0");
|
|
|
|
|
writer.WriteElementString("mission", "0");
|
|
|
|
|
writer.WriteElementString("maxlimit", "1000");
|
|
|
|
|
writer.WriteElementString("state", "0");
|
|
|
|
|
writer.WriteElementString("real_name", "0");
|
|
|
|
|
writer.WriteElementString("source", "0");
|
2021-04-29 18:08:55 +08:00
|
|
|
|
|
2020-12-15 18:53:52 +08:00
|
|
|
|
writer.WriteStartElement("BililiveRecorder");
|
2021-02-26 21:57:10 +08:00
|
|
|
|
writer.WriteAttributeString("version", GitVersionInformation.FullSemVer);
|
2020-12-15 18:53:52 +08:00
|
|
|
|
writer.WriteEndElement();
|
2021-04-29 18:08:55 +08:00
|
|
|
|
|
2020-12-15 18:53:52 +08:00
|
|
|
|
writer.WriteStartElement("BililiveRecorderRecordInfo");
|
2021-02-23 18:03:37 +08:00
|
|
|
|
writer.WriteAttributeString("roomid", room.RoomConfig.RoomId.ToString());
|
2021-03-01 21:38:13 +08:00
|
|
|
|
writer.WriteAttributeString("shortid", room.ShortId.ToString());
|
2021-08-28 21:34:00 +08:00
|
|
|
|
writer.WriteAttributeString("name", RemoveInvalidXMLChars(room.Name));
|
|
|
|
|
writer.WriteAttributeString("title", RemoveInvalidXMLChars(room.Title));
|
|
|
|
|
writer.WriteAttributeString("areanameparent", RemoveInvalidXMLChars(room.AreaNameParent));
|
|
|
|
|
writer.WriteAttributeString("areanamechild", RemoveInvalidXMLChars(room.AreaNameChild));
|
2020-12-15 18:53:52 +08:00
|
|
|
|
writer.WriteAttributeString("start_time", DateTimeOffset.Now.ToString("O"));
|
|
|
|
|
writer.WriteEndElement();
|
2021-04-29 18:08:55 +08:00
|
|
|
|
|
2021-08-10 18:53:04 +08:00
|
|
|
|
// see BililiveRecorder.ToolBox\Tool\DanmakuMerger\DanmakuMergerHandler.cs
|
2022-05-26 22:46:27 +08:00
|
|
|
|
const string style = @"<z:stylesheet version=""1.0"" id=""s"" xml:id=""s"" xmlns:z=""http://www.w3.org/1999/XSL/Transform""><z:output method=""html""/><z:template match=""/""><html><meta name=""viewport"" content=""width=device-width""/><title>B站录播姬弹幕文件 - <z:value-of select=""/i/BililiveRecorderRecordInfo/@name""/></title><style>body{margin:0}h1,h2,p,table{margin-left:5px}table{border-spacing:0}td,th{border:1px solid grey;padding:1px}th{position:sticky;top:0;background:#4098de}tr:hover{background:#d9f4ff}div{overflow:auto;max-height:80vh;max-width:100vw;width:fit-content}</style><h1><a href=""https://rec.danmuji.org"">B站录播姬</a>弹幕XML文件</h1><p>本文件不支持在 IE 浏览器里预览,请使用 Chrome Firefox Edge 等浏览器。</p><p>文件用法参考文档 <a href=""https://rec.danmuji.org/user/danmaku/"">https://rec.danmuji.org/user/danmaku/</a></p><table><tr><td>录播姬版本</td><td><z:value-of select=""/i/BililiveRecorder/@version""/></td></tr><tr><td>房间号</td><td><z:value-of select=""/i/BililiveRecorderRecordInfo/@roomid""/></td></tr><tr><td>主播名</td><td><z:value-of select=""/i/BililiveRecorderRecordInfo/@name""/></td></tr><tr><td>录制开始时间</td><td><z:value-of select=""/i/BililiveRecorderRecordInfo/@start_time""/></td></tr><tr><td><a href=""#d"">弹幕</a></td><td>共<z:value-of select=""count(/i/d)""/>条记录</td></tr><tr><td><a href=""#guard"">上船</a></td><td>共<z:value-of select=""count(/i/guard)""/>条记录</td></tr><tr><td><a href=""#sc"">SC</a></td><td>共<z:value-of select=""count(/i/sc)""/>条记录</td></tr><tr><td><a href=""#gift"">礼物</a></td><td>共<z:value-of select=""count(/i/gift)""/>条记录</td></tr></table><h2 id=""d"">弹幕</h2><div id=""dm""><table><tr><th>用户名</th><th>出现时间</th><th>用户ID</th><th>弹幕</th><th>参数</th></tr><z:for-each select=""/i/d""><tr><td><z:value-of select=""@user""/></td><td></td><td></td><td><z:value-of select="".""/></td><td><z:value-of select=""@p""/></td></tr></z:for-each></table></div><script>Array.from(document.querySelectorAll('#dm tr')).slice(1).map(t=>t.querySelectorAll('td')).forEach(t=>{let p=t[4].textContent.split(','),a=p[0];t[1].textContent=`${(Math.floor(a/60/60)+'').padStart(2,0)}:${(Math.floor(a/60%60)+'').padStart(2,0)}:${(a%60).toFixed(3).padStart(6,0)}`;t[2].innerHTML=`<a target=_blank rel=""nofollow noreferrer"" href=""https://space.bilibili.com/${p[6]}"">${p[6]}</a>`})</script><h2 id=""guard"">舰长购买</h2><div><table><tr><th>用户名</th><th>用户ID</th><th>舰长等级</th><th>购买数量</th><th>出现时间</th></tr><z:for-each select=""/i/guard""><tr><td><z:value-of select=""@user""/></td><td><a rel=""nofollow noreferrer""><z:attribute name=""href""><z:text>https://space.bilibili.com/</z:text><z:value-of select=""@uid"" /></z:attribute><z:value-of select=""@uid""/></a></td><td><z:value-of select=""@level""/></td><td><z:value-of select=""@count""/></td><td><z:value-of select=""@ts""/></td></tr></z:for-each></table></div><h2 id=""sc"">SuperChat 醒目留言</h2><div><table><tr><th>用户名</th><th>用户ID</th><th>内容</th><th>显示时长</th><th>价格</th><th>出现时间</th></tr><z:for-each select=""/i/sc""><tr><td><z:value-of select=""@user""/></td><td><a rel=""nofollow noreferrer""><z:attribute name=""href""><z:text>https://space.bilibili.com/</z:text><z:value-of select=""@uid"" /></z:attribute><z:value-of select=""@uid""/></a></td><td><z:value-of select="".""/></td><td><z:value-of select=""@time""/></td><td><z:value-of select=""@price""/></td><td><z:value-of select=""@ts""/></td></tr></z:for-each></table></div><h2 id=""gift"">礼物</h2><div><table><tr><th>用户名</th><th>用户ID</th><th>礼物名</th><th>礼物数量</th><th>出现时间</th></tr><z:for-each select=""/i/gift""><tr><td><z:value-of select=""@user""/></td><td><a rel=""nofollow noreferrer""><z:attribute name=""href""><z:text>https://space.bilibili.com/</z:text><z:value-of select=""@uid"" /></z:attribute><z:value-of select=""@uid""/></a></td><td><z:value
|
2021-04-29 18:08:55 +08:00
|
|
|
|
|
2020-12-29 07:36:27 +08:00
|
|
|
|
writer.WriteStartElement("BililiveRecorderXmlStyle");
|
|
|
|
|
writer.WriteRaw(style);
|
|
|
|
|
writer.WriteEndElement();
|
2020-11-24 20:15:21 +08:00
|
|
|
|
writer.Flush();
|
2020-11-23 17:35:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool disposedValue;
|
|
|
|
|
|
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
|
|
|
{
|
2021-01-01 14:46:27 +08:00
|
|
|
|
if (!this.disposedValue)
|
2020-11-23 17:35:42 +08:00
|
|
|
|
{
|
|
|
|
|
if (disposing)
|
|
|
|
|
{
|
|
|
|
|
// dispose managed state (managed objects)
|
2021-01-01 14:46:27 +08:00
|
|
|
|
this.semaphoreSlim.Dispose();
|
|
|
|
|
this.xmlWriter?.Close();
|
|
|
|
|
this.xmlWriter?.Dispose();
|
2021-04-26 22:45:28 +08:00
|
|
|
|
this.xmlWriter = null;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// free unmanaged resources (unmanaged objects) and override finalizer
|
|
|
|
|
// set large fields to null
|
2021-01-01 14:46:27 +08:00
|
|
|
|
this.disposedValue = true;
|
2020-11-23 17:35:42 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
2021-01-01 14:46:27 +08:00
|
|
|
|
this.Dispose(disposing: true);
|
2020-11-23 17:35:42 +08:00
|
|
|
|
GC.SuppressFinalize(this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|