mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 11:42:22 +08:00
Add ToolBox
This commit is contained in:
parent
14ffd7b700
commit
c6eae11f95
|
@ -33,6 +33,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BililiveRecorder.Core\BililiveRecorder.Core.csproj" />
|
||||
<ProjectReference Include="..\BililiveRecorder.ToolBox\BililiveRecorder.ToolBox.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -9,6 +9,7 @@ using BililiveRecorder.Core;
|
|||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Config.V2;
|
||||
using BililiveRecorder.DependencyInjection;
|
||||
using BililiveRecorder.ToolBox;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
@ -39,7 +40,8 @@ namespace BililiveRecorder.Cli
|
|||
var root = new RootCommand("A Stream Recorder For Bilibili Live")
|
||||
{
|
||||
cmd_run,
|
||||
cmd_portable
|
||||
cmd_portable,
|
||||
new ToolCommand()
|
||||
};
|
||||
|
||||
return root.Invoke(args);
|
||||
|
@ -85,7 +87,8 @@ namespace BililiveRecorder.Cli
|
|||
var logger = BuildLogger();
|
||||
Log.Logger = logger;
|
||||
|
||||
var config = new ConfigV2(){
|
||||
var config = new ConfigV2()
|
||||
{
|
||||
DisableConfigSave = true,
|
||||
};
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ namespace BililiveRecorder.Flv.Writer
|
|||
if (this.disposedValue)
|
||||
throw new ObjectDisposedException(nameof(FlvTagFileWriter));
|
||||
|
||||
System.Diagnostics.Debug.Assert(this.stream is null, "stream is not null");
|
||||
this.stream?.Dispose();
|
||||
|
||||
(this.stream, this.State) = this.targetProvider.CreateOutputStream();
|
||||
|
|
16
BililiveRecorder.ToolBox/BililiveRecorder.ToolBox.csproj
Normal file
16
BililiveRecorder.ToolBox/BililiveRecorder.ToolBox.csproj
Normal file
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20574.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BililiveRecorder.Flv\BililiveRecorder.Flv.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
158
BililiveRecorder.ToolBox/Commands/Analyze.cs
Normal file
158
BililiveRecorder.ToolBox/Commands/Analyze.cs
Normal file
|
@ -0,0 +1,158 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Flv;
|
||||
using BililiveRecorder.Flv.Amf;
|
||||
using BililiveRecorder.Flv.Grouping;
|
||||
using BililiveRecorder.Flv.Parser;
|
||||
using BililiveRecorder.Flv.Pipeline;
|
||||
using BililiveRecorder.Flv.Writer;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
|
||||
namespace BililiveRecorder.ToolBox.Commands
|
||||
{
|
||||
public class AnalyzeRequest : ICommandRequest<AnalyzeResponse>
|
||||
{
|
||||
public string Input { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AnalyzeResponse
|
||||
{
|
||||
public string InputPath { get; set; } = string.Empty;
|
||||
|
||||
public bool NeedFix { get; set; }
|
||||
public bool Unrepairable { get; set; }
|
||||
|
||||
public int OutputFileCount { get; set; }
|
||||
|
||||
public int IssueTypeOther { get; set; }
|
||||
public int IssueTypeUnrepairable { get; set; }
|
||||
public int IssueTypeTimestampJump { get; set; }
|
||||
public int IssueTypeDecodingHeader { get; set; }
|
||||
public int IssueTypeRepeatingData { get; set; }
|
||||
}
|
||||
|
||||
public class AnalyzeHandler : ICommandHandler<AnalyzeRequest, AnalyzeResponse>
|
||||
{
|
||||
private static readonly ILogger logger = Log.ForContext<AnalyzeHandler>();
|
||||
|
||||
public Task<AnalyzeResponse> Handle(AnalyzeRequest request) => this.Handle(request, null);
|
||||
|
||||
public async Task<AnalyzeResponse> Handle(AnalyzeRequest request, Func<double, Task>? progress)
|
||||
{
|
||||
var inputPath = Path.GetFullPath(request.Input);
|
||||
|
||||
var memoryStreamProvider = new DefaultMemoryStreamProvider();
|
||||
var tagWriter = new AnalyzeMockFlvTagWriter();
|
||||
var comments = new List<ProcessingComment>();
|
||||
var context = new FlvProcessingContext();
|
||||
var session = new Dictionary<object, object?>();
|
||||
|
||||
{
|
||||
using var inputStream = File.OpenRead(inputPath);
|
||||
|
||||
using var grouping = new TagGroupReader(new FlvTagPipeReader(PipeReader.Create(inputStream), memoryStreamProvider, skipData: false, logger: logger));
|
||||
using var writer = new FlvProcessingContextWriter(tagWriter);
|
||||
var pipeline = new ProcessingPipelineBuilder(new ServiceCollection().BuildServiceProvider()).AddDefault().AddRemoveFillerData().Build();
|
||||
|
||||
var count = 0;
|
||||
while (true)
|
||||
{
|
||||
var group = await grouping.ReadGroupAsync(default).ConfigureAwait(false);
|
||||
if (group is null)
|
||||
break;
|
||||
|
||||
context.Reset(group, session);
|
||||
pipeline(context);
|
||||
|
||||
if (context.Comments.Count > 0)
|
||||
{
|
||||
comments.AddRange(context.Comments);
|
||||
logger.Debug("分析逻辑输出 {@Comments}", context.Comments);
|
||||
}
|
||||
|
||||
await writer.WriteAsync(context).ConfigureAwait(false);
|
||||
|
||||
foreach (var action in context.Actions)
|
||||
if (action is PipelineDataAction dataAction)
|
||||
foreach (var tag in dataAction.Tags)
|
||||
tag.BinaryData?.Dispose();
|
||||
|
||||
if (count++ % 10 == 0)
|
||||
{
|
||||
progress?.Invoke((double)inputStream.Position / inputStream.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var countableComments = comments.Where(x => x.T != CommentType.Logging);
|
||||
|
||||
var response = new AnalyzeResponse
|
||||
{
|
||||
InputPath = inputPath,
|
||||
|
||||
NeedFix = tagWriter.OutputFileCount != 1 || countableComments.Any(),
|
||||
Unrepairable = countableComments.Any(x => x.T == CommentType.Unrepairable),
|
||||
|
||||
OutputFileCount = tagWriter.OutputFileCount,
|
||||
|
||||
IssueTypeOther = countableComments.Count(x => x.T == CommentType.Other),
|
||||
IssueTypeUnrepairable = countableComments.Count(x => x.T == CommentType.Unrepairable),
|
||||
IssueTypeTimestampJump = countableComments.Count(x => x.T == CommentType.TimestampJump),
|
||||
IssueTypeDecodingHeader = countableComments.Count(x => x.T == CommentType.DecodingHeader),
|
||||
IssueTypeRepeatingData = countableComments.Count(x => x.T == CommentType.RepeatingData)
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public void PrintResponse(AnalyzeResponse response)
|
||||
{
|
||||
Console.Write("Input: ");
|
||||
Console.WriteLine(response.InputPath);
|
||||
|
||||
Console.WriteLine(response.NeedFix ? "File needs repair" : "File doesn't need repair");
|
||||
|
||||
if (response.Unrepairable)
|
||||
Console.WriteLine("File contains error(s) that are unrepairable (yet), please send sample to the author of this program.");
|
||||
|
||||
Console.WriteLine("Will output {0} file(s) if repaired", response.OutputFileCount);
|
||||
|
||||
Console.WriteLine("Types of error:");
|
||||
Console.Write("Other: ");
|
||||
Console.WriteLine(response.IssueTypeOther);
|
||||
Console.Write("Unrepairable: ");
|
||||
Console.WriteLine(response.IssueTypeUnrepairable);
|
||||
Console.Write("TimestampJump: ");
|
||||
Console.WriteLine(response.IssueTypeTimestampJump);
|
||||
Console.Write("DecodingHeader: ");
|
||||
Console.WriteLine(response.IssueTypeDecodingHeader);
|
||||
Console.Write("RepeatingData: ");
|
||||
Console.WriteLine(response.IssueTypeRepeatingData);
|
||||
}
|
||||
|
||||
private class AnalyzeMockFlvTagWriter : IFlvTagWriter
|
||||
{
|
||||
public long FileSize => 0;
|
||||
public object? State => null;
|
||||
|
||||
public int OutputFileCount { get; private set; }
|
||||
|
||||
public bool CloseCurrentFile() => true;
|
||||
public Task CreateNewFile()
|
||||
{
|
||||
this.OutputFileCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
public Task OverwriteMetadata(ScriptTagBody metadata) => Task.CompletedTask;
|
||||
public Task WriteAlternativeHeaders(IEnumerable<Tag> tags) => Task.CompletedTask;
|
||||
public Task WriteTag(Tag tag) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
66
BililiveRecorder.ToolBox/Commands/Export.cs
Normal file
66
BililiveRecorder.ToolBox/Commands/Export.cs
Normal file
|
@ -0,0 +1,66 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Flv;
|
||||
using BililiveRecorder.Flv.Parser;
|
||||
using BililiveRecorder.Flv.Xml;
|
||||
using Serilog;
|
||||
|
||||
namespace BililiveRecorder.ToolBox.Commands
|
||||
{
|
||||
public class ExportRequest : ICommandRequest<ExportResponse>
|
||||
{
|
||||
public string Input { get; set; } = string.Empty;
|
||||
|
||||
public string Output { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ExportResponse
|
||||
{
|
||||
}
|
||||
|
||||
public class ExportHandler : ICommandHandler<ExportRequest, ExportResponse>
|
||||
{
|
||||
private static readonly ILogger logger = Log.ForContext<ExportHandler>();
|
||||
|
||||
public Task<ExportResponse> Handle(ExportRequest request) => this.Handle(request, null);
|
||||
|
||||
public async Task<ExportResponse> Handle(ExportRequest request, Func<double, Task>? progress)
|
||||
{
|
||||
using var inputStream = File.OpenRead(request.Input);
|
||||
using var outputStream = File.OpenWrite(request.Output);
|
||||
|
||||
var tags = new List<Tag>();
|
||||
|
||||
{
|
||||
using var reader = new FlvTagPipeReader(PipeReader.Create(inputStream), new DefaultMemoryStreamProvider(), skipData: true, logger: logger);
|
||||
var count = 0;
|
||||
while (true)
|
||||
{
|
||||
var tag = await reader.ReadTagAsync(default).ConfigureAwait(false);
|
||||
if (tag is null) break;
|
||||
tags.Add(tag);
|
||||
|
||||
if (count++ % 300 == 0)
|
||||
progress?.Invoke((double)inputStream.Position / inputStream.Length);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
using var writer = new StreamWriter(new GZipStream(outputStream, CompressionLevel.Optimal));
|
||||
XmlFlvFile.Serializer.Serialize(writer, new XmlFlvFile
|
||||
{
|
||||
Tags = tags
|
||||
});
|
||||
}
|
||||
|
||||
return new ExportResponse();
|
||||
}
|
||||
|
||||
public void PrintResponse(ExportResponse response)
|
||||
{ }
|
||||
}
|
||||
}
|
180
BililiveRecorder.ToolBox/Commands/Fix.cs
Normal file
180
BililiveRecorder.ToolBox/Commands/Fix.cs
Normal file
|
@ -0,0 +1,180 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Flv;
|
||||
using BililiveRecorder.Flv.Grouping;
|
||||
using BililiveRecorder.Flv.Parser;
|
||||
using BililiveRecorder.Flv.Pipeline;
|
||||
using BililiveRecorder.Flv.Writer;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
|
||||
namespace BililiveRecorder.ToolBox.Commands
|
||||
{
|
||||
public class FixRequest : ICommandRequest<FixResponse>
|
||||
{
|
||||
public string Input { get; set; } = string.Empty;
|
||||
|
||||
public string OutputBase { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class FixResponse
|
||||
{
|
||||
public string InputPath { get; set; } = string.Empty;
|
||||
|
||||
public string[] OutputPaths { get; set; } = Array.Empty<string>();
|
||||
|
||||
public bool NeedFix { get; set; }
|
||||
public bool Unrepairable { get; set; }
|
||||
|
||||
public int OutputFileCount { get; set; }
|
||||
|
||||
public int IssueTypeOther { get; set; }
|
||||
public int IssueTypeUnrepairable { get; set; }
|
||||
public int IssueTypeTimestampJump { get; set; }
|
||||
public int IssueTypeDecodingHeader { get; set; }
|
||||
public int IssueTypeRepeatingData { get; set; }
|
||||
}
|
||||
|
||||
public class FixHandler : ICommandHandler<FixRequest, FixResponse>
|
||||
{
|
||||
private static readonly ILogger logger = Log.ForContext<FixHandler>();
|
||||
|
||||
public Task<FixResponse> Handle(FixRequest request) => this.Handle(request, null);
|
||||
|
||||
public async Task<FixResponse> Handle(FixRequest request, Func<double, Task>? progress)
|
||||
{
|
||||
var inputPath = Path.GetFullPath(request.Input);
|
||||
|
||||
var outputPaths = new List<string>();
|
||||
var targetProvider = new AutoFixFlvWriterTargetProvider(request.OutputBase);
|
||||
targetProvider.BeforeFileOpen += (sender, path) => outputPaths.Add(path);
|
||||
|
||||
var memoryStreamProvider = new DefaultMemoryStreamProvider();
|
||||
var tagWriter = new FlvTagFileWriter(targetProvider, memoryStreamProvider, logger);
|
||||
|
||||
var comments = new List<ProcessingComment>();
|
||||
var context = new FlvProcessingContext();
|
||||
var session = new Dictionary<object, object?>();
|
||||
|
||||
{
|
||||
using var inputStream = File.OpenRead(inputPath);
|
||||
|
||||
using var grouping = new TagGroupReader(new FlvTagPipeReader(PipeReader.Create(inputStream), memoryStreamProvider, skipData: false, logger: logger));
|
||||
using var writer = new FlvProcessingContextWriter(tagWriter);
|
||||
var pipeline = new ProcessingPipelineBuilder(new ServiceCollection().BuildServiceProvider()).AddDefault().AddRemoveFillerData().Build();
|
||||
|
||||
var count = 0;
|
||||
while (true)
|
||||
{
|
||||
var group = await grouping.ReadGroupAsync(default).ConfigureAwait(false);
|
||||
if (group is null)
|
||||
break;
|
||||
|
||||
context.Reset(group, session);
|
||||
pipeline(context);
|
||||
|
||||
if (context.Comments.Count > 0)
|
||||
{
|
||||
comments.AddRange(context.Comments);
|
||||
logger.Debug("修复逻辑输出 {@Comments}", context.Comments);
|
||||
}
|
||||
|
||||
await writer.WriteAsync(context).ConfigureAwait(false);
|
||||
|
||||
foreach (var action in context.Actions)
|
||||
if (action is PipelineDataAction dataAction)
|
||||
foreach (var tag in dataAction.Tags)
|
||||
tag.BinaryData?.Dispose();
|
||||
|
||||
if (count++ % 10 == 0)
|
||||
{
|
||||
progress?.Invoke((double)inputStream.Position / inputStream.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var countableComments = comments.Where(x => x.T != CommentType.Logging);
|
||||
|
||||
var response = new FixResponse
|
||||
{
|
||||
InputPath = inputPath,
|
||||
OutputPaths = outputPaths.ToArray(),
|
||||
OutputFileCount = outputPaths.Count,
|
||||
|
||||
NeedFix = outputPaths.Count != 1 || countableComments.Any(),
|
||||
Unrepairable = countableComments.Any(x => x.T == CommentType.Unrepairable),
|
||||
|
||||
IssueTypeOther = countableComments.Count(x => x.T == CommentType.Other),
|
||||
IssueTypeUnrepairable = countableComments.Count(x => x.T == CommentType.Unrepairable),
|
||||
IssueTypeTimestampJump = countableComments.Count(x => x.T == CommentType.TimestampJump),
|
||||
IssueTypeDecodingHeader = countableComments.Count(x => x.T == CommentType.DecodingHeader),
|
||||
IssueTypeRepeatingData = countableComments.Count(x => x.T == CommentType.RepeatingData)
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public void PrintResponse(FixResponse response)
|
||||
{
|
||||
Console.Write("Input: ");
|
||||
Console.WriteLine(response.InputPath);
|
||||
|
||||
Console.WriteLine(response.NeedFix ? "File needs repair" : "File doesn't need repair");
|
||||
|
||||
if (response.Unrepairable)
|
||||
Console.WriteLine("File contains error(s) that are unrepairable (yet), please send sample to the author of this program.");
|
||||
|
||||
Console.WriteLine("{0} file(s) written", response.OutputFileCount);
|
||||
|
||||
foreach (var path in response.OutputPaths)
|
||||
{
|
||||
Console.Write(" ");
|
||||
Console.WriteLine(path);
|
||||
}
|
||||
|
||||
Console.WriteLine("Types of error:");
|
||||
Console.Write("Other: ");
|
||||
Console.WriteLine(response.IssueTypeOther);
|
||||
Console.Write("Unrepairable: ");
|
||||
Console.WriteLine(response.IssueTypeUnrepairable);
|
||||
Console.Write("TimestampJump: ");
|
||||
Console.WriteLine(response.IssueTypeTimestampJump);
|
||||
Console.Write("DecodingHeader: ");
|
||||
Console.WriteLine(response.IssueTypeDecodingHeader);
|
||||
Console.Write("RepeatingData: ");
|
||||
Console.WriteLine(response.IssueTypeRepeatingData);
|
||||
}
|
||||
|
||||
private class AutoFixFlvWriterTargetProvider : IFlvWriterTargetProvider
|
||||
{
|
||||
private readonly string pathTemplate;
|
||||
private int fileIndex = 1;
|
||||
|
||||
public event EventHandler<string>? BeforeFileOpen;
|
||||
|
||||
public AutoFixFlvWriterTargetProvider(string pathTemplate)
|
||||
{
|
||||
this.pathTemplate = pathTemplate;
|
||||
}
|
||||
|
||||
public Stream CreateAlternativeHeaderStream()
|
||||
{
|
||||
var path = Path.ChangeExtension(this.pathTemplate, "header.txt");
|
||||
return File.Open(path, FileMode.Append, FileAccess.Write, FileShare.None);
|
||||
}
|
||||
|
||||
public (Stream stream, object state) CreateOutputStream()
|
||||
{
|
||||
var i = this.fileIndex++;
|
||||
var path = Path.ChangeExtension(this.pathTemplate, $"fix_p{i}.flv");
|
||||
var fileStream = File.Create(path);
|
||||
BeforeFileOpen?.Invoke(this, path);
|
||||
return (fileStream, null!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
BililiveRecorder.ToolBox/ICommandHandler.cs
Normal file
10
BililiveRecorder.ToolBox/ICommandHandler.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace BililiveRecorder.ToolBox
|
||||
{
|
||||
public interface ICommandHandler<TRequest, TResponse> where TRequest : ICommandRequest<TResponse>
|
||||
{
|
||||
Task<TResponse> Handle(TRequest request);
|
||||
void PrintResponse(TResponse response);
|
||||
}
|
||||
}
|
4
BililiveRecorder.ToolBox/ICommandRequest.cs
Normal file
4
BililiveRecorder.ToolBox/ICommandRequest.cs
Normal file
|
@ -0,0 +1,4 @@
|
|||
namespace BililiveRecorder.ToolBox
|
||||
{
|
||||
public interface ICommandRequest<TResponse> { }
|
||||
}
|
67
BililiveRecorder.ToolBox/ToolCommand.cs
Normal file
67
BililiveRecorder.ToolBox/ToolCommand.cs
Normal file
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.ToolBox.Commands;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BililiveRecorder.ToolBox
|
||||
{
|
||||
public class ToolCommand : Command
|
||||
{
|
||||
public ToolCommand() : base("tool", "Run Tools")
|
||||
{
|
||||
this.RegisterCommand<AnalyzeHandler, AnalyzeRequest, AnalyzeResponse>("analyze", null, c =>
|
||||
{
|
||||
c.Add(new Argument<string>("input", "example: input.flv"));
|
||||
});
|
||||
|
||||
this.RegisterCommand<FixHandler, FixRequest, FixResponse>("fix", null, c =>
|
||||
{
|
||||
c.Add(new Argument<string>("input", "example: input.flv"));
|
||||
c.Add(new Argument<string>("output-base", "example: output.flv"));
|
||||
});
|
||||
|
||||
this.RegisterCommand<ExportHandler, ExportRequest, ExportResponse>("export", null, c =>
|
||||
{
|
||||
c.Add(new Argument<string>("input", "example: input.flv"));
|
||||
c.Add(new Argument<string>("output", "example: output.brec.xml.gz"));
|
||||
});
|
||||
}
|
||||
|
||||
private void RegisterCommand<IHandler, IRequest, IResponse>(string name, string? description, Action<Command> configure)
|
||||
where IHandler : ICommandHandler<IRequest, IResponse>
|
||||
where IRequest : ICommandRequest<IResponse>
|
||||
{
|
||||
var cmd = new Command(name, description)
|
||||
{
|
||||
new Option<bool>("--json", "print result as json string"),
|
||||
new Option<bool>("--json-indented", "print result as indented json string")
|
||||
};
|
||||
cmd.Handler = CommandHandler.Create((IRequest r, bool json, bool jsonIndented) => RunSubCommand<IHandler, IRequest, IResponse>(r, json, jsonIndented));
|
||||
configure(cmd);
|
||||
this.Add(cmd);
|
||||
}
|
||||
|
||||
private static async Task<int> RunSubCommand<IHandler, IRequest, IResponse>(IRequest request, bool json, bool jsonIndented)
|
||||
where IHandler : ICommandHandler<IRequest, IResponse>
|
||||
where IRequest : ICommandRequest<IResponse>
|
||||
{
|
||||
var handler = Activator.CreateInstance<IHandler>();
|
||||
|
||||
var response = await handler.Handle(request).ConfigureAwait(false);
|
||||
|
||||
if (json || jsonIndented)
|
||||
{
|
||||
var json_str = JsonConvert.SerializeObject(response, jsonIndented ? Formatting.Indented : Formatting.None);
|
||||
Console.WriteLine(json_str);
|
||||
}
|
||||
else
|
||||
{
|
||||
handler.PrintResponse(response);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -120,7 +120,6 @@
|
|||
<Compile Include="Converters\RatioToArrowIconConverter.cs" />
|
||||
<Compile Include="Converters\ShortRoomIdToVisibilityConverter.cs" />
|
||||
<Compile Include="Converters\ValueConverterGroup.cs" />
|
||||
<Compile Include="Models\AnalyzeResultModel.cs" />
|
||||
<Compile Include="Models\Commands.cs" />
|
||||
<Compile Include="Models\LogModel.cs" />
|
||||
<Compile Include="Models\PollyPolicyModel.cs" />
|
||||
|
@ -293,6 +292,10 @@
|
|||
<Project>{7610e19c-d3ab-4cbc-983e-6fda36f4d4b3}</Project>
|
||||
<Name>BililiveRecorder.Flv</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\BililiveRecorder.ToolBox\BililiveRecorder.ToolBox.csproj">
|
||||
<Project>{4faae8e7-ac4e-4e99-a7d1-53d20ad8a200}</Project>
|
||||
<Name>BililiveRecorder.ToolBox</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GitVersion.MsBuild" Version="5.6.6">
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.WPF.Models
|
||||
{
|
||||
internal class AnalyzeResultModel : INotifyPropertyChanged
|
||||
{
|
||||
private string file = string.Empty;
|
||||
private bool needFix;
|
||||
private bool unrepairable;
|
||||
private int issueTypeOther;
|
||||
private int issueTypeUnrepairable;
|
||||
private int issueTypeTimestampJump;
|
||||
private int issueTypeDecodingHeader;
|
||||
private int issueTypeRepeatingData;
|
||||
|
||||
public string File { get => this.file; set => this.SetField(ref this.file, value); }
|
||||
public bool NeedFix { get => this.needFix; set => this.SetField(ref this.needFix, value); }
|
||||
public bool Unrepairable { get => this.unrepairable; set => this.SetField(ref this.unrepairable, value); }
|
||||
|
||||
public int IssueTypeOther { get => this.issueTypeOther; set => this.SetField(ref this.issueTypeOther, value); }
|
||||
public int IssueTypeUnrepairable { get => this.issueTypeUnrepairable; set => this.SetField(ref this.issueTypeUnrepairable, value); }
|
||||
public int IssueTypeTimestampJump { get => this.issueTypeTimestampJump; set => this.SetField(ref this.issueTypeTimestampJump, value); }
|
||||
public int IssueTypeDecodingHeader { get => this.issueTypeDecodingHeader; set => this.SetField(ref this.issueTypeDecodingHeader, value); }
|
||||
public int IssueTypeRepeatingData { get => this.issueTypeRepeatingData; set => this.SetField(ref this.issueTypeRepeatingData, value); }
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) { return false; }
|
||||
field = value; this.OnPropertyChanged(propertyName); return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@
|
|||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||
xmlns:model="clr-namespace:BililiveRecorder.WPF.Models"
|
||||
xmlns:c="clr-namespace:BililiveRecorder.WPF.Converters"
|
||||
xmlns:tool="clr-namespace:BililiveRecorder.ToolBox.Commands;assembly=BililiveRecorder.ToolBox"
|
||||
mc:Ignorable="d" DataContext="{x:Null}"
|
||||
d:DesignHeight="600" d:DesignWidth="900"
|
||||
Title="ToolboxAutoFixPage">
|
||||
|
@ -67,7 +68,7 @@
|
|||
<TextBlock Text="注:不分析也可以进行修复操作" VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="NormalAnalyzeResult" DataType="{x:Type model:AnalyzeResultModel}">
|
||||
<DataTemplate x:Key="NormalAnalyzeResult" DataType="{x:Type tool:AnalyzeResponse}">
|
||||
<Grid Margin="5">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
|
@ -75,7 +76,7 @@
|
|||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBox Grid.Row="0" IsReadOnly="True" Text="{Binding File}" ui:ControlHelper.Header="文件:"/>
|
||||
<TextBox Grid.Row="0" IsReadOnly="True" Text="{Binding InputPath}" ui:ControlHelper.Header="文件:"/>
|
||||
<TextBlock Grid.Row="1" HorizontalAlignment="Center" FontSize="24" Text="无需修复" Foreground="Green"
|
||||
Visibility="{Binding NeedFix,Converter={StaticResource InvertBooleanToVisibilityCollapsedConverter}}"/>
|
||||
<TextBlock Grid.Row="1" HorizontalAlignment="Center" FontSize="24" Text="需要修复" Foreground="Red"
|
||||
|
@ -86,6 +87,7 @@
|
|||
<TextBlock HorizontalAlignment="Center" Text="请点击“修复失败?”按钮并反馈本问题"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="3" HorizontalAlignment="Center" Margin="10">
|
||||
<TextBlock Text="{Binding OutputFileCount,StringFormat=修复将会输出 {0} 个文件}" Margin="0,0,0,5"/>
|
||||
<TextBlock Text="{Binding IssueTypeTimestampJump,StringFormat=时间戳问题 {0} 处}"/>
|
||||
<TextBlock Text="{Binding IssueTypeDecodingHeader,StringFormat=分辨率、解码问题 {0} 处}"/>
|
||||
<TextBlock Text="{Binding IssueTypeRepeatingData,StringFormat=重复片段 {0} 处}"/>
|
||||
|
|
|
@ -12,6 +12,7 @@ using BililiveRecorder.Flv.Parser;
|
|||
using BililiveRecorder.Flv.Pipeline;
|
||||
using BililiveRecorder.Flv.Writer;
|
||||
using BililiveRecorder.Flv.Xml;
|
||||
using BililiveRecorder.ToolBox.Commands;
|
||||
using BililiveRecorder.WPF.Controls;
|
||||
using BililiveRecorder.WPF.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
@ -72,7 +73,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
progressDialog = new AutoFixProgressDialog();
|
||||
var showTask = progressDialog.ShowAsync();
|
||||
|
||||
IFlvWriterTargetProvider? targetProvider = null;
|
||||
string? output_path;
|
||||
{
|
||||
var title = "选择保存位置";
|
||||
var fileDialog = new CommonSaveFileDialog()
|
||||
|
@ -87,49 +88,25 @@ namespace BililiveRecorder.WPF.Pages
|
|||
DefaultFileName = Path.GetFileName(inputPath)
|
||||
};
|
||||
if (fileDialog.ShowDialog() == CommonFileDialogResult.Ok)
|
||||
targetProvider = new AutoFixFlvWriterTargetProvider(fileDialog.FileName);
|
||||
output_path = fileDialog.FileName;
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
using var inputStream = File.OpenRead(inputPath);
|
||||
var memoryStreamProvider = new DefaultMemoryStreamProvider();
|
||||
using var grouping = new TagGroupReader(new FlvTagPipeReader(PipeReader.Create(inputStream), memoryStreamProvider, skipData: false, logger: logger));
|
||||
using var writer = new FlvProcessingContextWriter(new FlvTagFileWriter(targetProvider, memoryStreamProvider, logger));
|
||||
var context = new FlvProcessingContext();
|
||||
var session = new Dictionary<object, object?>();
|
||||
var pipeline = new ProcessingPipelineBuilder(new ServiceCollection().BuildServiceProvider()).AddDefault().AddRemoveFillerData().Build();
|
||||
|
||||
await Task.Run(async () =>
|
||||
var req = new FixRequest
|
||||
{
|
||||
var count = 0;
|
||||
while (true)
|
||||
{
|
||||
var group = await grouping.ReadGroupAsync(default).ConfigureAwait(false);
|
||||
if (group is null)
|
||||
break;
|
||||
Input = inputPath,
|
||||
OutputBase = output_path,
|
||||
};
|
||||
|
||||
context.Reset(group, session);
|
||||
pipeline(context);
|
||||
var handler = new FixHandler();
|
||||
|
||||
if (context.Comments.Count > 0)
|
||||
logger.Debug("修复逻辑输出 {@Comments}", context.Comments);
|
||||
|
||||
await writer.WriteAsync(context).ConfigureAwait(false);
|
||||
|
||||
foreach (var action in context.Actions)
|
||||
if (action is PipelineDataAction dataAction)
|
||||
foreach (var tag in dataAction.Tags)
|
||||
tag.BinaryData?.Dispose();
|
||||
|
||||
if (count++ % 5 == 0)
|
||||
var resp = await handler.Handle(req, async p =>
|
||||
{
|
||||
await this.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
progressDialog.Progress = (int)((double)inputStream.Position / inputStream.Length * 98d);
|
||||
progressDialog.Progress = (int)(p * 98d);
|
||||
});
|
||||
}
|
||||
}
|
||||
}).ConfigureAwait(true);
|
||||
|
||||
progressDialog.Hide();
|
||||
|
@ -138,6 +115,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "修复时发生错误");
|
||||
MessageBox.Show("修复时发生错误\n" + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -163,60 +141,22 @@ namespace BililiveRecorder.WPF.Pages
|
|||
progressDialog = new AutoFixProgressDialog();
|
||||
var showTask = progressDialog.ShowAsync();
|
||||
|
||||
using var inputStream = File.OpenRead(inputPath);
|
||||
var memoryStreamProvider = new DefaultMemoryStreamProvider();
|
||||
using var grouping = new TagGroupReader(new FlvTagPipeReader(PipeReader.Create(inputStream), memoryStreamProvider, skipData: false, logger: logger));
|
||||
var comments = new List<ProcessingComment>();
|
||||
var context = new FlvProcessingContext();
|
||||
var session = new Dictionary<object, object?>();
|
||||
var pipeline = new ProcessingPipelineBuilder(new ServiceCollection().BuildServiceProvider()).AddDefault().AddRemoveFillerData().Build();
|
||||
await Task.Run(async () =>
|
||||
var req = new AnalyzeRequest
|
||||
{
|
||||
var count = 0;
|
||||
while (true)
|
||||
{
|
||||
var group = await grouping.ReadGroupAsync(default).ConfigureAwait(false);
|
||||
if (group is null)
|
||||
break;
|
||||
Input = inputPath
|
||||
};
|
||||
|
||||
context.Reset(group, session);
|
||||
pipeline(context);
|
||||
var handler = new AnalyzeHandler();
|
||||
|
||||
if (context.Comments.Count > 0)
|
||||
{
|
||||
logger.Debug("分析逻辑输出 {@Comments}", context.Comments);
|
||||
comments.AddRange(context.Comments);
|
||||
}
|
||||
|
||||
foreach (var action in context.Actions)
|
||||
if (action is PipelineDataAction dataAction)
|
||||
foreach (var tag in dataAction.Tags)
|
||||
tag.BinaryData?.Dispose();
|
||||
|
||||
if (count++ % 5 == 0)
|
||||
var resp = await handler.Handle(req, async p =>
|
||||
{
|
||||
await this.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
progressDialog.Progress = (int)((double)inputStream.Position / inputStream.Length * 98d);
|
||||
progressDialog.Progress = (int)(p * 98d);
|
||||
});
|
||||
}
|
||||
}
|
||||
}).ConfigureAwait(true);
|
||||
|
||||
var countableComments = comments.Where(x => x.T != CommentType.Logging);
|
||||
var model = new AnalyzeResultModel
|
||||
{
|
||||
File = inputPath,
|
||||
NeedFix = countableComments.Any(),
|
||||
Unrepairable = countableComments.Any(x => x.T == CommentType.Unrepairable),
|
||||
IssueTypeOther = countableComments.Count(x => x.T == CommentType.Other),
|
||||
IssueTypeUnrepairable = countableComments.Count(x => x.T == CommentType.Unrepairable),
|
||||
IssueTypeTimestampJump = countableComments.Count(x => x.T == CommentType.TimestampJump),
|
||||
IssueTypeDecodingHeader = countableComments.Count(x => x.T == CommentType.DecodingHeader),
|
||||
IssueTypeRepeatingData = countableComments.Count(x => x.T == CommentType.RepeatingData)
|
||||
};
|
||||
|
||||
this.analyzeResultDisplayArea.DataContext = model;
|
||||
this.analyzeResultDisplayArea.DataContext = resp;
|
||||
|
||||
progressDialog.Hide();
|
||||
await showTask.ConfigureAwait(true);
|
||||
|
@ -224,6 +164,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "分析时发生错误");
|
||||
MessageBox.Show("分析时发生错误\n" + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -273,35 +214,19 @@ namespace BililiveRecorder.WPF.Pages
|
|||
return;
|
||||
}
|
||||
|
||||
using var inputStream = File.OpenRead(inputPath);
|
||||
var outputStream = File.OpenWrite(outputPath);
|
||||
var req = new ExportRequest
|
||||
{
|
||||
Input = inputPath,
|
||||
Output = outputPath
|
||||
};
|
||||
|
||||
var tags = new List<Tag>();
|
||||
using var reader = new FlvTagPipeReader(PipeReader.Create(inputStream), new DefaultMemoryStreamProvider(), skipData: true, logger: logger);
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var count = 0;
|
||||
while (true)
|
||||
{
|
||||
var tag = await reader.ReadTagAsync(default).ConfigureAwait(false);
|
||||
if (tag is null) break;
|
||||
tags.Add(tag);
|
||||
if (count++ % 300 == 0)
|
||||
var handler = new ExportHandler();
|
||||
|
||||
var resp = await handler.Handle(req, async p =>
|
||||
{
|
||||
await this.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
progressDialog.Progress = (int)((double)inputStream.Position / inputStream.Length * 95d);
|
||||
});
|
||||
}
|
||||
}
|
||||
}).ConfigureAwait(true);
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
using var writer = new StreamWriter(new GZipStream(outputStream, CompressionLevel.Optimal));
|
||||
XmlFlvFile.Serializer.Serialize(writer, new XmlFlvFile
|
||||
{
|
||||
Tags = tags
|
||||
progressDialog.Progress = (int)(p * 95d);
|
||||
});
|
||||
}).ConfigureAwait(true);
|
||||
|
||||
|
@ -311,6 +236,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "导出时发生错误");
|
||||
MessageBox.Show("导出时发生错误\n" + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -321,30 +247,5 @@ namespace BililiveRecorder.WPF.Pages
|
|||
catch (Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
private class AutoFixFlvWriterTargetProvider : IFlvWriterTargetProvider
|
||||
{
|
||||
private readonly string pathTemplate;
|
||||
private int fileIndex = 1;
|
||||
|
||||
public AutoFixFlvWriterTargetProvider(string pathTemplate)
|
||||
{
|
||||
this.pathTemplate = pathTemplate;
|
||||
}
|
||||
|
||||
public Stream CreateAlternativeHeaderStream()
|
||||
{
|
||||
var path = Path.ChangeExtension(this.pathTemplate, "header.txt");
|
||||
return File.Open(path, FileMode.Append, FileAccess.Write, FileShare.None);
|
||||
}
|
||||
|
||||
public (Stream stream, object state) CreateOutputStream()
|
||||
{
|
||||
var i = this.fileIndex++;
|
||||
var path = Path.ChangeExtension(this.pathTemplate, $"fix_p{i}.flv");
|
||||
var fileStream = File.Create(path);
|
||||
return (fileStream, null!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@ using System.CommandLine.Invocation;
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
using BililiveRecorder.ToolBox;
|
||||
using Sentry;
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
|
@ -29,6 +31,7 @@ namespace BililiveRecorder.WPF
|
|||
|
||||
static Program()
|
||||
{
|
||||
AttachConsole(-1);
|
||||
levelSwitchGlobal = new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Debug);
|
||||
if (Debugger.IsAttached)
|
||||
levelSwitchGlobal.MinimumLevel = Serilog.Events.LogEventLevel.Verbose;
|
||||
|
@ -82,7 +85,8 @@ namespace BililiveRecorder.WPF
|
|||
new Option<bool>("--squirrel-firstrun")
|
||||
{
|
||||
IsHidden = true
|
||||
}
|
||||
},
|
||||
new ToolCommand(),
|
||||
};
|
||||
root.Handler = CommandHandler.Create((bool squirrelFirstrun) => Commands.RunWpfHandler(null, squirrelFirstrun));
|
||||
return root;
|
||||
|
@ -126,20 +130,6 @@ namespace BililiveRecorder.WPF
|
|||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
}
|
||||
}
|
||||
|
||||
internal static int Tool_Fix(string path)
|
||||
{
|
||||
levelSwitchConsole.MinimumLevel = Serilog.Events.LogEventLevel.Information;
|
||||
// run code
|
||||
return 0;
|
||||
}
|
||||
|
||||
internal static int Tool_Parse(string path)
|
||||
{
|
||||
levelSwitchConsole.MinimumLevel = Serilog.Events.LogEventLevel.Information;
|
||||
// run code
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static Logger BuildLogger() => new LoggerConfiguration()
|
||||
|
@ -156,7 +146,7 @@ namespace BililiveRecorder.WPF
|
|||
#else
|
||||
.WriteTo.Sink<WpfLogEventSink>(Serilog.Events.LogEventLevel.Information)
|
||||
#endif
|
||||
.WriteTo.File(new CompactJsonFormatter(), "./logs/bilirec.txt", shared: true, rollingInterval: RollingInterval.Day)
|
||||
.WriteTo.File(new CompactJsonFormatter(), "./logs/bilirec.txt", shared: true, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true)
|
||||
.WriteTo.Sentry(o =>
|
||||
{
|
||||
o.Dsn = "https://7c6c5da3140543809661813aaa836207@o210546.ingest.sentry.io/5556540";
|
||||
|
@ -175,6 +165,9 @@ namespace BililiveRecorder.WPF
|
|||
})
|
||||
.CreateLogger();
|
||||
|
||||
[DllImport("kernel32")]
|
||||
private static extern bool AttachConsole(int pid);
|
||||
|
||||
[HandleProcessCorruptedStateExceptions, SecurityCritical]
|
||||
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
|
|
|
@ -27,7 +27,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Core.UnitT
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Flv.UnitTests", "test\BililiveRecorder.Flv.UnitTests\BililiveRecorder.Flv.UnitTests.csproj", "{560E8483-9293-410E-81E9-AB36B49F8A7C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.Flv.RuleTests", "test\BililiveRecorder.Flv.RuleTests\BililiveRecorder.Flv.RuleTests.csproj", "{75DA0162-DE06-4FA0-B6F8-C82C11AF65BC}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Flv.RuleTests", "test\BililiveRecorder.Flv.RuleTests\BililiveRecorder.Flv.RuleTests.csproj", "{75DA0162-DE06-4FA0-B6F8-C82C11AF65BC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.ToolBox", "BililiveRecorder.ToolBox\BililiveRecorder.ToolBox.csproj", "{4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
@ -63,6 +65,10 @@ Global
|
|||
{75DA0162-DE06-4FA0-B6F8-C82C11AF65BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{75DA0162-DE06-4FA0-B6F8-C82C11AF65BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{75DA0162-DE06-4FA0-B6F8-C82C11AF65BC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -75,12 +81,13 @@ Global
|
|||
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}
|
||||
{560E8483-9293-410E-81E9-AB36B49F8A7C} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}
|
||||
{75DA0162-DE06-4FA0-B6F8-C82C11AF65BC} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}
|
||||
{4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200} = {2D44A59D-E437-4FEE-8A2E-3FF00D53A64D}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
RESX_ShowErrorsInErrorList = False
|
||||
RESX_SaveFilesImmediatelyUponChange = True
|
||||
RESX_NeutralResourcesLanguage = zh-Hans
|
||||
SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170}
|
||||
RESX_SortFileContentOnSave = True
|
||||
SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170}
|
||||
RESX_NeutralResourcesLanguage = zh-Hans
|
||||
RESX_SaveFilesImmediatelyUponChange = True
|
||||
RESX_ShowErrorsInErrorList = False
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
Loading…
Reference in New Issue
Block a user