From 9c33d6473478c83781ab6f0a7d2f30de50ee87c8 Mon Sep 17 00:00:00 2001 From: Genteure Date: Thu, 15 Jul 2021 19:56:58 +0800 Subject: [PATCH] Toolbox & CLI: Redo console output, Add `configure` subcommand --- .../Configure/ConfigureCommand.cs | 202 ++++++++++++++++++ .../Configure/JsonSchemaSelection.cs | 13 ++ .../Configure/RootMenuSelection.cs | 31 +++ BililiveRecorder.Cli/Program.cs | 2 + BililiveRecorder.Core/Config/ConfigParser.cs | 2 +- .../BililiveRecorder.ToolBox.csproj | 1 + BililiveRecorder.ToolBox/CommandResponse.cs | 4 +- BililiveRecorder.ToolBox/ICommandHandler.cs | 7 +- BililiveRecorder.ToolBox/ICommandRequest.cs | 2 +- BililiveRecorder.ToolBox/IResponseData.cs | 7 + BililiveRecorder.ToolBox/ProgressCallback.cs | 6 + .../Tool/Analyze/AnalyzeHandler.cs | 31 +-- .../Tool/Analyze/AnalyzeResponse.cs | 44 +++- .../Tool/Export/ExportHandler.cs | 10 +- .../Tool/Export/ExportResponse.cs | 5 +- .../Tool/Fix/FixHandler.cs | 37 +--- .../Tool/Fix/FixResponse.cs | 48 ++++- BililiveRecorder.ToolBox/ToolCommand.cs | 68 +++++- 18 files changed, 428 insertions(+), 92 deletions(-) create mode 100644 BililiveRecorder.Cli/Configure/ConfigureCommand.cs create mode 100644 BililiveRecorder.Cli/Configure/JsonSchemaSelection.cs create mode 100644 BililiveRecorder.Cli/Configure/RootMenuSelection.cs create mode 100644 BililiveRecorder.ToolBox/IResponseData.cs create mode 100644 BililiveRecorder.ToolBox/ProgressCallback.cs diff --git a/BililiveRecorder.Cli/Configure/ConfigureCommand.cs b/BililiveRecorder.Cli/Configure/ConfigureCommand.cs new file mode 100644 index 0000000..723c641 --- /dev/null +++ b/BililiveRecorder.Cli/Configure/ConfigureCommand.cs @@ -0,0 +1,202 @@ +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using BililiveRecorder.Core.Config; +using BililiveRecorder.Core.Config.V2; +using Spectre.Console; + +namespace BililiveRecorder.Cli.Configure +{ + public class ConfigureCommand : Command + { + public ConfigureCommand() : base("configure", "Interactively configure config.json") + { + this.AddArgument(new Argument("path") { Description = "Path to work directory or config.json" }); + this.Handler = CommandHandler.Create(Run); + } + + private static int Run(string path) + { + if (!FindConfig(path, out var config, out var fullPath)) + return 1; + + ShowRootMenu(config, fullPath); + + return 0; + } + + private static void ShowRootMenu(ConfigV2 config, string fullPath) + { + while (true) + { + var selection = PromptEnumSelection(); + AnsiConsole.Clear(); + switch (selection) + { + case RootMenuSelection.ListRooms: + ListRooms(config); + break; + case RootMenuSelection.AddRoom: + AddRoom(config); + break; + case RootMenuSelection.DeleteRoom: + DeleteRoom(config); + break; + case RootMenuSelection.SetRoomConfig: + // TODO + AnsiConsole.MarkupLine("[bold red]Not Implemented Yet[/]"); + break; + case RootMenuSelection.SetGlobalConfig: + // TODO + AnsiConsole.MarkupLine("[bold red]Not Implemented Yet[/]"); + break; + case RootMenuSelection.SetJsonSchema: + SetJsonSchema(config); + break; + case RootMenuSelection.Exit: + return; + case RootMenuSelection.SaveAndExit: + if (SaveConfig(config, fullPath)) + return; + else + break; + default: + break; + } + } + } + + private static void ListRooms(ConfigV2 config) + { + var table = new Table() + .AddColumns("Roomid", "AutoRecord") + .Border(TableBorder.Rounded); + + foreach (var room in config.Rooms) + { + table.AddRow(room.RoomId.ToString(), room.AutoRecord ? "[green]Enabled[/]" : "[red]Disabled[/]"); + } + + AnsiConsole.Render(table); + } + + private static void AddRoom(ConfigV2 config) + { + while (true) + { + var roomid = AnsiConsole.Prompt(new TextPrompt("[grey](type 0 to cancel)[/] [green]Roomid[/]:").Validate(x => x switch + { + < 0 => ValidationResult.Error("[red]Roomid can't be negative[/]"), + _ => ValidationResult.Success(), + })); + + if (roomid == 0) + break; + + if (config.Rooms.Any(x => x.RoomId == roomid)) + { + AnsiConsole.MarkupLine("[red]Room already exist[/]"); + continue; + } + + var autoRecord = AnsiConsole.Confirm("Enable auto record?"); + + config.Rooms.Add(new RoomConfig { RoomId = roomid, AutoRecord = autoRecord }); + + AnsiConsole.MarkupLine("[green]Room {0} added to config[/]", roomid); + } + } + + private static void DeleteRoom(ConfigV2 config) + { + var toBeDeleted = AnsiConsole.Prompt(new MultiSelectionPrompt() + .Title("Delete rooms") + .NotRequired() + .UseConverter(r => r.RoomId.ToString()) + .PageSize(15) + .MoreChoicesText("[grey](Move up and down to reveal more rooms)[/]") + .InstructionsText("[grey](Press [blue][/] to toggle selection, [green][/] to delete)[/]") + .AddChoices(config.Rooms)); + + for (var i = 0; i < toBeDeleted.Count; i++) + config.Rooms.Remove(toBeDeleted[i]); + + AnsiConsole.MarkupLine("[green]{0} rooms deleted[/]", toBeDeleted.Count); + } + + private static void SetJsonSchema(ConfigV2 config) + { + var selection = PromptEnumSelection(); + switch (selection) + { + case JsonSchemaSelection.Default: + config.DollarSignSchema = "https://raw.githubusercontent.com/Bililive/BililiveRecorder/dev-1.3/BililiveRecorder.Core/Config/V2/config.schema.json"; + break; + case JsonSchemaSelection.Custom: + config.DollarSignSchema = AnsiConsole.Prompt(new TextPrompt("[green]JSON Schema[/]:").AllowEmpty()); + break; + default: + break; + } + } + + private static bool SaveConfig(ConfigV2 config, string fullPath) + { + try + { + var json = ConfigParser.SaveJson(config); + using var file = new StreamWriter(File.Open(fullPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None)); + file.Write(json); + return true; + } + catch (Exception ex) + { + AnsiConsole.MarkupLine("[red]Write config failed[/]"); + AnsiConsole.WriteException(ex, ExceptionFormats.ShortenPaths | ExceptionFormats.ShowLinks); + return false; + } + } + + private static bool FindConfig(string path, [NotNullWhen(true)] out ConfigV2? config, out string fullPath) + { + if (File.Exists(path)) + { + fullPath = Path.GetFullPath(path); + goto readFile; + } + else if (Directory.Exists(path)) + { + fullPath = Path.GetFullPath(Path.Combine(path, ConfigParser.CONFIG_FILE_NAME)); + goto readFile; + } + else + { + AnsiConsole.MarkupLine("[red]Path does not exist.[/]"); + config = null; + fullPath = string.Empty; + return false; + } + + readFile: + config = ConfigParser.LoadJson(File.ReadAllText(fullPath, Encoding.UTF8)); + var result = config != null; + if (!result) + AnsiConsole.MarkupLine("[red]Load failed.\nBroken or corrupted file, or no permission.[/]"); + return result; + } + + private static string EnumToDescriptionConverter(T value) where T : struct, Enum + { + var type = typeof(T); + var attrs = type.GetMember(Enum.GetName(type, value)!)[0].GetCustomAttributes(typeof(DescriptionAttribute), false); + return (attrs.Length > 0) ? ((DescriptionAttribute)attrs[0]).Description : string.Empty; + } + + private static T PromptEnumSelection() where T : struct, Enum => AnsiConsole.Prompt(new SelectionPrompt().AddChoices(Enum.GetValues()).UseConverter(EnumToDescriptionConverter)); + } +} diff --git a/BililiveRecorder.Cli/Configure/JsonSchemaSelection.cs b/BililiveRecorder.Cli/Configure/JsonSchemaSelection.cs new file mode 100644 index 0000000..a16f975 --- /dev/null +++ b/BililiveRecorder.Cli/Configure/JsonSchemaSelection.cs @@ -0,0 +1,13 @@ +using System.ComponentModel; + +namespace BililiveRecorder.Cli.Configure +{ + public enum JsonSchemaSelection + { + [Description("https://raw.githubusercontent.com/.../config.schema.json")] + Default, + + [Description("Custom")] + Custom + } +} diff --git a/BililiveRecorder.Cli/Configure/RootMenuSelection.cs b/BililiveRecorder.Cli/Configure/RootMenuSelection.cs new file mode 100644 index 0000000..3223b35 --- /dev/null +++ b/BililiveRecorder.Cli/Configure/RootMenuSelection.cs @@ -0,0 +1,31 @@ +using System.ComponentModel; + +namespace BililiveRecorder.Cli.Configure +{ + public enum RootMenuSelection + { + [Description("List rooms")] + ListRooms, + + [Description("Add room")] + AddRoom, + + [Description("Delete room")] + DeleteRoom, + + [Description("Update room config")] + SetRoomConfig, + + [Description("Update global config")] + SetGlobalConfig, + + [Description("Update JSON Schema")] + SetJsonSchema, + + [Description("Exit and discard all changes")] + Exit, + + [Description("Save and Exit")] + SaveAndExit, + } +} diff --git a/BililiveRecorder.Cli/Program.cs b/BililiveRecorder.Cli/Program.cs index 8c324a0..cb5abea 100644 --- a/BililiveRecorder.Cli/Program.cs +++ b/BililiveRecorder.Cli/Program.cs @@ -5,6 +5,7 @@ using System.CommandLine.Invocation; using System.IO; using System.Linq; using System.Threading; +using BililiveRecorder.Cli.Configure; using BililiveRecorder.Core; using BililiveRecorder.Core.Config; using BililiveRecorder.Core.Config.V2; @@ -50,6 +51,7 @@ namespace BililiveRecorder.Cli { cmd_run, cmd_portable, + new ConfigureCommand(), new ToolCommand() }; diff --git a/BililiveRecorder.Core/Config/ConfigParser.cs b/BililiveRecorder.Core/Config/ConfigParser.cs index 6bf3890..008dd93 100644 --- a/BililiveRecorder.Core/Config/ConfigParser.cs +++ b/BililiveRecorder.Core/Config/ConfigParser.cs @@ -10,7 +10,7 @@ namespace BililiveRecorder.Core.Config { public class ConfigParser { - private const string CONFIG_FILE_NAME = "config.json"; + public const string CONFIG_FILE_NAME = "config.json"; private static readonly ILogger logger = Log.ForContext(); private static readonly JsonSerializerSettings settings = new JsonSerializerSettings() { diff --git a/BililiveRecorder.ToolBox/BililiveRecorder.ToolBox.csproj b/BililiveRecorder.ToolBox/BililiveRecorder.ToolBox.csproj index 4728e12..7c9e122 100644 --- a/BililiveRecorder.ToolBox/BililiveRecorder.ToolBox.csproj +++ b/BililiveRecorder.ToolBox/BililiveRecorder.ToolBox.csproj @@ -7,6 +7,7 @@ + diff --git a/BililiveRecorder.ToolBox/CommandResponse.cs b/BililiveRecorder.ToolBox/CommandResponse.cs index 43ba160..46baf3a 100644 --- a/BililiveRecorder.ToolBox/CommandResponse.cs +++ b/BililiveRecorder.ToolBox/CommandResponse.cs @@ -4,12 +4,12 @@ using Newtonsoft.Json.Converters; namespace BililiveRecorder.ToolBox { - public class CommandResponse where TResponse : class + public class CommandResponse where TResponseData : IResponseData { [JsonConverter(typeof(StringEnumConverter))] public ResponseStatus Status { get; set; } - public TResponse? Result { get; set; } + public TResponseData? Data { get; set; } public string? ErrorMessage { get; set; } diff --git a/BililiveRecorder.ToolBox/ICommandHandler.cs b/BililiveRecorder.ToolBox/ICommandHandler.cs index bfdb607..acfe6a0 100644 --- a/BililiveRecorder.ToolBox/ICommandHandler.cs +++ b/BililiveRecorder.ToolBox/ICommandHandler.cs @@ -1,12 +1,13 @@ +using System.Threading; using System.Threading.Tasks; namespace BililiveRecorder.ToolBox { public interface ICommandHandler where TRequest : ICommandRequest - where TResponse : class + where TResponse : IResponseData { - Task> Handle(TRequest request); - void PrintResponse(TResponse response); + string Name { get; } + Task> Handle(TRequest request, CancellationToken cancellationToken, ProgressCallback? progress); } } diff --git a/BililiveRecorder.ToolBox/ICommandRequest.cs b/BililiveRecorder.ToolBox/ICommandRequest.cs index 3109ce5..2a72b4e 100644 --- a/BililiveRecorder.ToolBox/ICommandRequest.cs +++ b/BililiveRecorder.ToolBox/ICommandRequest.cs @@ -1,6 +1,6 @@ namespace BililiveRecorder.ToolBox { public interface ICommandRequest - where TResponse : class + where TResponse : IResponseData { } } diff --git a/BililiveRecorder.ToolBox/IResponseData.cs b/BililiveRecorder.ToolBox/IResponseData.cs new file mode 100644 index 0000000..a0d0197 --- /dev/null +++ b/BililiveRecorder.ToolBox/IResponseData.cs @@ -0,0 +1,7 @@ +namespace BililiveRecorder.ToolBox +{ + public interface IResponseData + { + void PrintToConsole(); + } +} diff --git a/BililiveRecorder.ToolBox/ProgressCallback.cs b/BililiveRecorder.ToolBox/ProgressCallback.cs new file mode 100644 index 0000000..3ab7ee7 --- /dev/null +++ b/BililiveRecorder.ToolBox/ProgressCallback.cs @@ -0,0 +1,6 @@ +using System.Threading.Tasks; + +namespace BililiveRecorder.ToolBox +{ + public delegate Task ProgressCallback(double progress); +} diff --git a/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeHandler.cs b/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeHandler.cs index 6a08258..a80d8b6 100644 --- a/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeHandler.cs +++ b/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeHandler.cs @@ -24,9 +24,9 @@ namespace BililiveRecorder.ToolBox.Tool.Analyze { private static readonly ILogger logger = Log.ForContext(); - public Task> Handle(AnalyzeRequest request) => this.Handle(request, default, null); + public string Name => "Analyze"; - public async Task> Handle(AnalyzeRequest request, CancellationToken cancellationToken, Func? progress) + public async Task> Handle(AnalyzeRequest request, CancellationToken cancellationToken, ProgressCallback? progress) { FileStream? flvFileStream = null; try @@ -152,7 +152,7 @@ namespace BililiveRecorder.ToolBox.Tool.Analyze return new CommandResponse { Status = ResponseStatus.OK, - Result = response + Data = response }; } catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) @@ -192,31 +192,6 @@ namespace BililiveRecorder.ToolBox.Tool.Analyze } } - 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; diff --git a/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeResponse.cs b/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeResponse.cs index 493fbcd..10c7106 100644 --- a/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeResponse.cs +++ b/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeResponse.cs @@ -1,8 +1,9 @@ -using BililiveRecorder.ToolBox.ProcessingRules; +using BililiveRecorder.ToolBox.ProcessingRules; +using Spectre.Console; namespace BililiveRecorder.ToolBox.Tool.Analyze { - public class AnalyzeResponse + public class AnalyzeResponse : IResponseData { public string InputPath { get; set; } = string.Empty; @@ -20,5 +21,44 @@ namespace BililiveRecorder.ToolBox.Tool.Analyze public int IssueTypeTimestampOffset { get; set; } public int IssueTypeDecodingHeader { get; set; } public int IssueTypeRepeatingData { get; set; } + + public void PrintToConsole() + { + if (this.NeedFix) + AnsiConsole.Render(new FigletText("Need Fix").Color(Color.Red)); + else + AnsiConsole.Render(new FigletText("All Good").Color(Color.Green)); + + if (this.Unrepairable) + { + AnsiConsole.Render(new Panel("This file contains error(s) that are identified as unrepairable (yet).\n" + + "Please check if you're using the newest version.\n" + + "Please consider send a sample file to the developer.") + { + Header = new PanelHeader("Important Note"), + Border = BoxBorder.Rounded, + BorderStyle = new Style(foreground: Color.Red) + }); + } + + AnsiConsole.Render(new Panel(this.InputPath.EscapeMarkup()) + { + Header = new PanelHeader("Input"), + Border = BoxBorder.Rounded + }); + + AnsiConsole.MarkupLine("Will output [lime]{0}[/] file(s) if repaired", this.OutputFileCount); + + AnsiConsole.Render(new Table() + .Border(TableBorder.Rounded) + .AddColumns("Category", "Count") + .AddRow("Unrepairable", this.IssueTypeUnrepairable.ToString()) + .AddRow("Other", this.IssueTypeOther.ToString()) + .AddRow("TimestampJump", this.IssueTypeTimestampJump.ToString()) + .AddRow("TimestampOffset", this.IssueTypeTimestampOffset.ToString()) + .AddRow("DecodingHeader", this.IssueTypeDecodingHeader.ToString()) + .AddRow("RepeatingData", this.IssueTypeRepeatingData.ToString()) + ); + } } } diff --git a/BililiveRecorder.ToolBox/Tool/Export/ExportHandler.cs b/BililiveRecorder.ToolBox/Tool/Export/ExportHandler.cs index a8174af..425f522 100644 --- a/BililiveRecorder.ToolBox/Tool/Export/ExportHandler.cs +++ b/BililiveRecorder.ToolBox/Tool/Export/ExportHandler.cs @@ -15,10 +15,10 @@ namespace BililiveRecorder.ToolBox.Tool.Export public class ExportHandler : ICommandHandler { private static readonly ILogger logger = Log.ForContext(); + + public string Name => "Export"; - public Task> Handle(ExportRequest request) => this.Handle(request, default, null); - - public async Task> Handle(ExportRequest request, CancellationToken cancellationToken, Func? progress) + public async Task> Handle(ExportRequest request, CancellationToken cancellationToken, ProgressCallback? progress) { FileStream? inputStream = null, outputStream = null; try @@ -92,7 +92,7 @@ namespace BililiveRecorder.ToolBox.Tool.Export }); }); - return new CommandResponse { Status = ResponseStatus.OK, Result = new ExportResponse() }; + return new CommandResponse { Status = ResponseStatus.OK, Data = new ExportResponse() }; } catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -131,7 +131,5 @@ namespace BililiveRecorder.ToolBox.Tool.Export outputStream?.Dispose(); } } - - public void PrintResponse(ExportResponse response) => Console.WriteLine("OK"); } } diff --git a/BililiveRecorder.ToolBox/Tool/Export/ExportResponse.cs b/BililiveRecorder.ToolBox/Tool/Export/ExportResponse.cs index 8ffbb15..ca0dbb7 100644 --- a/BililiveRecorder.ToolBox/Tool/Export/ExportResponse.cs +++ b/BililiveRecorder.ToolBox/Tool/Export/ExportResponse.cs @@ -1,6 +1,7 @@ -namespace BililiveRecorder.ToolBox.Tool.Export +namespace BililiveRecorder.ToolBox.Tool.Export { - public class ExportResponse + public class ExportResponse : IResponseData { + public void PrintToConsole() { } } } diff --git a/BililiveRecorder.ToolBox/Tool/Fix/FixHandler.cs b/BililiveRecorder.ToolBox/Tool/Fix/FixHandler.cs index 979aba1..35194f4 100644 --- a/BililiveRecorder.ToolBox/Tool/Fix/FixHandler.cs +++ b/BililiveRecorder.ToolBox/Tool/Fix/FixHandler.cs @@ -23,9 +23,9 @@ namespace BililiveRecorder.ToolBox.Tool.Fix { private static readonly ILogger logger = Log.ForContext(); - public Task> Handle(FixRequest request) => this.Handle(request, default, null); + public string Name => "Fix"; - public async Task> Handle(FixRequest request, CancellationToken cancellationToken, Func? progress) + public async Task> Handle(FixRequest request, CancellationToken cancellationToken, ProgressCallback? progress) { FileStream? flvFileStream = null; try @@ -194,7 +194,7 @@ namespace BililiveRecorder.ToolBox.Tool.Fix }; }); - return new CommandResponse { Status = ResponseStatus.OK, Result = response }; + return new CommandResponse { Status = ResponseStatus.OK, Data = response }; } catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -233,37 +233,6 @@ namespace BililiveRecorder.ToolBox.Tool.Fix } } - 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; diff --git a/BililiveRecorder.ToolBox/Tool/Fix/FixResponse.cs b/BililiveRecorder.ToolBox/Tool/Fix/FixResponse.cs index e6f4134..a7ca460 100644 --- a/BililiveRecorder.ToolBox/Tool/Fix/FixResponse.cs +++ b/BililiveRecorder.ToolBox/Tool/Fix/FixResponse.cs @@ -1,9 +1,10 @@ -using System; +using System; using BililiveRecorder.ToolBox.ProcessingRules; +using Spectre.Console; namespace BililiveRecorder.ToolBox.Tool.Fix { - public class FixResponse + public class FixResponse : IResponseData { public string InputPath { get; set; } = string.Empty; @@ -23,5 +24,48 @@ namespace BililiveRecorder.ToolBox.Tool.Fix public int IssueTypeTimestampOffset { get; set; } public int IssueTypeDecodingHeader { get; set; } public int IssueTypeRepeatingData { get; set; } + + public void PrintToConsole() + { + AnsiConsole.Render(new FigletText("Done").Color(Color.Green)); + + if (this.Unrepairable) + { + AnsiConsole.Render(new Panel("This file contains error(s) that are identified as unrepairable (yet).\n" + + "Please check if you're using the newest version.\n" + + "Please consider send a sample file to the developer.") + { + Header = new PanelHeader("Important Note"), + Border = BoxBorder.Rounded, + BorderStyle = new Style(foreground: Color.Red) + }); + } + + AnsiConsole.Render(new Panel(this.InputPath.EscapeMarkup()) + { + Header = new PanelHeader("Input"), + Border = BoxBorder.Rounded + }); + + var table_output = new Table() + .Border(TableBorder.Rounded) + .AddColumns("Output"); + + for (var i = 0; i < this.OutputPaths.Length; i++) + table_output.AddRow(this.OutputPaths[i]); + + AnsiConsole.Render(table_output); + + AnsiConsole.Render(new Table() + .Border(TableBorder.Rounded) + .AddColumns("Category", "Count") + .AddRow("Unrepairable", this.IssueTypeUnrepairable.ToString()) + .AddRow("Other", this.IssueTypeOther.ToString()) + .AddRow("TimestampJump", this.IssueTypeTimestampJump.ToString()) + .AddRow("TimestampOffset", this.IssueTypeTimestampOffset.ToString()) + .AddRow("DecodingHeader", this.IssueTypeDecodingHeader.ToString()) + .AddRow("RepeatingData", this.IssueTypeRepeatingData.ToString()) + ); + } } } diff --git a/BililiveRecorder.ToolBox/ToolCommand.cs b/BililiveRecorder.ToolBox/ToolCommand.cs index 611b9dd..cff2407 100644 --- a/BililiveRecorder.ToolBox/ToolCommand.cs +++ b/BililiveRecorder.ToolBox/ToolCommand.cs @@ -6,6 +6,7 @@ using BililiveRecorder.ToolBox.Tool.Analyze; using BililiveRecorder.ToolBox.Tool.Export; using BililiveRecorder.ToolBox.Tool.Fix; using Newtonsoft.Json; +using Spectre.Console; namespace BililiveRecorder.ToolBox { @@ -34,7 +35,7 @@ namespace BililiveRecorder.ToolBox private void RegisterCommand(string name, string? description, Action configure) where THandler : ICommandHandler where TRequest : ICommandRequest - where TResponse : class + where TResponse : IResponseData { var cmd = new Command(name, description) { @@ -49,32 +50,77 @@ namespace BililiveRecorder.ToolBox private static async Task RunSubCommand(TRequest request, bool json, bool jsonIndented) where THandler : ICommandHandler where TRequest : ICommandRequest - where TResponse : class + where TResponse : IResponseData { + var isInteractive = !(json || jsonIndented); var handler = Activator.CreateInstance(); - var response = await handler.Handle(request).ConfigureAwait(false); - if (json || jsonIndented) + CommandResponse? response; + if (isInteractive) { - var json_str = JsonConvert.SerializeObject(response, jsonIndented ? Formatting.Indented : Formatting.None); - Console.WriteLine(json_str); + response = await AnsiConsole + .Progress() + .Columns(new ProgressColumn[] + { + new TaskDescriptionColumn(), + new ProgressBarColumn(), + new PercentageColumn(), + new SpinnerColumn(Spinner.Known.Dots10), + }) + .StartAsync(async ctx => + { + var t = ctx.AddTask(handler.Name); + t.MaxValue = 1d; + var r = await handler.Handle(request, default, async p => t.Value = p).ConfigureAwait(false); + t.Value = 1d; + return r; + }) + .ConfigureAwait(false); } else + { + response = await handler.Handle(request, default, null).ConfigureAwait(false); + } + + if (isInteractive) { if (response.Status == ResponseStatus.OK) { - handler.PrintResponse(response.Result!); + response.Data?.PrintToConsole(); + + return 0; } else { - Console.Write("Error: "); - Console.WriteLine(response.Status); - Console.WriteLine(response.ErrorMessage); + AnsiConsole.Render(new FigletText("Error").Color(Color.Red)); + + var errorInfo = new Table + { + Border = TableBorder.Rounded + }; + errorInfo.AddColumn(new TableColumn("Error Code").Centered()); + errorInfo.AddColumn(new TableColumn("Error Message").Centered()); + errorInfo.AddRow("[red]" + response.Status.ToString().EscapeMarkup() + "[/]", "[red]" + (response.ErrorMessage ?? string.Empty) + "[/]"); + AnsiConsole.Render(errorInfo); + + if (response.Exception is not null) + AnsiConsole.Render(new Panel(response.Exception.GetRenderable(ExceptionFormats.ShortenPaths | ExceptionFormats.ShowLinks)) + { + Header = new PanelHeader("Exception Info"), + Border = BoxBorder.Rounded + }); + + return 1; } } + else + { + var json_str = JsonConvert.SerializeObject(response, jsonIndented ? Formatting.Indented : Formatting.None); + Console.WriteLine(json_str); - return 0; + return response.Status == ResponseStatus.OK ? 0 : 1; + } } } }