diff --git a/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj b/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj index b671d53..340a5f7 100644 --- a/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj +++ b/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj @@ -33,6 +33,7 @@ + diff --git a/BililiveRecorder.Cli/Program.cs b/BililiveRecorder.Cli/Program.cs index eacdb7f..db2bc55 100644 --- a/BililiveRecorder.Cli/Program.cs +++ b/BililiveRecorder.Cli/Program.cs @@ -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, }; diff --git a/BililiveRecorder.Flv/Writer/FlvTagFileWriter.cs b/BililiveRecorder.Flv/Writer/FlvTagFileWriter.cs index 26ee860..0e66bfe 100644 --- a/BililiveRecorder.Flv/Writer/FlvTagFileWriter.cs +++ b/BililiveRecorder.Flv/Writer/FlvTagFileWriter.cs @@ -34,7 +34,7 @@ namespace BililiveRecorder.Flv.Writer public bool CloseCurrentFile() { - if (this.disposedValue) + if (this.disposedValue) throw new ObjectDisposedException(nameof(FlvTagFileWriter)); if (this.stream is null) @@ -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(); diff --git a/BililiveRecorder.ToolBox/BililiveRecorder.ToolBox.csproj b/BililiveRecorder.ToolBox/BililiveRecorder.ToolBox.csproj new file mode 100644 index 0000000..2957349 --- /dev/null +++ b/BililiveRecorder.ToolBox/BililiveRecorder.ToolBox.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + diff --git a/BililiveRecorder.ToolBox/Commands/Analyze.cs b/BililiveRecorder.ToolBox/Commands/Analyze.cs new file mode 100644 index 0000000..99131d3 --- /dev/null +++ b/BililiveRecorder.ToolBox/Commands/Analyze.cs @@ -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 + { + 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 + { + private static readonly ILogger logger = Log.ForContext(); + + public Task Handle(AnalyzeRequest request) => this.Handle(request, null); + + public async Task Handle(AnalyzeRequest request, Func? progress) + { + var inputPath = Path.GetFullPath(request.Input); + + var memoryStreamProvider = new DefaultMemoryStreamProvider(); + var tagWriter = new AnalyzeMockFlvTagWriter(); + var comments = new List(); + var context = new FlvProcessingContext(); + var session = new Dictionary(); + + { + 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 tags) => Task.CompletedTask; + public Task WriteTag(Tag tag) => Task.CompletedTask; + } + } +} diff --git a/BililiveRecorder.ToolBox/Commands/Export.cs b/BililiveRecorder.ToolBox/Commands/Export.cs new file mode 100644 index 0000000..1608887 --- /dev/null +++ b/BililiveRecorder.ToolBox/Commands/Export.cs @@ -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 + { + public string Input { get; set; } = string.Empty; + + public string Output { get; set; } = string.Empty; + } + + public class ExportResponse + { + } + + public class ExportHandler : ICommandHandler + { + private static readonly ILogger logger = Log.ForContext(); + + public Task Handle(ExportRequest request) => this.Handle(request, null); + + public async Task Handle(ExportRequest request, Func? progress) + { + using var inputStream = File.OpenRead(request.Input); + using var outputStream = File.OpenWrite(request.Output); + + var tags = new List(); + + { + 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) + { } + } +} diff --git a/BililiveRecorder.ToolBox/Commands/Fix.cs b/BililiveRecorder.ToolBox/Commands/Fix.cs new file mode 100644 index 0000000..206e1ec --- /dev/null +++ b/BililiveRecorder.ToolBox/Commands/Fix.cs @@ -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 + { + 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(); + + 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 + { + private static readonly ILogger logger = Log.ForContext(); + + public Task Handle(FixRequest request) => this.Handle(request, null); + + public async Task Handle(FixRequest request, Func? progress) + { + var inputPath = Path.GetFullPath(request.Input); + + var outputPaths = new List(); + 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(); + var context = new FlvProcessingContext(); + var session = new Dictionary(); + + { + 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? 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!); + } + } + } +} diff --git a/BililiveRecorder.ToolBox/ICommandHandler.cs b/BililiveRecorder.ToolBox/ICommandHandler.cs new file mode 100644 index 0000000..421fd87 --- /dev/null +++ b/BililiveRecorder.ToolBox/ICommandHandler.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace BililiveRecorder.ToolBox +{ + public interface ICommandHandler where TRequest : ICommandRequest + { + Task Handle(TRequest request); + void PrintResponse(TResponse response); + } +} diff --git a/BililiveRecorder.ToolBox/ICommandRequest.cs b/BililiveRecorder.ToolBox/ICommandRequest.cs new file mode 100644 index 0000000..683b1d7 --- /dev/null +++ b/BililiveRecorder.ToolBox/ICommandRequest.cs @@ -0,0 +1,4 @@ +namespace BililiveRecorder.ToolBox +{ + public interface ICommandRequest { } +} diff --git a/BililiveRecorder.ToolBox/ToolCommand.cs b/BililiveRecorder.ToolBox/ToolCommand.cs new file mode 100644 index 0000000..ad517bc --- /dev/null +++ b/BililiveRecorder.ToolBox/ToolCommand.cs @@ -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("analyze", null, c => + { + c.Add(new Argument("input", "example: input.flv")); + }); + + this.RegisterCommand("fix", null, c => + { + c.Add(new Argument("input", "example: input.flv")); + c.Add(new Argument("output-base", "example: output.flv")); + }); + + this.RegisterCommand("export", null, c => + { + c.Add(new Argument("input", "example: input.flv")); + c.Add(new Argument("output", "example: output.brec.xml.gz")); + }); + } + + private void RegisterCommand(string name, string? description, Action configure) + where IHandler : ICommandHandler + where IRequest : ICommandRequest + { + var cmd = new Command(name, description) + { + new Option("--json", "print result as json string"), + new Option("--json-indented", "print result as indented json string") + }; + cmd.Handler = CommandHandler.Create((IRequest r, bool json, bool jsonIndented) => RunSubCommand(r, json, jsonIndented)); + configure(cmd); + this.Add(cmd); + } + + private static async Task RunSubCommand(IRequest request, bool json, bool jsonIndented) + where IHandler : ICommandHandler + where IRequest : ICommandRequest + { + var handler = Activator.CreateInstance(); + + 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; + } + } +} diff --git a/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj b/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj index 83f3e3e..93e94d6 100644 --- a/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj +++ b/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj @@ -120,7 +120,6 @@ - @@ -293,6 +292,10 @@ {7610e19c-d3ab-4cbc-983e-6fda36f4d4b3} BililiveRecorder.Flv + + {4faae8e7-ac4e-4e99-a7d1-53d20ad8a200} + BililiveRecorder.ToolBox + diff --git a/BililiveRecorder.WPF/Models/AnalyzeResultModel.cs b/BililiveRecorder.WPF/Models/AnalyzeResultModel.cs deleted file mode 100644 index c90989e..0000000 --- a/BililiveRecorder.WPF/Models/AnalyzeResultModel.cs +++ /dev/null @@ -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(ref T field, T value, [CallerMemberName] string propertyName = "") - { - if (EqualityComparer.Default.Equals(field, value)) { return false; } - field = value; this.OnPropertyChanged(propertyName); return true; - } - } -} diff --git a/BililiveRecorder.WPF/Pages/ToolboxAutoFixPage.xaml b/BililiveRecorder.WPF/Pages/ToolboxAutoFixPage.xaml index 5e028a3..889b6b6 100644 --- a/BililiveRecorder.WPF/Pages/ToolboxAutoFixPage.xaml +++ b/BililiveRecorder.WPF/Pages/ToolboxAutoFixPage.xaml @@ -11,7 +11,8 @@ l:ResxLocalizationProvider.DefaultDictionary="Strings" xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages" xmlns:model="clr-namespace:BililiveRecorder.WPF.Models" - xmlns:c="clr-namespace:BililiveRecorder.WPF.Converters" + 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 @@ - + @@ -75,7 +76,7 @@ - + + diff --git a/BililiveRecorder.WPF/Pages/ToolboxAutoFixPage.xaml.cs b/BililiveRecorder.WPF/Pages/ToolboxAutoFixPage.xaml.cs index 985e928..f09c833 100644 --- a/BililiveRecorder.WPF/Pages/ToolboxAutoFixPage.xaml.cs +++ b/BililiveRecorder.WPF/Pages/ToolboxAutoFixPage.xaml.cs @@ -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(); - var pipeline = new ProcessingPipelineBuilder(new ServiceCollection().BuildServiceProvider()).AddDefault().AddRemoveFillerData().Build(); - - await Task.Run(async () => + var req = new FixRequest { - var count = 0; - while (true) + Input = inputPath, + OutputBase = output_path, + }; + + var handler = new FixHandler(); + + var resp = await handler.Handle(req, async p => + { + await this.Dispatcher.InvokeAsync(() => { - var group = await grouping.ReadGroupAsync(default).ConfigureAwait(false); - if (group is null) - break; - - context.Reset(group, session); - pipeline(context); - - 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) - { - 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(); - var context = new FlvProcessingContext(); - var session = new Dictionary(); - 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; - - context.Reset(group, session); - pipeline(context); - - 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) - { - await this.Dispatcher.InvokeAsync(() => - { - progressDialog.Progress = (int)((double)inputStream.Position / inputStream.Length * 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) + Input = inputPath }; - this.analyzeResultDisplayArea.DataContext = model; + var handler = new AnalyzeHandler(); + + var resp = await handler.Handle(req, async p => + { + await this.Dispatcher.InvokeAsync(() => + { + progressDialog.Progress = (int)(p * 98d); + }); + }).ConfigureAwait(true); + + 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 tags = new List(); - using var reader = new FlvTagPipeReader(PipeReader.Create(inputStream), new DefaultMemoryStreamProvider(), skipData: true, logger: logger); - await Task.Run(async () => + var req = new ExportRequest { - 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) - { - await this.Dispatcher.InvokeAsync(() => - { - progressDialog.Progress = (int)((double)inputStream.Position / inputStream.Length * 95d); - }); - } - } - }).ConfigureAwait(true); + Input = inputPath, + Output = outputPath + }; - await Task.Run(() => + var handler = new ExportHandler(); + + var resp = await handler.Handle(req, async p => { - using var writer = new StreamWriter(new GZipStream(outputStream, CompressionLevel.Optimal)); - XmlFlvFile.Serializer.Serialize(writer, new XmlFlvFile + await this.Dispatcher.InvokeAsync(() => { - 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!); - } - } } } diff --git a/BililiveRecorder.WPF/Program.cs b/BililiveRecorder.WPF/Program.cs index dbc0304..652793c 100644 --- a/BililiveRecorder.WPF/Program.cs +++ b/BililiveRecorder.WPF/Program.cs @@ -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("--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(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) { diff --git a/BililiveRecorder.sln b/BililiveRecorder.sln index d0dac8e..a448d53 100644 --- a/BililiveRecorder.sln +++ b/BililiveRecorder.sln @@ -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