Toolbox: Add danmaku merger

This commit is contained in:
Genteure 2021-08-10 18:53:04 +08:00
parent df8559424c
commit 5fb7d074d6
13 changed files with 678 additions and 2 deletions

View File

@ -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");

View File

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

View File

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

View File

@ -0,0 +1,7 @@
namespace BililiveRecorder.ToolBox.Tool.DanmakuMerger
{
public class DanmakuMergerResponse : IResponseData
{
public void PrintToConsole() { }
}
}

View File

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

View File

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

View File

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

View File

@ -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;

View 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>

View 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);
}
}
}

View File

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

View File

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

View File

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