mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 03:32:20 +08:00
Toolbox: Add danmaku merger
This commit is contained in:
parent
df8559424c
commit
5fb7d074d6
|
@ -60,7 +60,7 @@ namespace BililiveRecorder.Core.Danmaku
|
|||
this.config = room.RoomConfig;
|
||||
|
||||
this.xmlWriter = XmlWriter.Create(stream, xmlWriterSettings);
|
||||
this.WriteStartDocument(this.xmlWriter, room);
|
||||
WriteStartDocument(this.xmlWriter, room);
|
||||
this.offset = DateTimeOffset.UtcNow;
|
||||
this.writeCount = 0;
|
||||
}
|
||||
|
@ -201,7 +201,7 @@ namespace BililiveRecorder.Core.Danmaku
|
|||
}
|
||||
}
|
||||
|
||||
private void WriteStartDocument(XmlWriter writer, IRoom room)
|
||||
private static void WriteStartDocument(XmlWriter writer, IRoom room)
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteProcessingInstruction("xml-stylesheet", "type=\"text/xsl\" href=\"#s\"");
|
||||
|
@ -229,6 +229,7 @@ namespace BililiveRecorder.Core.Danmaku
|
|||
writer.WriteAttributeString("start_time", DateTimeOffset.Now.ToString("O"));
|
||||
writer.WriteEndElement();
|
||||
|
||||
// see BililiveRecorder.ToolBox\Tool\DanmakuMerger\DanmakuMergerHandler.cs
|
||||
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>B站录播姬弹幕XML文件</h1><p>本文件的弹幕信息兼容B站主站视频弹幕XML格式,可以使用现有的转换工具把文件中的弹幕转为ass字幕文件</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><table><tr><th>用户名</th><th>弹幕</th><th>参数</th></tr><z:for-each select=""/i/d""><tr><td><z:value-of select=""@user""/></td><td><z:value-of select="".""/></td><td><z:value-of select=""@p""/></td></tr></z:for-each></table></div><h2 id=""guard"">舰长购买</h2><div><table><tr><th>用户名</th><th>舰长等级</th><th>购买数量</th><th>出现时间</th></tr><z:for-each select=""/i/guard""><tr><td><z:value-of select=""@user""/></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>内容</th><th>显示时长</th><th>价格</th><th>出现时间</th></tr><z:for-each select=""/i/sc""><tr><td><z:value-of select=""@user""/></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>礼物名</th><th>礼物数量</th><th>出现时间</th></tr><z:for-each select=""/i/gift""><tr><td><z:value-of select=""@user""/></td><td><z:value-of select=""@giftname""/></td><td><z:value-of select=""@giftcount""/></td><td><z:value-of select=""@ts""/></td></tr></z:for-each></table></div></html></z:template></z:stylesheet>";
|
||||
|
||||
writer.WriteStartElement("BililiveRecorderXmlStyle");
|
||||
|
|
|
@ -0,0 +1,288 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace BililiveRecorder.ToolBox.Tool.DanmakuMerger
|
||||
{
|
||||
public class DanmakuMergerHandler : ICommandHandler<DanmakuMergerRequest, DanmakuMergerResponse>
|
||||
{
|
||||
private static readonly string[] DanmakuElementNames = new[] { "d", "gift", "sc", "guard" };
|
||||
|
||||
public string Name => "Merge Danmaku";
|
||||
|
||||
public async Task<CommandResponse<DanmakuMergerResponse>> Handle(DanmakuMergerRequest request, CancellationToken cancellationToken, ProgressCallback? progress)
|
||||
{
|
||||
var inputLength = request.Inputs.Length;
|
||||
|
||||
if (inputLength < 2)
|
||||
return new CommandResponse<DanmakuMergerResponse>
|
||||
{
|
||||
Status = ResponseStatus.Error,
|
||||
ErrorMessage = "At least 2 input files required"
|
||||
};
|
||||
|
||||
var files = new FileStream[inputLength];
|
||||
var readers = new XmlReader?[inputLength];
|
||||
|
||||
FileStream? outputFile = null;
|
||||
XmlWriter? writer = null;
|
||||
XElement recordInfo;
|
||||
|
||||
DateTimeOffset baseTime;
|
||||
TimeSpan[] timeDiff;
|
||||
|
||||
try // finally
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
// 打开输入文件
|
||||
for (var i = 0; i < inputLength; i++)
|
||||
{
|
||||
var file = File.Open(request.Inputs[i], FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
files[i] = file;
|
||||
readers[i] = XmlReader.Create(file, null);
|
||||
}
|
||||
|
||||
// 计算时间差
|
||||
var startTimes = new (DateTimeOffset time, XElement element)[inputLength];
|
||||
for (var i = 0; i < inputLength; i++)
|
||||
{
|
||||
var r = readers[i]!;
|
||||
r.ReadStartElement("i");
|
||||
while (r.Name != "i")
|
||||
{
|
||||
if (r.Name == "BililiveRecorderRecordInfo")
|
||||
{
|
||||
var el = (XNode.ReadFrom(r) as XElement)!;
|
||||
var time = (DateTimeOffset)el.Attribute("start_time");
|
||||
startTimes[i] = (time, el);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
r.Skip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var b = startTimes.OrderBy(x => x.time).First();
|
||||
recordInfo = b.element;
|
||||
baseTime = b.time;
|
||||
timeDiff = startTimes.Select(x => x.time - baseTime).ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CommandResponse<DanmakuMergerResponse>
|
||||
{
|
||||
Status = ResponseStatus.InputIOError,
|
||||
Exception = ex,
|
||||
ErrorMessage = ex.Message
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 打开输出文件
|
||||
outputFile = File.Open(request.Output, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
||||
writer = XmlWriter.Create(outputFile, new XmlWriterSettings
|
||||
{
|
||||
Indent = true,
|
||||
IndentChars = " ",
|
||||
Encoding = Encoding.UTF8,
|
||||
CloseOutput = true,
|
||||
WriteEndDocumentOnClose = true,
|
||||
});
|
||||
|
||||
// 写入文件开头
|
||||
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.WriteAttributeString("merged", "by toolbox");
|
||||
writer.WriteEndElement();
|
||||
|
||||
// 写入直播间信息
|
||||
recordInfo.WriteTo(writer);
|
||||
|
||||
// see BililiveRecorder.Core\Danmaku\BasicDanmakuWriter.cs
|
||||
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>B站录播姬弹幕XML文件</h1><p>本文件的弹幕信息兼容B站主站视频弹幕XML格式,可以使用现有的转换工具把文件中的弹幕转为ass字幕文件</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><table><tr><th>用户名</th><th>弹幕</th><th>参数</th></tr><z:for-each select=""/i/d""><tr><td><z:value-of select=""@user""/></td><td><z:value-of select="".""/></td><td><z:value-of select=""@p""/></td></tr></z:for-each></table></div><h2 id=""guard"">舰长购买</h2><div><table><tr><th>用户名</th><th>舰长等级</th><th>购买数量</th><th>出现时间</th></tr><z:for-each select=""/i/guard""><tr><td><z:value-of select=""@user""/></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>内容</th><th>显示时长</th><th>价格</th><th>出现时间</th></tr><z:for-each select=""/i/sc""><tr><td><z:value-of select=""@user""/></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>礼物名</th><th>礼物数量</th><th>出现时间</th></tr><z:for-each select=""/i/gift""><tr><td><z:value-of select=""@user""/></td><td><z:value-of select=""@giftname""/></td><td><z:value-of select=""@giftcount""/></td><td><z:value-of select=""@ts""/></td></tr></z:for-each></table></div></html></z:template></z:stylesheet>";
|
||||
|
||||
writer.WriteStartElement("BililiveRecorderXmlStyle");
|
||||
writer.WriteRaw(style);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CommandResponse<DanmakuMergerResponse>
|
||||
{
|
||||
Status = ResponseStatus.OutputIOError,
|
||||
Exception = ex,
|
||||
ErrorMessage = ex.Message
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var els = new List<(TimeSpan time, XElement el, int reader)>();
|
||||
|
||||
// 取出所有文件里第一条数据
|
||||
for (var i = 0; i < inputLength; i++)
|
||||
{
|
||||
var r = readers[i]!;
|
||||
var el = ReadDanmakuElement(r);
|
||||
if (el is null)
|
||||
{
|
||||
readers[i] = null;
|
||||
continue;
|
||||
}
|
||||
var time = UpdateTimestamp(el, timeDiff[i]);
|
||||
els.Add((time, el, i));
|
||||
}
|
||||
|
||||
// 排序
|
||||
els.Sort((a, b) => a.time.CompareTo(b.time));
|
||||
|
||||
while (true)
|
||||
{
|
||||
// 写入时间最小的数据
|
||||
|
||||
// 所有数据写完就退出循环
|
||||
if (els.Count == 0)
|
||||
break;
|
||||
|
||||
(var time, var el, var readerIndex) = els[0];
|
||||
el.WriteTo(writer);
|
||||
els.RemoveAt(0);
|
||||
|
||||
// 读取一个新的数据
|
||||
|
||||
var reader = readers[readerIndex];
|
||||
// 检查这个文件是否还有更多数据
|
||||
if (reader is not null)
|
||||
{
|
||||
readNextElementFromSameReader:
|
||||
var newEl = ReadDanmakuElement(reader);
|
||||
if (newEl is null)
|
||||
{
|
||||
// 文件已结束
|
||||
reader.Dispose();
|
||||
readers[readerIndex] = null;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 计算新的时间
|
||||
var newTime = UpdateTimestamp(newEl, timeDiff[readerIndex]);
|
||||
if (els.Count < 1 || newTime < els[0].time)
|
||||
{
|
||||
// 如果这是最后一个文件,或当前数据的时间是所有数据里最小的
|
||||
// 直接写入输出文件
|
||||
newEl.WriteTo(writer);
|
||||
goto readNextElementFromSameReader;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果其他数据比本数据的时间更小
|
||||
// 添加到列表中,后面排序再来
|
||||
els.Add((newTime, newEl, readerIndex));
|
||||
els.Sort((a, b) => a.time.CompareTo(b.time));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CommandResponse<DanmakuMergerResponse>
|
||||
{
|
||||
Status = ResponseStatus.Error,
|
||||
Exception = ex,
|
||||
ErrorMessage = ex.Message
|
||||
};
|
||||
}
|
||||
|
||||
return new CommandResponse<DanmakuMergerResponse> { Status = ResponseStatus.OK, Data = new DanmakuMergerResponse() };
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
writer?.Dispose();
|
||||
outputFile?.Dispose();
|
||||
}
|
||||
catch (Exception) { }
|
||||
|
||||
for (var i = 0; i < inputLength; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
readers[i]?.Dispose();
|
||||
files[i]?.Dispose();
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static XElement? ReadDanmakuElement(XmlReader r)
|
||||
{
|
||||
while (r.Name != "i")
|
||||
{
|
||||
if (DanmakuElementNames.Contains(r.Name))
|
||||
{
|
||||
var el = (XNode.ReadFrom(r) as XElement)!;
|
||||
return el;
|
||||
}
|
||||
else
|
||||
{
|
||||
r.Skip();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static TimeSpan UpdateTimestamp(XElement element, TimeSpan offset)
|
||||
{
|
||||
switch (element.Name.LocalName)
|
||||
{
|
||||
case "d":
|
||||
{
|
||||
var p = element.Attribute("p");
|
||||
var i = p.Value.IndexOf(',');
|
||||
var t = TimeSpan.FromSeconds(double.Parse(p.Value.Substring(0, i)));
|
||||
t += offset;
|
||||
p.Value = t.TotalSeconds.ToString("F3") + p.Value.Substring(i);
|
||||
return t;
|
||||
}
|
||||
case "gift":
|
||||
case "sc":
|
||||
case "guard":
|
||||
{
|
||||
var ts = TimeSpan.FromSeconds((double)element.Attribute("ts"));
|
||||
ts += offset;
|
||||
element.SetAttributeValue("ts", ts.TotalSeconds.ToString("F3"));
|
||||
return ts;
|
||||
}
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
|
||||
namespace BililiveRecorder.ToolBox.Tool.DanmakuMerger
|
||||
{
|
||||
public class DanmakuMergerRequest : ICommandRequest<DanmakuMergerResponse>
|
||||
{
|
||||
public string[] Inputs { get; set; } = Array.Empty<string>();
|
||||
|
||||
public string Output { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace BililiveRecorder.ToolBox.Tool.DanmakuMerger
|
||||
{
|
||||
public class DanmakuMergerResponse : IResponseData
|
||||
{
|
||||
public void PrintToConsole() { }
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ using System.CommandLine;
|
|||
using System.CommandLine.Invocation;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.ToolBox.Tool.Analyze;
|
||||
using BililiveRecorder.ToolBox.Tool.DanmakuMerger;
|
||||
using BililiveRecorder.ToolBox.Tool.Export;
|
||||
using BililiveRecorder.ToolBox.Tool.Fix;
|
||||
using Newtonsoft.Json;
|
||||
|
@ -30,6 +31,12 @@ namespace BililiveRecorder.ToolBox
|
|||
c.Add(new Argument<string>("input", "example: input.flv"));
|
||||
c.Add(new Argument<string>("output", "example: output.brec.xml.gz"));
|
||||
});
|
||||
|
||||
this.RegisterCommand<DanmakuMergerHandler, DanmakuMergerRequest, DanmakuMergerResponse>("danmaku-merge", null, c =>
|
||||
{
|
||||
c.Add(new Argument<string>("output", "example: output.xml"));
|
||||
c.Add(new Argument<string[]>("inputs", "example: 1.xml 2.xml ..."));
|
||||
});
|
||||
}
|
||||
|
||||
private void RegisterCommand<THandler, TRequest, TResponse>(string name, string? description, Action<Command> configure)
|
||||
|
|
|
@ -144,6 +144,9 @@
|
|||
<Compile Include="Pages\ToolboxAutoFixPage.xaml.cs">
|
||||
<DependentUpon>ToolboxAutoFixPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Pages\ToolboxDanmakuMergerPage.xaml.cs">
|
||||
<DependentUpon>ToolboxDanmakuMergerPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Pages\ToolboxRemuxPage.xaml.cs">
|
||||
<DependentUpon>ToolboxRemuxPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
|
@ -245,6 +248,10 @@
|
|||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Pages\ToolboxDanmakuMergerPage.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Pages\ToolboxRemuxPage.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
|
|
@ -108,6 +108,12 @@
|
|||
<ui:PathIcon Style="{StaticResource PathIconDataAlphaRCircleOutline}"/>
|
||||
</ui:NavigationViewItem.Icon>
|
||||
</ui:NavigationViewItem>
|
||||
<ui:NavigationViewItem l:ResxLocalizationProvider.DefaultDictionary="Strings"
|
||||
Content="{l:Loc Toolbox_Merge_Title}" Tag="ToolboxDanmakuMergerPage">
|
||||
<ui:NavigationViewItem.Icon>
|
||||
<ui:PathIcon Style="{StaticResource PathIconDataMerge}"/>
|
||||
</ui:NavigationViewItem.Icon>
|
||||
</ui:NavigationViewItem>
|
||||
</ui:NavigationViewItem.MenuItems>
|
||||
</ui:NavigationViewItem>
|
||||
</ui:NavigationView.MenuItems>
|
||||
|
|
|
@ -54,6 +54,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
AddType(typeof(AnnouncementPage));
|
||||
AddType(typeof(ToolboxAutoFixPage));
|
||||
AddType(typeof(ToolboxRemuxPage));
|
||||
AddType(typeof(ToolboxDanmakuMergerPage));
|
||||
|
||||
this.Model = new RootModel();
|
||||
this.DataContext = this.Model;
|
||||
|
|
46
BililiveRecorder.WPF/Pages/ToolboxDanmakuMergerPage.xaml
Normal file
46
BililiveRecorder.WPF/Pages/ToolboxDanmakuMergerPage.xaml
Normal file
|
@ -0,0 +1,46 @@
|
|||
<ui:Page
|
||||
x:Class="BililiveRecorder.WPF.Pages.ToolboxDanmakuMergerPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:l="https://github.com/XAMLMarkupExtensions/WPFLocalizationExtension"
|
||||
l:LocalizeDictionary.DesignCulture=""
|
||||
l:ResxLocalizationProvider.DefaultAssembly="BililiveRecorder.WPF"
|
||||
l:ResxLocalizationProvider.DefaultDictionary="Strings"
|
||||
mc:Ignorable="d" DataContext="{x:Null}"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
Title="ToolboxDanmakuMergerPage">
|
||||
<Grid>
|
||||
<Grid.Resources>
|
||||
<ResourceDictionary Source="pack://application:,,,/ModernWpf;component/DensityStyles/Compact.xaml" />
|
||||
</Grid.Resources>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock FontSize="20" TextAlignment="Center" Text="{l:Loc Toolbox_Merge_Title_Long}"/>
|
||||
<TextBlock Margin="2" Grid.Row="1" TextAlignment="Center" Text="{l:Loc Toolbox_Merge_Tip}"/>
|
||||
<StackPanel Grid.Row="2" Margin="5,5,5,0" HorizontalAlignment="Center" Orientation="Horizontal">
|
||||
<Button Margin="0,0,5,0" Content="{l:Loc Toolbox_Merge_Button_AddFile}" Click="AddFile_Click"/>
|
||||
<Button Content="{l:Loc Toolbox_Merge_Button_Merge}" Click="Merge_Click"/>
|
||||
</StackPanel>
|
||||
<ListBox Grid.Row="3" Margin="5" x:Name="listBox" AllowDrop="True" Drop="listBox_Drop">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Margin="0,0,5,0" Padding="2" Content="{l:Loc Toolbox_Merge_Button_Remove}" Click="RemoveFile_Click"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding}" ToolTip="{Binding}" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</ui:Page>
|
191
BililiveRecorder.WPF/Pages/ToolboxDanmakuMergerPage.xaml.cs
Normal file
191
BililiveRecorder.WPF/Pages/ToolboxDanmakuMergerPage.xaml.cs
Normal file
|
@ -0,0 +1,191 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using BililiveRecorder.ToolBox;
|
||||
using BililiveRecorder.ToolBox.Tool.DanmakuMerger;
|
||||
using BililiveRecorder.WPF.Controls;
|
||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||
using Serilog;
|
||||
using WPFLocalizeExtension.Extensions;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.WPF.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for ToolboxDanmakuMergerPage.xaml
|
||||
/// </summary>
|
||||
public partial class ToolboxDanmakuMergerPage
|
||||
{
|
||||
private static readonly ILogger logger = Log.ForContext<ToolboxDanmakuMergerPage>();
|
||||
private static readonly string DesktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
|
||||
private readonly ObservableCollection<string> Files = new ObservableCollection<string>();
|
||||
|
||||
public ToolboxDanmakuMergerPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
this.listBox.ItemsSource = this.Files;
|
||||
}
|
||||
|
||||
private void RemoveFile_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var b = (Button)sender;
|
||||
var f = (string)b.DataContext;
|
||||
this.Files.Remove(f);
|
||||
}
|
||||
|
||||
private void listBox_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
{
|
||||
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
|
||||
for (var i = 0; i < files.Length; i++)
|
||||
{
|
||||
var file = files[i];
|
||||
if (file.EndsWith(".xml", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
this.Files.Add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{ }
|
||||
}
|
||||
|
||||
private void AddFile_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var d = new CommonOpenFileDialog
|
||||
{
|
||||
Title = LocExtension.GetLocalizedValue<string>("BililiveRecorder.WPF:Strings:Toolbox_Merge_OpenFileDialogTitle"),
|
||||
AllowNonFileSystemItems = false,
|
||||
EnsureFileExists = true,
|
||||
EnsurePathExists = true,
|
||||
EnsureValidNames = true,
|
||||
DefaultDirectory = DesktopPath,
|
||||
DefaultExtension = "xml",
|
||||
Multiselect = true,
|
||||
};
|
||||
|
||||
d.Filters.Add(new CommonFileDialogFilter(LocExtension.GetLocalizedValue<string>("BililiveRecorder.WPF:Strings:Toolbox_Merge_XmlDanmakuFiles"), "*.xml"));
|
||||
|
||||
if (d.ShowDialog() != CommonFileDialogResult.Ok)
|
||||
return;
|
||||
|
||||
foreach (var file in d.FileNames)
|
||||
{
|
||||
this.Files.Add(file);
|
||||
}
|
||||
}
|
||||
|
||||
private async void Merge_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AutoFixProgressDialog? progressDialog = null;
|
||||
|
||||
try
|
||||
{
|
||||
var inputPaths = this.Files.Distinct().ToArray();
|
||||
|
||||
if (inputPaths.Length < 2)
|
||||
{
|
||||
MessageBox.Show(LocExtension.GetLocalizedValue<string>("BililiveRecorder.WPF:Strings:Toolbox_Merge_Error_AtLeastTwo"),
|
||||
LocExtension.GetLocalizedValue<string>("BililiveRecorder.WPF:Strings:Toolbox_Merge_Title"),
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.Debug("合并弹幕文件 {Paths}", inputPaths);
|
||||
|
||||
progressDialog = new AutoFixProgressDialog()
|
||||
{
|
||||
CancelButtonVisibility = Visibility.Collapsed,
|
||||
CancellationTokenSource = new CancellationTokenSource()
|
||||
};
|
||||
var token = progressDialog.CancellationTokenSource.Token;
|
||||
var showTask = progressDialog.ShowAsync();
|
||||
|
||||
string? outputPath;
|
||||
{
|
||||
var fileDialog = new CommonSaveFileDialog()
|
||||
{
|
||||
Title = LocExtension.GetLocalizedValue<string>("BililiveRecorder.WPF:Strings:Toolbox_AutoFix_SelectOutputDialog_Title"),
|
||||
AlwaysAppendDefaultExtension = true,
|
||||
AddToMostRecentlyUsedList = false,
|
||||
DefaultExtension = "xml",
|
||||
EnsurePathExists = true,
|
||||
EnsureValidNames = true,
|
||||
NavigateToShortcut = true,
|
||||
OverwritePrompt = false,
|
||||
InitialDirectory = Path.GetDirectoryName(inputPaths[0]),
|
||||
};
|
||||
|
||||
fileDialog.Filters.Add(new CommonFileDialogFilter(LocExtension.GetLocalizedValue<string>("BililiveRecorder.WPF:Strings:Toolbox_Merge_XmlDanmakuFiles"), "*.xml"));
|
||||
|
||||
if (fileDialog.ShowDialog() == CommonFileDialogResult.Ok)
|
||||
outputPath = fileDialog.FileName;
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
var req = new DanmakuMergerRequest
|
||||
{
|
||||
Inputs = inputPaths,
|
||||
Output = outputPath,
|
||||
};
|
||||
|
||||
var handler = new DanmakuMergerHandler();
|
||||
|
||||
var resp = await handler.Handle(req, token, async p =>
|
||||
{
|
||||
await this.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
progressDialog.Progress = (int)(p * 98d);
|
||||
});
|
||||
}).ConfigureAwait(true);
|
||||
|
||||
logger.Debug("弹幕合并结果 {@Response}", resp);
|
||||
|
||||
if (resp.Status != ResponseStatus.Cancelled && resp.Status != ResponseStatus.OK)
|
||||
{
|
||||
logger.Warning(resp.Exception, "弹幕合并时发生错误 (@Status)", resp.Status);
|
||||
await Task.Run(() => ShowErrorMessageBox(resp)).ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Files.Clear();
|
||||
}
|
||||
|
||||
progressDialog.Hide();
|
||||
await showTask.ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "弹幕合并时发生未处理的错误");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = this.Dispatcher.BeginInvoke((Action)(() => progressDialog?.Hide()));
|
||||
progressDialog?.CancellationTokenSource?.Cancel();
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShowErrorMessageBox<T>(CommandResponse<T> resp) where T : IResponseData
|
||||
{
|
||||
var title = LocExtension.GetLocalizedValue<string>("BililiveRecorder.WPF:Strings:Toolbox_AutoFix_Error_Title");
|
||||
var type = LocExtension.GetLocalizedValue<string>("BililiveRecorder.WPF:Strings:Toolbox_AutoFix_Error_Type_" + resp.Status.ToString());
|
||||
MessageBox.Show($"{type}\n{resp.ErrorMessage}", title, MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
}
|
81
BililiveRecorder.WPF/Properties/Strings.Designer.cs
generated
81
BililiveRecorder.WPF/Properties/Strings.Designer.cs
generated
|
@ -1567,6 +1567,87 @@ namespace BililiveRecorder.WPF.Properties {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 添加文件.
|
||||
/// </summary>
|
||||
public static string Toolbox_Merge_Button_AddFile {
|
||||
get {
|
||||
return ResourceManager.GetString("Toolbox_Merge_Button_AddFile", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 合并.
|
||||
/// </summary>
|
||||
public static string Toolbox_Merge_Button_Merge {
|
||||
get {
|
||||
return ResourceManager.GetString("Toolbox_Merge_Button_Merge", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 移除.
|
||||
/// </summary>
|
||||
public static string Toolbox_Merge_Button_Remove {
|
||||
get {
|
||||
return ResourceManager.GetString("Toolbox_Merge_Button_Remove", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 最少需要 2 个文件才能合并.
|
||||
/// </summary>
|
||||
public static string Toolbox_Merge_Error_AtLeastTwo {
|
||||
get {
|
||||
return ResourceManager.GetString("Toolbox_Merge_Error_AtLeastTwo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 选择要合并的 XML 弹幕文件.
|
||||
/// </summary>
|
||||
public static string Toolbox_Merge_OpenFileDialogTitle {
|
||||
get {
|
||||
return ResourceManager.GetString("Toolbox_Merge_OpenFileDialogTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 点击下方添加按钮,或拖动文件到此界面.
|
||||
/// </summary>
|
||||
public static string Toolbox_Merge_Tip {
|
||||
get {
|
||||
return ResourceManager.GetString("Toolbox_Merge_Tip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 弹幕合并.
|
||||
/// </summary>
|
||||
public static string Toolbox_Merge_Title {
|
||||
get {
|
||||
return ResourceManager.GetString("Toolbox_Merge_Title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 弹幕 XML 文件合并.
|
||||
/// </summary>
|
||||
public static string Toolbox_Merge_Title_Long {
|
||||
get {
|
||||
return ResourceManager.GetString("Toolbox_Merge_Title_Long", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to XML 弹幕文件.
|
||||
/// </summary>
|
||||
public static string Toolbox_Merge_XmlDanmakuFiles {
|
||||
get {
|
||||
return ResourceManager.GetString("Toolbox_Merge_XmlDanmakuFiles", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 选择需要转封装的 FLV 文件.
|
||||
/// </summary>
|
||||
|
|
|
@ -647,6 +647,33 @@ Will use default setting and disable user input when checked.</comment>
|
|||
<data name="Toolbox_AutoFix_Title" xml:space="preserve">
|
||||
<value>录播修复</value>
|
||||
</data>
|
||||
<data name="Toolbox_Merge_Button_AddFile" xml:space="preserve">
|
||||
<value>添加文件</value>
|
||||
</data>
|
||||
<data name="Toolbox_Merge_Button_Merge" xml:space="preserve">
|
||||
<value>合并</value>
|
||||
</data>
|
||||
<data name="Toolbox_Merge_Button_Remove" xml:space="preserve">
|
||||
<value>移除</value>
|
||||
</data>
|
||||
<data name="Toolbox_Merge_Error_AtLeastTwo" xml:space="preserve">
|
||||
<value>最少需要 2 个文件才能合并</value>
|
||||
</data>
|
||||
<data name="Toolbox_Merge_OpenFileDialogTitle" xml:space="preserve">
|
||||
<value>选择要合并的 XML 弹幕文件</value>
|
||||
</data>
|
||||
<data name="Toolbox_Merge_Tip" xml:space="preserve">
|
||||
<value>点击下方添加按钮,或拖动文件到此界面</value>
|
||||
</data>
|
||||
<data name="Toolbox_Merge_Title" xml:space="preserve">
|
||||
<value>弹幕合并</value>
|
||||
</data>
|
||||
<data name="Toolbox_Merge_Title_Long" xml:space="preserve">
|
||||
<value>弹幕 XML 文件合并</value>
|
||||
</data>
|
||||
<data name="Toolbox_Merge_XmlDanmakuFiles" xml:space="preserve">
|
||||
<value>XML 弹幕文件</value>
|
||||
</data>
|
||||
<data name="Toolbox_Remux_OpenFileTitle" xml:space="preserve">
|
||||
<value>选择需要转封装的 FLV 文件</value>
|
||||
</data>
|
||||
|
|
|
@ -94,6 +94,9 @@
|
|||
<Style TargetType="ui:PathIcon" x:Key="PathIconDataAlphaRCircleOutline">
|
||||
<Setter Property="Data" Value="M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"/>
|
||||
</Style>
|
||||
<Style TargetType="ui:PathIcon" x:Key="PathIconDataMerge">
|
||||
<Setter Property="Data" Value="M8 17L12 13H15.2C15.6 14.2 16.7 15 18 15C19.7 15 21 13.7 21 12S19.7 9 18 9C16.7 9 15.6 9.8 15.2 11H12L8 7V3H3V8H6L10.2 12L6 16H3V21H8V17Z"/>
|
||||
</Style>
|
||||
<Style TargetType="ui:PathIcon" x:Key="PathIconData">
|
||||
<Setter Property="Data" Value=""/>
|
||||
</Style>
|
||||
|
|
Loading…
Reference in New Issue
Block a user