[CLI] Remove ExportMultipleCommandBase and allow multiple channel IDs in ExportChannelCommand

pull/497/head
Tyrrrz 4 years ago
parent 390735032e
commit 026927265a

@ -1,12 +1,20 @@
using System.IO;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Infrastructure;
using DiscordChatExporter.Cli.Utils.Extensions;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Exceptions;
using DiscordChatExporter.Core.Exporting;
using DiscordChatExporter.Core.Utils.Extensions;
using Spectre.Console;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Cli.Commands.Base
{
@ -27,6 +35,9 @@ namespace DiscordChatExporter.Cli.Commands.Base
[CommandOption("partition", 'p', Description = "Split output into partitions limited to this number of messages.")]
public int? PartitionLimit { get; init; }
[CommandOption("parallel", Description = "Limits how many channels can be exported in parallel.")]
public int ParallelLimit { get; init; } = 1;
[CommandOption("media", Description = "Download referenced media content.")]
public bool ShouldDownloadMedia { get; init; }
@ -39,34 +50,93 @@ namespace DiscordChatExporter.Cli.Commands.Base
private ChannelExporter? _channelExporter;
protected ChannelExporter Exporter => _channelExporter ??= new ChannelExporter(Discord);
protected async ValueTask ExportChannelAsync(Guild guild, Channel channel, ProgressContext progressContext)
protected async ValueTask ExportAsync(IConsole console, IReadOnlyList<Channel> channels)
{
var request = new ExportRequest(
guild,
channel,
OutputPath,
ExportFormat,
After,
Before,
PartitionLimit,
ShouldDownloadMedia,
ShouldReuseMedia,
DateFormat
);
var progress = progressContext.AddTask(
$"{channel.Category} / {channel.Name}",
new ProgressTaskSettings {MaxValue = 1}
);
try
await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)...");
var errors = new ConcurrentDictionary<Channel, string>();
// Wrap everything in a progress ticker
await console.CreateProgressTicker().StartAsync(async progressContext =>
{
await channels.ParallelForEachAsync(async channel =>
{
// Export
try
{
var guild = await Discord.GetGuildAsync(channel.GuildId);
var request = new ExportRequest(
guild,
channel,
OutputPath,
ExportFormat,
After,
Before,
PartitionLimit,
ShouldDownloadMedia,
ShouldReuseMedia,
DateFormat
);
var progress = progressContext.AddTask(
$"{channel.Category} / {channel.Name}",
new ProgressTaskSettings {MaxValue = 1}
);
try
{
await Exporter.ExportChannelAsync(request, progress);
}
finally
{
progress.StopTask();
}
}
catch (DiscordChatExporterException ex) when (!ex.IsCritical)
{
errors[channel] = ex.Message;
}
}, ParallelLimit.ClampMin(1));
await console.Output.WriteLineAsync();
});
// Print result
using (console.WithForegroundColor(ConsoleColor.Green))
{
await Exporter.ExportChannelAsync(request, progress);
await console.Output.WriteLineAsync(
$"Successfully exported {channels.Count - errors.Count} channel(s)."
);
}
finally
// Print errors
if (errors.Any())
{
progress.StopTask();
await console.Output.WriteLineAsync();
using (console.WithForegroundColor(ConsoleColor.Red))
await console.Output.WriteLineAsync($"Failed to export {errors.Count} channel(s):");
foreach (var (channel, error) in errors)
{
await console.Output.WriteAsync($"{channel.Category} / {channel.Name}: ");
using (console.WithForegroundColor(ConsoleColor.Red))
await console.Output.WriteLineAsync(error);
}
await console.Output.WriteLineAsync();
}
// 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("Done.");
}
public override ValueTask ExecuteAsync(IConsole console)

@ -1,78 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Infrastructure;
using DiscordChatExporter.Cli.Utils.Extensions;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Exceptions;
using DiscordChatExporter.Core.Utils.Extensions;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Cli.Commands.Base
{
public abstract class ExportMultipleCommandBase : ExportCommandBase
{
[CommandOption("parallel", Description = "Limits how many channels can be exported in parallel.")]
public int ParallelLimit { get; init; } = 1;
protected async ValueTask ExportChannelsAsync(IConsole console, IReadOnlyList<Channel> channels)
{
await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)...");
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)."
);
// Print errors
if (errors.Any())
{
using (console.WithForegroundColor(ConsoleColor.Red))
await console.Output.WriteLineAsync($"Failed to export {errors.Count} channel(s):");
foreach (var (channel, error) in errors)
{
await console.Output.WriteAsync($"{channel.Category} / {channel.Name}: ");
using (console.WithForegroundColor(ConsoleColor.Red))
await console.Output.WriteLineAsync(error);
}
await console.Output.WriteLineAsync();
}
// 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("Done.");
}
}
}

@ -8,7 +8,7 @@ using DiscordChatExporter.Core.Discord.Data;
namespace DiscordChatExporter.Cli.Commands
{
[Command("exportall", Description = "Export all accessible channels.")]
public class ExportAllCommand : ExportMultipleCommandBase
public class ExportAllCommand : ExportCommandBase
{
[CommandOption("include-dm", Description = "Include direct message channels.")]
public bool IncludeDirectMessages { get; init; } = true;
@ -36,7 +36,7 @@ namespace DiscordChatExporter.Cli.Commands
}
// Export
await ExportChannelsAsync(console, channels);
await ExportAsync(console, channels);
}
}
}

@ -1,34 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Infrastructure;
using DiscordChatExporter.Cli.Commands.Base;
using DiscordChatExporter.Cli.Utils.Extensions;
using DiscordChatExporter.Core.Discord;
namespace DiscordChatExporter.Cli.Commands
{
[Command("export", Description = "Export a channel.")]
public class ExportChannelCommand : ExportCommandBase
{
[CommandOption("channel", 'c', IsRequired = true, Description = "Channel ID.")]
public Snowflake ChannelId { get; init; }
public override async ValueTask ExecuteAsync(IConsole console)
{
await base.ExecuteAsync(console);
// Get channel metadata
await console.Output.WriteLineAsync("Fetching channel...");
var channel = await Discord.GetChannelAsync(ChannelId);
var guild = await Discord.GetGuildAsync(channel.GuildId);
// Export
await console.Output.WriteLineAsync("Exporting...");
await console.CreateProgressTicker().StartAsync(async progressContext =>
{
await ExportChannelAsync(guild, channel, progressContext);
});
await console.Output.WriteLineAsync("Done.");
}
}
}

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Infrastructure;
using DiscordChatExporter.Cli.Commands.Base;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Discord.Data;
namespace DiscordChatExporter.Cli.Commands
{
[Command("export", Description = "Export one or multiple channels.")]
public class ExportChannelsCommand : ExportCommandBase
{
// TODO: change this to plural with a breaking change
[CommandOption("channel", 'c', IsRequired = true, Description = "Channel ID(s).")]
public IReadOnlyList<Snowflake> ChannelIds { get; init; } = Array.Empty<Snowflake>();
public override async ValueTask ExecuteAsync(IConsole console)
{
await base.ExecuteAsync(console);
// Get channel metadata
await console.Output.WriteLineAsync("Fetching channel(s)...");
var channels = new List<Channel>();
foreach (var channelId in ChannelIds)
{
var channel = await Discord.GetChannelAsync(channelId);
channels.Add(channel);
}
// Export
await ExportAsync(console, channels);
}
}
}

@ -8,7 +8,7 @@ using DiscordChatExporter.Core.Utils.Extensions;
namespace DiscordChatExporter.Cli.Commands
{
[Command("exportdm", Description = "Export all direct message channels.")]
public class ExportDirectMessagesCommand : ExportMultipleCommandBase
public class ExportDirectMessagesCommand : ExportCommandBase
{
public override async ValueTask ExecuteAsync(IConsole console)
{
@ -19,7 +19,7 @@ namespace DiscordChatExporter.Cli.Commands
var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id);
// Export
await ExportChannelsAsync(console, channels);
await ExportAsync(console, channels);
}
}
}

@ -8,7 +8,7 @@ using DiscordChatExporter.Core.Utils.Extensions;
namespace DiscordChatExporter.Cli.Commands
{
[Command("exportguild", Description = "Export all channels within specified guild.")]
public class ExportGuildCommand : ExportMultipleCommandBase
public class ExportGuildCommand : ExportCommandBase
{
[CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")]
public Snowflake GuildId { get; init; }
@ -22,7 +22,7 @@ namespace DiscordChatExporter.Cli.Commands
var channels = await Discord.GetGuildChannelsAsync(GuildId);
// Export
await ExportChannelsAsync(console, channels);
await ExportAsync(console, channels);
}
}
}
Loading…
Cancel
Save