[CLI] Update CliFx and use Spectre.Console for progress reporting

This commit is contained in:
Tyrrrz
2021-03-23 22:38:44 +02:00
parent 6f90c367b9
commit 017ed5ae6d
13 changed files with 193 additions and 121 deletions

View File

@@ -1,12 +1,12 @@
using System.IO;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Utilities;
using CliFx.Infrastructure;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Exporting;
using Spectre.Console;
namespace DiscordChatExporter.Cli.Commands.Base
{
@@ -39,14 +39,8 @@ namespace DiscordChatExporter.Cli.Commands.Base
private ChannelExporter? _channelExporter;
protected ChannelExporter Exporter => _channelExporter ??= new ChannelExporter(Discord);
protected async ValueTask ExportAsync(IConsole console, Guild guild, Channel channel)
protected async ValueTask ExportChannelAsync(Guild guild, Channel channel, ProgressContext progressContext)
{
await console.Output.WriteAsync(
$"Exporting channel '{channel.Category} / {channel.Name}'... "
);
var progress = console.CreateProgressTicker();
var request = new ExportRequest(
guild,
channel,
@@ -60,22 +54,19 @@ namespace DiscordChatExporter.Cli.Commands.Base
DateFormat
);
await Exporter.ExportChannelAsync(request, progress);
var progress = progressContext.AddTask(
$"{channel.Category} / {channel.Name}",
new ProgressTaskSettings {MaxValue = 1}
);
await console.Output.WriteLineAsync();
await console.Output.WriteLineAsync("Done.");
}
protected async ValueTask ExportAsync(IConsole console, Channel channel)
{
var guild = await Discord.GetGuildAsync(channel.GuildId);
await ExportAsync(console, guild, channel);
}
protected async ValueTask ExportAsync(IConsole console, Snowflake channelId)
{
var channel = await Discord.GetChannelAsync(channelId);
await ExportAsync(console, channel);
try
{
await Exporter.ExportChannelAsync(request, progress);
}
finally
{
progress.StopTask();
}
}
public override ValueTask ExecuteAsync(IConsole console)

View File

@@ -1,16 +1,15 @@
using System.Collections.Concurrent;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Utilities;
using CliFx.Exceptions;
using CliFx.Infrastructure;
using DiscordChatExporter.Cli.Utils.Extensions;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Exceptions;
using DiscordChatExporter.Core.Exporting;
using DiscordChatExporter.Core.Utils.Extensions;
using Gress;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Cli.Commands.Base
@@ -20,63 +19,60 @@ namespace DiscordChatExporter.Cli.Commands.Base
[CommandOption("parallel", Description = "Limits how many channels can be exported in parallel.")]
public int ParallelLimit { get; init; } = 1;
protected async ValueTask ExportMultipleAsync(IConsole console, IReadOnlyList<Channel> channels)
protected async ValueTask ExportChannelsAsync(IConsole console, IReadOnlyList<Channel> channels)
{
// This uses a different route from ExportCommandBase.ExportAsync() because it runs
// in parallel and needs another way to report progress to console.
await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)...");
await console.Output.WriteAsync(
$"Exporting {channels.Count} channels... "
var errors = new ConcurrentDictionary<Channel, string>();
await console.CreateProgressTicker().StartAsync(async progressContext =>
{
await channels.ParallelForEachAsync(async channel =>
{
try
{
var guild = await Discord.GetGuildAsync(channel.GuildId);
await ExportChannelAsync(guild, channel, progressContext);
}
catch (DiscordChatExporterException ex) when (!ex.IsCritical)
{
errors[channel] = ex.Message;
}
}, ParallelLimit.ClampMin(1));
await console.Output.WriteLineAsync();
});
// Print result
await console.Output.WriteLineAsync(
$"Successfully exported {channels.Count - errors.Count} channel(s)."
);
var progress = console.CreateProgressTicker();
var operations = progress.Wrap().CreateOperations(channels.Count);
var successfulExportCount = 0;
var errors = new ConcurrentBag<(Channel, string)>();
await channels.Zip(operations).ParallelForEachAsync(async tuple =>
// Print errors
if (errors.Any())
{
var (channel, operation) = tuple;
using (console.WithForegroundColor(ConsoleColor.Red))
await console.Output.WriteLineAsync($"Failed to export {errors.Count} channel(s):");
try
foreach (var (channel, error) in errors)
{
var guild = await Discord.GetGuildAsync(channel.GuildId);
await console.Output.WriteAsync($"{channel.Category} / {channel.Name}: ");
var request = new ExportRequest(
guild,
channel,
OutputPath,
ExportFormat,
After,
Before,
PartitionLimit,
ShouldDownloadMedia,
ShouldReuseMedia,
DateFormat
);
await Exporter.ExportChannelAsync(request, operation);
Interlocked.Increment(ref successfulExportCount);
using (console.WithForegroundColor(ConsoleColor.Red))
await console.Output.WriteLineAsync(error);
}
catch (DiscordChatExporterException ex) when (!ex.IsCritical)
{
errors.Add((channel, ex.Message));
}
finally
{
operation.Dispose();
}
}, ParallelLimit.ClampMin(1));
await console.Output.WriteLineAsync();
await console.Output.WriteLineAsync();
}
foreach (var (channel, error) in errors)
await console.Error.WriteLineAsync($"Channel '{channel}': {error}");
// Fail the command if ALL channels failed to export.
// Having some of the channels fail to export is fine and expected.
if (errors.Count >= channels.Count)
{
throw new CommandException("Export failed.");
}
await console.Output.WriteLineAsync($"Successfully exported {successfulExportCount} channel(s).");
await console.Output.WriteLineAsync("Done.");
}
}
}

View File

@@ -1,16 +1,17 @@
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using DiscordChatExporter.Core.Discord;
namespace DiscordChatExporter.Cli.Commands.Base
{
public abstract class TokenCommandBase : ICommand
{
[CommandOption("token", 't', IsRequired = true, EnvironmentVariableName = "DISCORD_TOKEN", Description = "Authentication token.")]
[CommandOption("token", 't', IsRequired = true, EnvironmentVariable = "DISCORD_TOKEN", Description = "Authentication token.")]
public string TokenValue { get; init; } = "";
[CommandOption("bot", 'b', EnvironmentVariableName = "DISCORD_TOKEN_BOT", Description = "Authenticate as a bot.")]
[CommandOption("bot", 'b', EnvironmentVariable = "DISCORD_TOKEN_BOT", Description = "Authenticate as a bot.")]
public bool IsBotToken { get; init; }
private AuthToken GetAuthToken() => new(