mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 11:42:22 +08:00
Toolbox: Improve danmaku merge
This commit is contained in:
parent
2151393a77
commit
7ec8b1d405
|
@ -27,6 +27,18 @@ namespace BililiveRecorder.ToolBox.Tool.DanmakuMerger
|
||||||
ErrorMessage = "At least 2 input files required"
|
ErrorMessage = "At least 2 input files required"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (request.Offsets is not null)
|
||||||
|
{
|
||||||
|
if (request.Offsets.Length != inputLength)
|
||||||
|
{
|
||||||
|
return new CommandResponse<DanmakuMergerResponse>
|
||||||
|
{
|
||||||
|
Status = ResponseStatus.Error,
|
||||||
|
ErrorMessage = "The number of offsets should match the number of input files."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var files = new FileStream[inputLength];
|
var files = new FileStream[inputLength];
|
||||||
var readers = new XmlReader?[inputLength];
|
var readers = new XmlReader?[inputLength];
|
||||||
|
|
||||||
|
@ -34,14 +46,16 @@ namespace BililiveRecorder.ToolBox.Tool.DanmakuMerger
|
||||||
XmlWriter? writer = null;
|
XmlWriter? writer = null;
|
||||||
XElement recordInfo;
|
XElement recordInfo;
|
||||||
|
|
||||||
DateTimeOffset baseTime;
|
|
||||||
TimeSpan[] timeDiff;
|
TimeSpan[] timeDiff;
|
||||||
|
|
||||||
try // finally
|
try // finally
|
||||||
{
|
|
||||||
|
|
||||||
|
{
|
||||||
|
// 读取文件开头并计算时间差
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
DateTimeOffset baseTime;
|
||||||
|
|
||||||
// 打开输入文件
|
// 打开输入文件
|
||||||
for (var i = 0; i < inputLength; i++)
|
for (var i = 0; i < inputLength; i++)
|
||||||
{
|
{
|
||||||
|
@ -50,7 +64,7 @@ namespace BililiveRecorder.ToolBox.Tool.DanmakuMerger
|
||||||
readers[i] = XmlReader.Create(file, null);
|
readers[i] = XmlReader.Create(file, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算时间差
|
// 读取XML文件开头
|
||||||
var startTimes = new (DateTimeOffset time, XElement element)[inputLength];
|
var startTimes = new (DateTimeOffset time, XElement element)[inputLength];
|
||||||
for (var i = 0; i < inputLength; i++)
|
for (var i = 0; i < inputLength; i++)
|
||||||
{
|
{
|
||||||
|
@ -72,11 +86,23 @@ namespace BililiveRecorder.ToolBox.Tool.DanmakuMerger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.Offsets is not null)
|
||||||
|
{
|
||||||
|
// 使用传递进来的参数作为时间差
|
||||||
|
timeDiff = request.Offsets.Select(x => TimeSpan.FromSeconds(x)).ToArray();
|
||||||
|
var b = startTimes[Array.IndexOf(timeDiff, timeDiff.Min())];
|
||||||
|
recordInfo = b.element;
|
||||||
|
baseTime = b.time;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 使用文件内的开始时间作为时间差
|
||||||
var b = startTimes.OrderBy(x => x.time).First();
|
var b = startTimes.OrderBy(x => x.time).First();
|
||||||
recordInfo = b.element;
|
recordInfo = b.element;
|
||||||
baseTime = b.time;
|
baseTime = b.time;
|
||||||
timeDiff = startTimes.Select(x => x.time - baseTime).ToArray();
|
timeDiff = startTimes.Select(x => x.time - baseTime).ToArray();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return new CommandResponse<DanmakuMergerResponse>
|
return new CommandResponse<DanmakuMergerResponse>
|
||||||
|
|
|
@ -6,6 +6,8 @@ namespace BililiveRecorder.ToolBox.Tool.DanmakuMerger
|
||||||
{
|
{
|
||||||
public string[] Inputs { get; set; } = Array.Empty<string>();
|
public string[] Inputs { get; set; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
public int[]? Offsets { get; set; } = null;
|
||||||
|
|
||||||
public string Output { get; set; } = string.Empty;
|
public string Output { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.ToolBox.Tool.DanmakuStartTime
|
||||||
|
{
|
||||||
|
public class DanmakuStartTimeHandler : ICommandHandler<DanmakuStartTimeRequest, DanmakuStartTimeResponse>
|
||||||
|
{
|
||||||
|
public string Name => "Read Danmaku start_time";
|
||||||
|
|
||||||
|
public async Task<CommandResponse<DanmakuStartTimeResponse>> Handle(DanmakuStartTimeRequest request, CancellationToken cancellationToken, ProgressCallback? progress)
|
||||||
|
{
|
||||||
|
List<DanmakuStartTimeResponse.DanmakuStartTime> result = new();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
progress?.Invoke(0);
|
||||||
|
var finished = 0;
|
||||||
|
double total = request.Inputs.Length;
|
||||||
|
|
||||||
|
Parallel.ForEach(request.Inputs, input =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var file = File.Open(input, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
var r = XmlReader.Create(file, null);
|
||||||
|
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");
|
||||||
|
result.Add(new DanmakuStartTimeResponse.DanmakuStartTime { Path = input, StartTime = time });
|
||||||
|
|
||||||
|
Interlocked.Increment(ref finished);
|
||||||
|
progress?.Invoke(finished / total);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
r.Skip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception) { }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new CommandResponse<DanmakuStartTimeResponse>
|
||||||
|
{
|
||||||
|
Status = ResponseStatus.Error,
|
||||||
|
Exception = ex,
|
||||||
|
ErrorMessage = ex.Message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CommandResponse<DanmakuStartTimeResponse> { Status = ResponseStatus.OK, Data = new DanmakuStartTimeResponse { StartTimes = result.ToArray() } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.ToolBox.Tool.DanmakuStartTime
|
||||||
|
{
|
||||||
|
public class DanmakuStartTimeRequest : ICommandRequest<DanmakuStartTimeResponse>
|
||||||
|
{
|
||||||
|
public string[] Inputs { get; set; } = Array.Empty<string>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
using System;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.ToolBox.Tool.DanmakuStartTime
|
||||||
|
{
|
||||||
|
public class DanmakuStartTimeResponse : IResponseData
|
||||||
|
{
|
||||||
|
public DanmakuStartTime[] StartTimes { get; set; } = Array.Empty<DanmakuStartTime>();
|
||||||
|
|
||||||
|
public void PrintToConsole()
|
||||||
|
{
|
||||||
|
var t = new Table()
|
||||||
|
.AddColumns("Start Time", "File Path")
|
||||||
|
.Border(TableBorder.Rounded);
|
||||||
|
|
||||||
|
foreach (var item in this.StartTimes)
|
||||||
|
{
|
||||||
|
t.AddRow(item.StartTime.ToString().EscapeMarkup(), item.Path.EscapeMarkup());
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.Render(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DanmakuStartTime
|
||||||
|
{
|
||||||
|
public string Path { get; set; } = string.Empty;
|
||||||
|
public DateTimeOffset StartTime { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ using System.CommandLine.Invocation;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BililiveRecorder.ToolBox.Tool.Analyze;
|
using BililiveRecorder.ToolBox.Tool.Analyze;
|
||||||
using BililiveRecorder.ToolBox.Tool.DanmakuMerger;
|
using BililiveRecorder.ToolBox.Tool.DanmakuMerger;
|
||||||
|
using BililiveRecorder.ToolBox.Tool.DanmakuStartTime;
|
||||||
using BililiveRecorder.ToolBox.Tool.Export;
|
using BililiveRecorder.ToolBox.Tool.Export;
|
||||||
using BililiveRecorder.ToolBox.Tool.Fix;
|
using BililiveRecorder.ToolBox.Tool.Fix;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
@ -32,10 +33,16 @@ namespace BililiveRecorder.ToolBox
|
||||||
c.Add(new Argument<string>("output", "example: output.brec.xml.gz"));
|
c.Add(new Argument<string>("output", "example: output.brec.xml.gz"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.RegisterCommand<DanmakuStartTimeHandler, DanmakuStartTimeRequest, DanmakuStartTimeResponse>("danmaku-start-time", null, c =>
|
||||||
|
{
|
||||||
|
c.Add(new Argument<string[]>("inputs", "example: 1.xml 2.xml ..."));
|
||||||
|
});
|
||||||
|
|
||||||
this.RegisterCommand<DanmakuMergerHandler, DanmakuMergerRequest, DanmakuMergerResponse>("danmaku-merge", null, c =>
|
this.RegisterCommand<DanmakuMergerHandler, DanmakuMergerRequest, DanmakuMergerResponse>("danmaku-merge", null, c =>
|
||||||
{
|
{
|
||||||
c.Add(new Argument<string>("output", "example: output.xml"));
|
c.Add(new Argument<string>("output", "example: output.xml"));
|
||||||
c.Add(new Argument<string[]>("inputs", "example: 1.xml 2.xml ..."));
|
c.Add(new Argument<string[]>("inputs", "example: 1.xml 2.xml ..."));
|
||||||
|
c.Add(new Option<int[]?>("--offsets", "Use offsets provided instead of calculating from starttime attribute."));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -123,6 +123,7 @@
|
||||||
<Compile Include="Converters\ValueConverterGroup.cs" />
|
<Compile Include="Converters\ValueConverterGroup.cs" />
|
||||||
<Compile Include="Models\AboutModel.cs" />
|
<Compile Include="Models\AboutModel.cs" />
|
||||||
<Compile Include="Models\Commands.cs" />
|
<Compile Include="Models\Commands.cs" />
|
||||||
|
<Compile Include="Models\DanmakuFileWithOffset.cs" />
|
||||||
<Compile Include="Models\LogModel.cs" />
|
<Compile Include="Models\LogModel.cs" />
|
||||||
<Compile Include="Models\PollyPolicyModel.cs" />
|
<Compile Include="Models\PollyPolicyModel.cs" />
|
||||||
<Compile Include="Models\RootModel.cs" />
|
<Compile Include="Models\RootModel.cs" />
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<ContentControl Content="{Binding}" ContentTemplate="{StaticResource RoomDialogHeader}"/>
|
<ContentControl Content="{Binding}" ContentTemplate="{StaticResource RoomDialogHeader}"/>
|
||||||
|
<TextBlock TextAlignment="Center" Text="Tip: 这里是房间独立设置,是对↙全局设置↙的覆盖"/>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<ScrollViewer Grid.Row="1">
|
<ScrollViewer Grid.Row="1">
|
||||||
|
|
37
BililiveRecorder.WPF/Models/DanmakuFileWithOffset.cs
Normal file
37
BililiveRecorder.WPF/Models/DanmakuFileWithOffset.cs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
namespace BililiveRecorder.WPF.Models
|
||||||
|
{
|
||||||
|
internal class DanmakuFileWithOffset : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private string path;
|
||||||
|
private DateTimeOffset startTime;
|
||||||
|
private int offset;
|
||||||
|
|
||||||
|
public string Path { get => this.path; set => this.SetField(ref this.path, value); }
|
||||||
|
public DateTimeOffset StartTime { get => this.startTime; set => this.SetField(ref this.startTime, value); }
|
||||||
|
public int Offset { get => this.offset; set => this.SetField(ref this.offset, value); }
|
||||||
|
|
||||||
|
public DanmakuFileWithOffset(string path)
|
||||||
|
{
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
protected bool SetField<T>(ref T location, T value, [CallerMemberName] string propertyName = "")
|
||||||
|
{
|
||||||
|
if (EqualityComparer<T>.Default.Equals(location, value))
|
||||||
|
return false;
|
||||||
|
location = value;
|
||||||
|
this.OnPropertyChanged(propertyName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() => HashCode.Combine(this.path, this.startTime, this.offset);
|
||||||
|
}
|
||||||
|
}
|
|
@ -91,6 +91,11 @@
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem Header="{l:Loc RoomListPage_Menu_Links_Sponsor}" Command="{x:Static m:Commands.OpenLink}" CommandParameter="https://rec.danmuji.org/link/sponsor/">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<ui:PathIcon Style="{StaticResource PathIconDataOpenInNew}"/>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
l:ResxLocalizationProvider.DefaultAssembly="BililiveRecorder.WPF"
|
l:ResxLocalizationProvider.DefaultAssembly="BililiveRecorder.WPF"
|
||||||
l:ResxLocalizationProvider.DefaultDictionary="Strings"
|
l:ResxLocalizationProvider.DefaultDictionary="Strings"
|
||||||
xmlns:c="clr-namespace:BililiveRecorder.WPF.Controls"
|
xmlns:c="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||||
|
xmlns:m="clr-namespace:BililiveRecorder.WPF.Models"
|
||||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
|
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||||
xmlns:config="clr-namespace:BililiveRecorder.Core.Config.V2;assembly=BililiveRecorder.Core"
|
xmlns:config="clr-namespace:BililiveRecorder.Core.Config.V2;assembly=BililiveRecorder.Core"
|
||||||
xmlns:confiv2="clr-namespace:BililiveRecorder.Core.Config.V2;assembly=BililiveRecorder.Core"
|
xmlns:confiv2="clr-namespace:BililiveRecorder.Core.Config.V2;assembly=BililiveRecorder.Core"
|
||||||
|
@ -80,14 +81,32 @@
|
||||||
<TextBlock FontSize="13" Text="{l:Loc Settings_FileName_Description_ToolTip}"
|
<TextBlock FontSize="13" Text="{l:Loc Settings_FileName_Description_ToolTip}"
|
||||||
l:ResxLocalizationProvider.DefaultDictionary="Strings"/>
|
l:ResxLocalizationProvider.DefaultDictionary="Strings"/>
|
||||||
</StackPanel.ToolTip>
|
</StackPanel.ToolTip>
|
||||||
<TextBlock Text="{l:Loc Settings_FileName_Description_Text}"/>
|
|
||||||
<ui:PathIcon Margin="2,0" VerticalAlignment="Center" Height="15" Style="{StaticResource PathIconDataInformationOutline}"/>
|
<ui:PathIcon Margin="2,0" VerticalAlignment="Center" Height="15" Style="{StaticResource PathIconDataInformationOutline}"/>
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{l:Loc Settings_FileName_Description_Text}"/>
|
||||||
|
<Button Margin="5,0,0,0" Command="{x:Static m:Commands.OpenLink}" CommandParameter="https://rec.danmuji.org/docs/basic/settings/#%E5%BD%95%E5%88%B6%E6%96%87%E4%BB%B6%E5%90%8D%E6%A0%BC%E5%BC%8F">
|
||||||
|
<ui:PathIcon Margin="2,0" VerticalAlignment="Center" Height="15" Style="{StaticResource PathIconDataOpenInNew}"/>
|
||||||
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordFilenameFormat}" Header="{l:Loc Settings_FileName_Record}">
|
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordFilenameFormat}" Header="{l:Loc Settings_FileName_Record}">
|
||||||
<TextBox Text="{Binding RecordFilenameFormat,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
<TextBox Text="{Binding RecordFilenameFormat,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
||||||
</c:SettingWithDefault>
|
</c:SettingWithDefault>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
<GroupBox Header="录制画质">
|
||||||
|
<StackPanel MaxWidth="400" HorizontalAlignment="Left">
|
||||||
|
<Button Margin="5,0,0,0" Command="{x:Static m:Commands.OpenLink}" CommandParameter="https://rec.danmuji.org/docs/basic/settings/#%E7%9B%B4%E6%92%AD%E7%94%BB%E8%B4%A8">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<ui:PathIcon Width="15" Height="15" Style="{StaticResource PathIconDataInformationOutline}"/>
|
||||||
|
<ui:PathIcon Margin="5,0,0,0" Width="15" Height="15" Style="{StaticResource PathIconDataOpenInNew}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<TextBlock Text="逗号分割的录制画质ID"/>
|
||||||
|
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordingQuality}">
|
||||||
|
<TextBox Text="{Binding RecordingQuality,UpdateSourceTrigger=PropertyChanged,Delay=1000}"
|
||||||
|
ui:TextBoxHelper.IsDeleteButtonVisible="False" Width="220" HorizontalAlignment="Left"/>
|
||||||
|
</c:SettingWithDefault>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
<GroupBox Header="{l:Loc Settings_Webhook_Title}">
|
<GroupBox Header="{l:Loc Settings_Webhook_Title}">
|
||||||
<StackPanel MaxWidth="400" HorizontalAlignment="Left">
|
<StackPanel MaxWidth="400" HorizontalAlignment="Left">
|
||||||
<TextBlock Text="{l:Loc Settings_Webhook_Address}" Margin="0,0,0,10"/>
|
<TextBlock Text="{l:Loc Settings_Webhook_Address}" Margin="0,0,0,10"/>
|
||||||
|
@ -99,15 +118,6 @@
|
||||||
Text="{Binding WebHookUrls,UpdateSourceTrigger=PropertyChanged,Delay=1000}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
Text="{Binding WebHookUrls,UpdateSourceTrigger=PropertyChanged,Delay=1000}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
<GroupBox Header="录制画质">
|
|
||||||
<StackPanel MaxWidth="400" HorizontalAlignment="Left">
|
|
||||||
<TextBlock Text="逗号分割的录制画质ID"/>
|
|
||||||
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordingQuality}">
|
|
||||||
<TextBox Text="{Binding RecordingQuality,UpdateSourceTrigger=PropertyChanged,Delay=1000}"
|
|
||||||
ui:TextBoxHelper.IsDeleteButtonVisible="False" Width="220" HorizontalAlignment="Left"/>
|
|
||||||
</c:SettingWithDefault>
|
|
||||||
</StackPanel>
|
|
||||||
</GroupBox>
|
|
||||||
</ui:SimpleStackPanel>
|
</ui:SimpleStackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ui:Page>
|
</ui:Page>
|
||||||
|
|
|
@ -28,19 +28,31 @@
|
||||||
<Button Margin="0,0,5,0" Content="{l:Loc Toolbox_Merge_Button_AddFile}" Click="AddFile_Click"/>
|
<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"/>
|
<Button Content="{l:Loc Toolbox_Merge_Button_Merge}" Click="Merge_Click"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<ListBox Grid.Row="3" Margin="5" x:Name="listBox" AllowDrop="True" Drop="listBox_Drop">
|
<ListView Grid.Row="3" Margin="5" x:Name="listView" AllowDrop="True" Drop="DragDrop">
|
||||||
<ListBox.ItemTemplate>
|
<ListView.View>
|
||||||
|
<GridView AllowsColumnReorder="False">
|
||||||
|
<GridViewColumn>
|
||||||
|
<GridViewColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Grid Margin="0,0">
|
<Button Padding="2" VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||||
<Grid.ColumnDefinitions>
|
Content="{l:Loc Toolbox_Merge_Button_Remove}" Click="RemoveFile_Click"/>
|
||||||
<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>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</GridViewColumn.CellTemplate>
|
||||||
</ListBox>
|
</GridViewColumn>
|
||||||
|
<GridViewColumn Header="偏移量(秒)" Width="120">
|
||||||
|
<GridViewColumn.CellTemplate>
|
||||||
|
<DataTemplate><!--
|
||||||
|
<ui:NumberBox Minimum="0" SmallChange="1" LargeChange="10" Text="{Binding Offset,UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
SpinButtonPlacementMode="Inline" ValidationMode="InvalidInputOverwritten"/>
|
||||||
|
-->
|
||||||
|
<TextBox Width="100" Text="{Binding Offset,UpdateSourceTrigger=PropertyChanged,Delay=300}"
|
||||||
|
ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</GridViewColumn.CellTemplate>
|
||||||
|
</GridViewColumn>
|
||||||
|
<GridViewColumn Header="文件路径" DisplayMemberBinding="{Binding Path}"/>
|
||||||
|
</GridView>
|
||||||
|
</ListView.View>
|
||||||
|
</ListView>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ui:Page>
|
</ui:Page>
|
||||||
|
|
|
@ -6,9 +6,12 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Threading;
|
||||||
using BililiveRecorder.ToolBox;
|
using BililiveRecorder.ToolBox;
|
||||||
using BililiveRecorder.ToolBox.Tool.DanmakuMerger;
|
using BililiveRecorder.ToolBox.Tool.DanmakuMerger;
|
||||||
|
using BililiveRecorder.ToolBox.Tool.DanmakuStartTime;
|
||||||
using BililiveRecorder.WPF.Controls;
|
using BililiveRecorder.WPF.Controls;
|
||||||
|
using BililiveRecorder.WPF.Models;
|
||||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using WPFLocalizeExtension.Extensions;
|
using WPFLocalizeExtension.Extensions;
|
||||||
|
@ -24,43 +27,38 @@ namespace BililiveRecorder.WPF.Pages
|
||||||
private static readonly ILogger logger = Log.ForContext<ToolboxDanmakuMergerPage>();
|
private static readonly ILogger logger = Log.ForContext<ToolboxDanmakuMergerPage>();
|
||||||
private static readonly string DesktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
private static readonly string DesktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||||
|
|
||||||
private readonly ObservableCollection<string> Files = new ObservableCollection<string>();
|
private readonly ObservableCollection<DanmakuFileWithOffset> Files = new();
|
||||||
|
|
||||||
public ToolboxDanmakuMergerPage()
|
public ToolboxDanmakuMergerPage()
|
||||||
{
|
{
|
||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
this.listBox.ItemsSource = this.Files;
|
this.listView.ItemsSource = this.Files;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveFile_Click(object sender, RoutedEventArgs e)
|
private void RemoveFile_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var b = (Button)sender;
|
var b = (Button)sender;
|
||||||
var f = (string)b.DataContext;
|
var f = (DanmakuFileWithOffset)b.DataContext;
|
||||||
this.Files.Remove(f);
|
this.Files.Remove(f);
|
||||||
|
|
||||||
|
this.CalculateOffsets();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void listBox_Drop(object sender, DragEventArgs e)
|
private async void DragDrop(object sender, DragEventArgs e)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||||
{
|
{
|
||||||
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
|
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
|
||||||
for (var i = 0; i < files.Length; i++)
|
await this.AddFilesAsync(files.Where(x => x.EndsWith(".xml", StringComparison.InvariantCultureIgnoreCase)).ToArray());
|
||||||
{
|
|
||||||
var file = files[i];
|
|
||||||
if (file.EndsWith(".xml", StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
{
|
|
||||||
this.Files.Add(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{ }
|
{ }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddFile_Click(object sender, RoutedEventArgs e)
|
private async void AddFile_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var d = new CommonOpenFileDialog
|
var d = new CommonOpenFileDialog
|
||||||
{
|
{
|
||||||
|
@ -79,10 +77,39 @@ namespace BililiveRecorder.WPF.Pages
|
||||||
if (d.ShowDialog() != CommonFileDialogResult.Ok)
|
if (d.ShowDialog() != CommonFileDialogResult.Ok)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var file in d.FileNames)
|
await this.AddFilesAsync(d.FileNames.ToArray());
|
||||||
{
|
|
||||||
this.Files.Add(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task AddFilesAsync(string[] paths)
|
||||||
|
{
|
||||||
|
var req = new DanmakuStartTimeRequest { Inputs = paths };
|
||||||
|
var handler = new DanmakuStartTimeHandler();
|
||||||
|
var resp = await handler.Handle(req, default, default).ConfigureAwait(true);
|
||||||
|
|
||||||
|
if (resp.Status != ResponseStatus.OK || resp.Data is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var toBeAdded = resp.Data.StartTimes.Select(x => new DanmakuFileWithOffset(x.Path) { StartTime = x.StartTime });
|
||||||
|
foreach (var file in toBeAdded)
|
||||||
|
this.Files.Add(file);
|
||||||
|
|
||||||
|
_ = this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)this.CalculateOffsets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CalculateOffsets()
|
||||||
|
{
|
||||||
|
if (this.Files.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var minTime = this.Files.Min(x => x.StartTime);
|
||||||
|
|
||||||
|
foreach (var item in this.Files)
|
||||||
|
{
|
||||||
|
item.Offset = (int)(item.StartTime - minTime).TotalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listView.DataContext = null;
|
||||||
|
this.listView.DataContext = this.Files;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Merge_Click(object sender, RoutedEventArgs e)
|
private async void Merge_Click(object sender, RoutedEventArgs e)
|
||||||
|
@ -124,7 +151,7 @@ namespace BililiveRecorder.WPF.Pages
|
||||||
EnsureValidNames = true,
|
EnsureValidNames = true,
|
||||||
NavigateToShortcut = true,
|
NavigateToShortcut = true,
|
||||||
OverwritePrompt = false,
|
OverwritePrompt = false,
|
||||||
InitialDirectory = Path.GetDirectoryName(inputPaths[0]),
|
InitialDirectory = Path.GetDirectoryName(inputPaths[0].Path),
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDialog.Filters.Add(new CommonFileDialogFilter(LocExtension.GetLocalizedValue<string>("BililiveRecorder.WPF:Strings:Toolbox_Merge_XmlDanmakuFiles"), "*.xml"));
|
fileDialog.Filters.Add(new CommonFileDialogFilter(LocExtension.GetLocalizedValue<string>("BililiveRecorder.WPF:Strings:Toolbox_Merge_XmlDanmakuFiles"), "*.xml"));
|
||||||
|
@ -137,7 +164,8 @@ namespace BililiveRecorder.WPF.Pages
|
||||||
|
|
||||||
var req = new DanmakuMergerRequest
|
var req = new DanmakuMergerRequest
|
||||||
{
|
{
|
||||||
Inputs = inputPaths,
|
Inputs = inputPaths.Select(x => x.Path).ToArray(),
|
||||||
|
Offsets = inputPaths.Select(x => x.Offset).ToArray(),
|
||||||
Output = outputPath,
|
Output = outputPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user