using System; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Xml; using BililiveRecorder.Core.Api.Danmaku; using BililiveRecorder.Core.Config.V2; #nullable enable namespace BililiveRecorder.Core.Danmaku { public class BasicDanmakuWriter : IBasicDanmakuWriter { private static readonly XmlWriterSettings xmlWriterSettings = new XmlWriterSettings { Indent = true, IndentChars = " ", Encoding = Encoding.UTF8, CloseOutput = true, WriteEndDocumentOnClose = true }; private static readonly Regex invalidXMLChars = new Regex(@"(? string.IsNullOrEmpty(text) ? string.Empty : invalidXMLChars.Replace(text, string.Empty); private XmlWriter? xmlWriter = null; private DateTimeOffset offset = DateTimeOffset.UtcNow; private uint writeCount = 0; private RoomConfig? config; public BasicDanmakuWriter() { } private readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); public void EnableWithPath(string path, IRoom room) { if (this.disposedValue) return; this.semaphoreSlim.Wait(); try { if (this.xmlWriter != null) { this.xmlWriter.Close(); this.xmlWriter.Dispose(); this.xmlWriter = null; } try { Directory.CreateDirectory(Path.GetDirectoryName(path)); } catch (Exception) { } var stream = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.Read); this.config = room.RoomConfig; this.xmlWriter = XmlWriter.Create(stream, xmlWriterSettings); this.WriteStartDocument(this.xmlWriter, room); this.offset = DateTimeOffset.UtcNow; this.writeCount = 0; } finally { this.semaphoreSlim.Release(); } } public void Disable() { if (this.disposedValue) return; this.semaphoreSlim.Wait(); try { if (this.xmlWriter != null) { this.xmlWriter.Close(); this.xmlWriter.Dispose(); this.xmlWriter = null; } } finally { this.semaphoreSlim.Release(); } } public void Write(DanmakuModel danmakuModel) { if (this.disposedValue) return; if (this.config is null) return; this.semaphoreSlim.Wait(); try { if (this.xmlWriter != null) { var write = true; var recordDanmakuRaw = this.config.RecordDanmakuRaw; switch (danmakuModel.MsgType) { case DanmakuMsgType.Comment: { var type = danmakuModel.RawObject?["info"]?[0]?[1]?.ToObject() ?? 1; var size = danmakuModel.RawObject?["info"]?[0]?[2]?.ToObject() ?? 25; var color = danmakuModel.RawObject?["info"]?[0]?[3]?.ToObject() ?? 0XFFFFFF; var st = danmakuModel.RawObject?["info"]?[0]?[4]?.ToObject() ?? 0L; var ts = Math.Max((DateTimeOffset.FromUnixTimeMilliseconds(st) - this.offset).TotalSeconds, 0d); this.xmlWriter.WriteStartElement("d"); this.xmlWriter.WriteAttributeString("p", $"{ts},{type},{size},{color},{st},0,{danmakuModel.UserID},0"); this.xmlWriter.WriteAttributeString("user", danmakuModel.UserName); if (recordDanmakuRaw) this.xmlWriter.WriteAttributeString("raw", danmakuModel.RawObject?["info"]?.ToString(Newtonsoft.Json.Formatting.None)); this.xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText)); this.xmlWriter.WriteEndElement(); } break; case DanmakuMsgType.SuperChat: if (this.config.RecordDanmakuSuperChat) { this.xmlWriter.WriteStartElement("sc"); var ts = Math.Max((DateTimeOffset.UtcNow - this.offset).TotalSeconds, 0d); this.xmlWriter.WriteAttributeString("ts", ts.ToString()); this.xmlWriter.WriteAttributeString("user", danmakuModel.UserName); this.xmlWriter.WriteAttributeString("price", danmakuModel.Price.ToString()); this.xmlWriter.WriteAttributeString("time", danmakuModel.SCKeepTime.ToString()); if (recordDanmakuRaw) this.xmlWriter.WriteAttributeString("raw", danmakuModel.RawObject?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); this.xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText)); this.xmlWriter.WriteEndElement(); } break; case DanmakuMsgType.GiftSend: if (this.config.RecordDanmakuGift) { this.xmlWriter.WriteStartElement("gift"); var ts = Math.Max((DateTimeOffset.UtcNow - this.offset).TotalSeconds, 0d); this.xmlWriter.WriteAttributeString("ts", ts.ToString()); this.xmlWriter.WriteAttributeString("user", danmakuModel.UserName); this.xmlWriter.WriteAttributeString("giftname", danmakuModel.GiftName); this.xmlWriter.WriteAttributeString("giftcount", danmakuModel.GiftCount.ToString()); if (recordDanmakuRaw) this.xmlWriter.WriteAttributeString("raw", danmakuModel.RawObject?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); this.xmlWriter.WriteEndElement(); } break; case DanmakuMsgType.GuardBuy: if (this.config.RecordDanmakuGuard) { this.xmlWriter.WriteStartElement("guard"); var ts = Math.Max((DateTimeOffset.UtcNow - this.offset).TotalSeconds, 0d); this.xmlWriter.WriteAttributeString("ts", ts.ToString()); this.xmlWriter.WriteAttributeString("user", danmakuModel.UserName); this.xmlWriter.WriteAttributeString("level", danmakuModel.UserGuardLevel.ToString()); ; this.xmlWriter.WriteAttributeString("count", danmakuModel.GiftCount.ToString()); if (recordDanmakuRaw) this.xmlWriter.WriteAttributeString("raw", danmakuModel.RawObject?["data"]?.ToString(Newtonsoft.Json.Formatting.None)); this.xmlWriter.WriteEndElement(); } break; default: write = false; break; } if (write && this.writeCount++ >= this.config.RecordDanmakuFlushInterval) { this.xmlWriter.Flush(); this.writeCount = 0; } } } finally { this.semaphoreSlim.Release(); } } private void WriteStartDocument(XmlWriter writer, IRoom room) { writer.WriteStartDocument(); writer.WriteProcessingInstruction("xml-stylesheet", "type=\"text/xsl\" href=\"#s\""); writer.WriteStartElement("i"); writer.WriteComment("\nB站录播姬 " + GitVersionInformation.FullSemVer + "\n本文件的弹幕信息兼容B站主站视频弹幕XML格式\n本XML自带样式可以在浏览器里打开(推荐使用Chrome)\n\nsc 为SuperChat\ngift为礼物\nguard为上船\n\nattribute \"raw\" 为原始数据\n"); 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"); writer.WriteStartElement("BililiveRecorder"); writer.WriteAttributeString("version", GitVersionInformation.FullSemVer); writer.WriteEndElement(); writer.WriteStartElement("BililiveRecorderRecordInfo"); writer.WriteAttributeString("roomid", room.RoomConfig.RoomId.ToString()); writer.WriteAttributeString("shortid", room.ShortId.ToString()); writer.WriteAttributeString("name", room.Name); writer.WriteAttributeString("title", room.Title); writer.WriteAttributeString("areanameparent", room.AreaNameParent); writer.WriteAttributeString("areanamechild", room.AreaNameChild); writer.WriteAttributeString("start_time", DateTimeOffset.Now.ToString("O")); writer.WriteEndElement(); const string style = @"B站录播姬弹幕文件 - <z:value-of select=""/i/BililiveRecorderRecordInfo/@name""/>

B站录播姬弹幕XML文件

本文件的弹幕信息兼容B站主站视频弹幕XML格式,可以使用现有的转换工具把文件中的弹幕转为ass字幕文件

录播姬版本
房间号
主播名
录制开始时间
弹幕 条记录
上船 条记录
SC 条记录
礼物 条记录

弹幕

用户名弹幕参数

舰长购买

用户名舰长等级购买数量出现时间

SuperChat 醒目留言

用户名内容显示时长价格出现时间

礼物

用户名礼物名礼物数量出现时间
"; writer.WriteStartElement("BililiveRecorderXmlStyle"); writer.WriteRaw(style); writer.WriteEndElement(); writer.Flush(); } private bool disposedValue; protected virtual void Dispose(bool disposing) { if (!this.disposedValue) { if (disposing) { // dispose managed state (managed objects) this.semaphoreSlim.Dispose(); this.xmlWriter?.Close(); this.xmlWriter?.Dispose(); } // free unmanaged resources (unmanaged objects) and override finalizer // set large fields to null this.disposedValue = true; } } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method this.Dispose(disposing: true); GC.SuppressFinalize(this); } } }